feat: disable trigger when import collection (#1417)
* feat: dump with pg user defined functions * chore: restore db in restorer * chore: restore log * feat: disable trigger when import collection
This commit is contained in:
parent
8657710997
commit
debd95894f
@ -15,7 +15,7 @@ describe('dump', () => {
|
||||
let testDir: string;
|
||||
beforeEach(async () => {
|
||||
testDir = path.resolve(os.tmpdir(), `nocobase-dump-${Date.now()}`);
|
||||
|
||||
await fsPromises.mkdir(testDir, { recursive: true });
|
||||
app = mockServer();
|
||||
|
||||
db = app.db;
|
||||
@ -52,6 +52,7 @@ describe('dump', () => {
|
||||
fields: [],
|
||||
});
|
||||
|
||||
await app.cleanDb();
|
||||
await db.sync();
|
||||
});
|
||||
|
||||
@ -116,4 +117,52 @@ describe('dump', () => {
|
||||
|
||||
await db.sequelize.query(sql, { type: 'INSERT' });
|
||||
});
|
||||
|
||||
it('should dump user defined functions', async () => {
|
||||
if (db.sequelize.getDialect() !== 'postgres') {
|
||||
return;
|
||||
}
|
||||
|
||||
await db.sequelize.query(`
|
||||
CREATE OR REPLACE FUNCTION add(integer, integer) RETURNS integer
|
||||
AS 'select $1 + $2;'
|
||||
LANGUAGE SQL
|
||||
IMMUTABLE
|
||||
RETURNS NULL ON NULL INPUT;
|
||||
`);
|
||||
|
||||
await db.sequelize.query(`
|
||||
CREATE OR REPLACE FUNCTION trigger_function()
|
||||
RETURNS TRIGGER
|
||||
LANGUAGE PLPGSQL
|
||||
AS $$
|
||||
BEGIN
|
||||
-- trigger logic
|
||||
END;
|
||||
$$`);
|
||||
|
||||
await db.sequelize.query(`
|
||||
CREATE TRIGGER last_name_changes
|
||||
BEFORE UPDATE
|
||||
ON ${app.db.getCollection('users').model.tableName}
|
||||
FOR EACH ROW
|
||||
EXECUTE PROCEDURE trigger_function();
|
||||
`);
|
||||
|
||||
await db.sequelize.query(`
|
||||
CREATE OR REPLACE VIEW vistaView AS SELECT 'Hello World' as hello;
|
||||
`);
|
||||
|
||||
const dumper = new Dumper(app, {
|
||||
workDir: testDir,
|
||||
});
|
||||
|
||||
await dumper.dumpDb();
|
||||
|
||||
const restorer = new Restorer(app, {
|
||||
workDir: testDir,
|
||||
});
|
||||
|
||||
await restorer.importDb();
|
||||
});
|
||||
});
|
||||
|
@ -72,10 +72,70 @@ export class Dumper extends AppMigrator {
|
||||
}
|
||||
|
||||
await this.dumpMeta();
|
||||
await this.dumpDb();
|
||||
await this.packDumpedDir();
|
||||
await this.clearWorkDir();
|
||||
}
|
||||
|
||||
async dumpDb() {
|
||||
const db = this.app.db;
|
||||
const dialect = db.sequelize.getDialect();
|
||||
let sqlContent = [];
|
||||
if (dialect === 'postgres') {
|
||||
// get user defined functions in postgres
|
||||
const functions = await db.sequelize.query(
|
||||
`SELECT
|
||||
n.nspname AS function_schema,
|
||||
p.proname AS function_name,
|
||||
pg_get_functiondef(p.oid) AS def
|
||||
FROM
|
||||
pg_proc p
|
||||
LEFT JOIN pg_namespace n ON p.pronamespace = n.oid
|
||||
WHERE
|
||||
n.nspname NOT IN ('pg_catalog', 'information_schema')
|
||||
ORDER BY
|
||||
function_schema,
|
||||
function_name;`,
|
||||
{
|
||||
type: 'SELECT',
|
||||
},
|
||||
);
|
||||
|
||||
for (const f of functions) {
|
||||
sqlContent.push(f['def']);
|
||||
}
|
||||
|
||||
// get user defined triggers in postgres
|
||||
const triggers = await db.sequelize.query(`select pg_get_triggerdef(oid) from pg_trigger`, {
|
||||
type: 'SELECT',
|
||||
});
|
||||
|
||||
for (const t of triggers) {
|
||||
sqlContent.push(t['pg_get_triggerdef']);
|
||||
}
|
||||
|
||||
// get user defined views in postgres
|
||||
const views = await db.sequelize.query(
|
||||
`SELECT table_schema, table_name, pg_get_viewdef("table_name", true) as def
|
||||
FROM information_schema.views
|
||||
WHERE table_schema NOT IN ('information_schema', 'pg_catalog')
|
||||
ORDER BY table_schema, table_name`,
|
||||
{
|
||||
type: 'SELECT',
|
||||
},
|
||||
);
|
||||
|
||||
for (const v of views) {
|
||||
sqlContent.push(`CREATE OR REPLACE VIEW ${v['table_name']} AS ${v['def']}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (sqlContent.length > 0) {
|
||||
const dbDumpPath = path.resolve(this.workDir, 'db.sql');
|
||||
await fsPromises.writeFile(dbDumpPath, JSON.stringify(sqlContent), 'utf8');
|
||||
}
|
||||
}
|
||||
|
||||
async dumpMeta() {
|
||||
const metaPath = path.resolve(this.workDir, 'meta');
|
||||
|
||||
|
@ -6,7 +6,7 @@ import { AppMigrator } from './app-migrator';
|
||||
import { CollectionGroupManager } from './collection-group-manager';
|
||||
import { FieldValueWriter } from './field-value-writer';
|
||||
import { readLines, sqlAdapter } from './utils';
|
||||
|
||||
import fs from 'fs';
|
||||
export class Restorer extends AppMigrator {
|
||||
direction = 'restore' as const;
|
||||
|
||||
@ -31,6 +31,7 @@ export class Restorer extends AppMigrator {
|
||||
|
||||
await this.decompressBackup(filePath);
|
||||
await this.importCollections();
|
||||
await this.importDb();
|
||||
await this.clearWorkDir();
|
||||
}
|
||||
|
||||
@ -122,7 +123,18 @@ export class Restorer extends AppMigrator {
|
||||
const results = await inquirer.prompt(questions);
|
||||
|
||||
const importCollection = async (collectionName: string) => {
|
||||
const collectionMetaPath = path.resolve(this.workDir, 'collections', collectionName, 'meta');
|
||||
|
||||
const metaContent = await fsPromises.readFile(collectionMetaPath, 'utf8');
|
||||
const meta = JSON.parse(metaContent);
|
||||
const tableName = meta.tableName;
|
||||
|
||||
try {
|
||||
// disable trigger
|
||||
if (this.app.db.inDialect('postgres')) {
|
||||
await this.app.db.sequelize.query(`ALTER TABLE IF EXISTS "${tableName}" DISABLE TRIGGER ALL`);
|
||||
}
|
||||
|
||||
await this.importCollection({
|
||||
name: collectionName,
|
||||
});
|
||||
@ -130,6 +142,10 @@ export class Restorer extends AppMigrator {
|
||||
this.app.log.warn(`import collection ${collectionName} failed`, {
|
||||
err,
|
||||
});
|
||||
} finally {
|
||||
if (this.app.db.inDialect('postgres')) {
|
||||
await this.app.db.sequelize.query(`ALTER TABLE IF EXISTS "${tableName}" ENABLE TRIGGER ALL`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -199,6 +215,7 @@ export class Restorer extends AppMigrator {
|
||||
const metaContent = await fsPromises.readFile(collectionMetaPath, 'utf8');
|
||||
const meta = JSON.parse(metaContent);
|
||||
app.log.info(`collection meta ${metaContent}`);
|
||||
|
||||
const tableName = meta.tableName;
|
||||
|
||||
if (options.clear !== false) {
|
||||
@ -321,4 +338,30 @@ export class Restorer extends AppMigrator {
|
||||
|
||||
this.importedCollections.push(collection.name);
|
||||
}
|
||||
|
||||
async importDb() {
|
||||
const sqlFilePath = path.resolve(this.workDir, 'db.sql');
|
||||
// if db.sql file not exists, skip import
|
||||
if (!fs.existsSync(sqlFilePath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// read file content from db.sql
|
||||
const queriesContent = await fsPromises.readFile(sqlFilePath, 'utf8');
|
||||
|
||||
const queries = JSON.parse(queriesContent);
|
||||
|
||||
for (const sql of queries) {
|
||||
try {
|
||||
this.app.log.info(`import sql: ${sql}`);
|
||||
await this.app.db.sequelize.query(sql);
|
||||
} catch (e) {
|
||||
if (e.name === 'SequelizeDatabaseError') {
|
||||
this.app.logger.error(e.message);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user