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;
|
let testDir: string;
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
testDir = path.resolve(os.tmpdir(), `nocobase-dump-${Date.now()}`);
|
testDir = path.resolve(os.tmpdir(), `nocobase-dump-${Date.now()}`);
|
||||||
|
await fsPromises.mkdir(testDir, { recursive: true });
|
||||||
app = mockServer();
|
app = mockServer();
|
||||||
|
|
||||||
db = app.db;
|
db = app.db;
|
||||||
@ -52,6 +52,7 @@ describe('dump', () => {
|
|||||||
fields: [],
|
fields: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await app.cleanDb();
|
||||||
await db.sync();
|
await db.sync();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -116,4 +117,52 @@ describe('dump', () => {
|
|||||||
|
|
||||||
await db.sequelize.query(sql, { type: 'INSERT' });
|
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.dumpMeta();
|
||||||
|
await this.dumpDb();
|
||||||
await this.packDumpedDir();
|
await this.packDumpedDir();
|
||||||
await this.clearWorkDir();
|
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() {
|
async dumpMeta() {
|
||||||
const metaPath = path.resolve(this.workDir, 'meta');
|
const metaPath = path.resolve(this.workDir, 'meta');
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ import { AppMigrator } from './app-migrator';
|
|||||||
import { CollectionGroupManager } from './collection-group-manager';
|
import { CollectionGroupManager } from './collection-group-manager';
|
||||||
import { FieldValueWriter } from './field-value-writer';
|
import { FieldValueWriter } from './field-value-writer';
|
||||||
import { readLines, sqlAdapter } from './utils';
|
import { readLines, sqlAdapter } from './utils';
|
||||||
|
import fs from 'fs';
|
||||||
export class Restorer extends AppMigrator {
|
export class Restorer extends AppMigrator {
|
||||||
direction = 'restore' as const;
|
direction = 'restore' as const;
|
||||||
|
|
||||||
@ -31,6 +31,7 @@ export class Restorer extends AppMigrator {
|
|||||||
|
|
||||||
await this.decompressBackup(filePath);
|
await this.decompressBackup(filePath);
|
||||||
await this.importCollections();
|
await this.importCollections();
|
||||||
|
await this.importDb();
|
||||||
await this.clearWorkDir();
|
await this.clearWorkDir();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -122,7 +123,18 @@ export class Restorer extends AppMigrator {
|
|||||||
const results = await inquirer.prompt(questions);
|
const results = await inquirer.prompt(questions);
|
||||||
|
|
||||||
const importCollection = async (collectionName: string) => {
|
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 {
|
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({
|
await this.importCollection({
|
||||||
name: collectionName,
|
name: collectionName,
|
||||||
});
|
});
|
||||||
@ -130,6 +142,10 @@ export class Restorer extends AppMigrator {
|
|||||||
this.app.log.warn(`import collection ${collectionName} failed`, {
|
this.app.log.warn(`import collection ${collectionName} failed`, {
|
||||||
err,
|
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 metaContent = await fsPromises.readFile(collectionMetaPath, 'utf8');
|
||||||
const meta = JSON.parse(metaContent);
|
const meta = JSON.parse(metaContent);
|
||||||
app.log.info(`collection meta ${metaContent}`);
|
app.log.info(`collection meta ${metaContent}`);
|
||||||
|
|
||||||
const tableName = meta.tableName;
|
const tableName = meta.tableName;
|
||||||
|
|
||||||
if (options.clear !== false) {
|
if (options.clear !== false) {
|
||||||
@ -321,4 +338,30 @@ export class Restorer extends AppMigrator {
|
|||||||
|
|
||||||
this.importedCollections.push(collection.name);
|
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