2022-03-24 13:13:59 +00:00
|
|
|
import fs from 'fs/promises';
|
2022-11-07 07:52:43 +00:00
|
|
|
import { DatabaseError } from 'fyo/utils/errors';
|
2022-05-24 11:34:41 +00:00
|
|
|
import path from 'path';
|
2022-04-07 13:38:17 +00:00
|
|
|
import { DatabaseDemuxBase, DatabaseMethod } from 'utils/db/types';
|
2022-03-24 13:13:59 +00:00
|
|
|
import { getSchemas } from '../../schemas';
|
2023-04-04 06:21:00 +00:00
|
|
|
import { checkFileAccess, databaseMethodSet, unlinkIfExists } from '../helpers';
|
2022-03-24 13:13:59 +00:00
|
|
|
import patches from '../patches';
|
2022-04-11 06:04:55 +00:00
|
|
|
import { BespokeQueries } from './bespoke';
|
2022-03-24 13:13:59 +00:00
|
|
|
import DatabaseCore from './core';
|
|
|
|
import { runPatches } from './runPatch';
|
2022-04-11 06:04:55 +00:00
|
|
|
import { BespokeFunction, Patch } from './types';
|
2022-03-24 13:13:59 +00:00
|
|
|
|
2022-04-07 13:38:17 +00:00
|
|
|
export class DatabaseManager extends DatabaseDemuxBase {
|
2022-03-25 10:12:39 +00:00
|
|
|
db?: DatabaseCore;
|
2022-03-24 13:13:59 +00:00
|
|
|
|
2022-03-31 07:18:32 +00:00
|
|
|
get #isInitialized(): boolean {
|
|
|
|
return this.db !== undefined && this.db.knex !== undefined;
|
|
|
|
}
|
|
|
|
|
2022-03-31 12:04:30 +00:00
|
|
|
getSchemaMap() {
|
2023-05-11 04:53:07 +00:00
|
|
|
if (this.#isInitialized) {
|
|
|
|
return this.db?.schemaMap ?? getSchemas();
|
|
|
|
}
|
|
|
|
|
|
|
|
return getSchemas();
|
2022-03-31 12:04:30 +00:00
|
|
|
}
|
|
|
|
|
2022-04-18 06:42:56 +00:00
|
|
|
async createNewDatabase(dbPath: string, countryCode: string) {
|
2022-08-30 12:43:42 +00:00
|
|
|
await unlinkIfExists(dbPath);
|
2022-04-18 11:29:20 +00:00
|
|
|
return await this.connectToDatabase(dbPath, countryCode);
|
2022-03-24 13:13:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
async connectToDatabase(dbPath: string, countryCode?: string) {
|
2022-05-24 11:34:41 +00:00
|
|
|
countryCode = await this._connect(dbPath, countryCode);
|
|
|
|
await this.#migrate();
|
|
|
|
return countryCode;
|
|
|
|
}
|
2022-04-16 07:25:36 +00:00
|
|
|
|
2022-05-24 11:34:41 +00:00
|
|
|
async _connect(dbPath: string, countryCode?: string) {
|
|
|
|
countryCode ??= await DatabaseCore.getCountryCode(dbPath);
|
2022-03-24 13:13:59 +00:00
|
|
|
this.db = new DatabaseCore(dbPath);
|
2022-05-20 11:12:32 +00:00
|
|
|
await this.db.connect();
|
2022-03-24 13:13:59 +00:00
|
|
|
const schemaMap = getSchemas(countryCode);
|
|
|
|
this.db.setSchemaMap(schemaMap);
|
2022-04-18 06:42:56 +00:00
|
|
|
return countryCode;
|
2022-03-24 13:13:59 +00:00
|
|
|
}
|
|
|
|
|
2022-03-31 07:18:32 +00:00
|
|
|
async #migrate(): Promise<void> {
|
|
|
|
if (!this.#isInitialized) {
|
2022-03-25 10:12:39 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2022-03-31 07:18:32 +00:00
|
|
|
const isFirstRun = await this.#getIsFirstRun();
|
|
|
|
if (isFirstRun) {
|
|
|
|
await this.db!.migrate();
|
|
|
|
}
|
|
|
|
|
2022-05-24 11:34:41 +00:00
|
|
|
/**
|
2022-05-24 19:06:54 +00:00
|
|
|
* This needs to be supplimented with transactions
|
2022-05-24 11:34:41 +00:00
|
|
|
* TODO: Add transactions in core.ts
|
|
|
|
*/
|
|
|
|
const dbPath = this.db!.dbPath;
|
|
|
|
const copyPath = await this.#makeTempCopy();
|
2022-05-24 19:06:54 +00:00
|
|
|
|
2022-05-24 11:34:41 +00:00
|
|
|
try {
|
|
|
|
await this.#runPatchesAndMigrate();
|
2023-04-04 06:21:00 +00:00
|
|
|
} catch (error) {
|
|
|
|
await this.#handleFailedMigration(error, dbPath, copyPath);
|
2022-05-24 19:06:54 +00:00
|
|
|
} finally {
|
2022-08-30 12:43:42 +00:00
|
|
|
await unlinkIfExists(copyPath);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async #handleFailedMigration(
|
|
|
|
error: unknown,
|
|
|
|
dbPath: string,
|
|
|
|
copyPath: string | null
|
|
|
|
) {
|
|
|
|
await this.db!.close();
|
|
|
|
|
2023-04-04 06:21:00 +00:00
|
|
|
if (copyPath && (await checkFileAccess(copyPath))) {
|
|
|
|
await fs.copyFile(copyPath, dbPath);
|
2022-08-30 12:43:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if (error instanceof Error) {
|
|
|
|
error.message = `failed migration\n${error.message}`;
|
|
|
|
}
|
|
|
|
|
|
|
|
throw error;
|
|
|
|
}
|
|
|
|
|
2022-05-24 11:34:41 +00:00
|
|
|
async #runPatchesAndMigrate() {
|
2022-03-24 13:13:59 +00:00
|
|
|
const patchesToExecute = await this.#getPatchesToExecute();
|
2022-05-24 19:06:54 +00:00
|
|
|
|
|
|
|
patchesToExecute.sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
|
2022-03-24 13:13:59 +00:00
|
|
|
const preMigrationPatches = patchesToExecute.filter(
|
|
|
|
(p) => p.patch.beforeMigrate
|
|
|
|
);
|
|
|
|
const postMigrationPatches = patchesToExecute.filter(
|
|
|
|
(p) => !p.patch.beforeMigrate
|
|
|
|
);
|
|
|
|
|
|
|
|
await runPatches(preMigrationPatches, this);
|
2022-03-31 07:18:32 +00:00
|
|
|
await this.db!.migrate();
|
2022-03-24 13:13:59 +00:00
|
|
|
await runPatches(postMigrationPatches, this);
|
|
|
|
}
|
|
|
|
|
|
|
|
async #getPatchesToExecute(): Promise<Patch[]> {
|
2022-03-25 10:12:39 +00:00
|
|
|
if (this.db === undefined) {
|
|
|
|
return [];
|
|
|
|
}
|
|
|
|
|
|
|
|
const query: { name: string }[] = await this.db.knex!('PatchRun').select(
|
|
|
|
'name'
|
|
|
|
);
|
2022-03-24 13:13:59 +00:00
|
|
|
const executedPatches = query.map((q) => q.name);
|
|
|
|
return patches.filter((p) => !executedPatches.includes(p.name));
|
|
|
|
}
|
|
|
|
|
2022-05-24 11:34:41 +00:00
|
|
|
async call(method: DatabaseMethod, ...args: unknown[]) {
|
|
|
|
if (!this.#isInitialized) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!databaseMethodSet.has(method)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// @ts-ignore
|
|
|
|
const response = await this.db[method](...args);
|
|
|
|
if (method === 'close') {
|
|
|
|
delete this.db;
|
|
|
|
}
|
|
|
|
|
|
|
|
return response;
|
|
|
|
}
|
|
|
|
|
|
|
|
async callBespoke(method: string, ...args: unknown[]): Promise<unknown> {
|
|
|
|
if (!this.#isInitialized) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!BespokeQueries.hasOwnProperty(method)) {
|
2022-11-07 07:52:43 +00:00
|
|
|
throw new DatabaseError(`invalid bespoke db function ${method}`);
|
2022-05-24 11:34:41 +00:00
|
|
|
}
|
|
|
|
|
2023-04-04 06:21:00 +00:00
|
|
|
const queryFunction: BespokeFunction =
|
|
|
|
BespokeQueries[method as keyof BespokeFunction];
|
2022-05-24 11:34:41 +00:00
|
|
|
return await queryFunction(this.db!, ...args);
|
|
|
|
}
|
|
|
|
|
2022-03-31 07:18:32 +00:00
|
|
|
async #getIsFirstRun(): Promise<boolean> {
|
|
|
|
if (!this.#isInitialized) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
const tableList: unknown[] = await this.db!.knex!.raw(
|
|
|
|
"SELECT name FROM sqlite_master WHERE type='table'"
|
|
|
|
);
|
|
|
|
return tableList.length === 0;
|
|
|
|
}
|
2022-05-24 11:34:41 +00:00
|
|
|
|
|
|
|
async #makeTempCopy() {
|
|
|
|
const src = this.db!.dbPath;
|
2022-05-31 06:09:45 +00:00
|
|
|
if (src === ':memory:') {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2022-05-24 19:06:54 +00:00
|
|
|
const dir = path.parse(src).dir;
|
|
|
|
const dest = path.join(dir, '__premigratory_temp.db');
|
2022-08-30 12:43:42 +00:00
|
|
|
|
|
|
|
try {
|
|
|
|
await fs.copyFile(src, dest);
|
|
|
|
} catch (err) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2022-05-24 11:34:41 +00:00
|
|
|
return dest;
|
|
|
|
}
|
2022-03-24 13:13:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
export default new DatabaseManager();
|