diff --git a/packages/plugins/duplicator/src/server/__tests__/dump.test.ts b/packages/plugins/duplicator/src/server/__tests__/dump.test.ts index 7360254c9..b6c119b41 100644 --- a/packages/plugins/duplicator/src/server/__tests__/dump.test.ts +++ b/packages/plugins/duplicator/src/server/__tests__/dump.test.ts @@ -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(); + }); }); diff --git a/packages/plugins/duplicator/src/server/dumper.ts b/packages/plugins/duplicator/src/server/dumper.ts index fb6b6b4f2..7762d142b 100644 --- a/packages/plugins/duplicator/src/server/dumper.ts +++ b/packages/plugins/duplicator/src/server/dumper.ts @@ -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'); diff --git a/packages/plugins/duplicator/src/server/restorer.ts b/packages/plugins/duplicator/src/server/restorer.ts index 3782cc78d..eed4daa36 100644 --- a/packages/plugins/duplicator/src/server/restorer.ts +++ b/packages/plugins/duplicator/src/server/restorer.ts @@ -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; + } + } + } + } }