diff --git a/src/backend/src/CoreModule.js b/src/backend/src/CoreModule.js index 31f2fc37b..2f54eaff1 100644 --- a/src/backend/src/CoreModule.js +++ b/src/backend/src/CoreModule.js @@ -358,6 +358,9 @@ const install = async ({ services, app, useapi, modapi }) => { const { AppIconService } = require('./services/AppIconService'); services.registerService('app-icon', AppIconService); + + const { OldAppNameService } = require('./services/OldAppNameService'); + services.registerService('old-app-name', OldAppNameService); } const install_legacy = async ({ services }) => { diff --git a/src/backend/src/helpers.js b/src/backend/src/helpers.js index 51cc0da9b..7d16df646 100644 --- a/src/backend/src/helpers.js +++ b/src/backend/src/helpers.js @@ -1214,6 +1214,10 @@ async function app_name_exists(name){ let rows = await db.read(`SELECT EXISTS(SELECT 1 FROM apps WHERE apps.name=?) AS app_name_exists`, [name]); if(rows[0].app_name_exists) return true; + + const svc_oldAppName = services.get('old-app-name'); + const name_info = await svc_oldAppName.check_app_name(name); + if ( name_info ) return true; } function send_email_verification_code(email_confirm_code, email){ diff --git a/src/backend/src/om/entitystorage/AppES.js b/src/backend/src/om/entitystorage/AppES.js index 920c14ae4..9ae44e899 100644 --- a/src/backend/src/om/entitystorage/AppES.js +++ b/src/backend/src/om/entitystorage/AppES.js @@ -88,9 +88,9 @@ class AppES extends BaseES { async upsert (entity, extra) { if ( await app_name_exists(await entity.get('name')) ) { const { old_entity } = extra; - const throw_it = ( ! old_entity ) || + const is_name_change = ( ! old_entity ) || ( await old_entity.get('name') !== await entity.get('name') ); - if ( throw_it && extra.options && extra.options.dedupe_name ) { + if ( is_name_change && extra?.options?.dedupe_name ) { const base = await entity.get('name'); let number = 1; while ( await app_name_exists(`${base}-${number}`) ) { @@ -98,10 +98,21 @@ class AppES extends BaseES { } await entity.set('name', `${base}-${number}`) } - else if ( throw_it ) { - throw APIError.create('app_name_already_in_use', null, { - name: await entity.get('name') - }); + else if ( is_name_change ) { + // The name might be taken because it's the old name + // of this same app. If it is, the app takes it back. + const svc_oldAppName = this.context.get('services').get('old-app-name'); + const name_info = await svc_oldAppName.check_app_name(await entity.get('name')); + if ( ! name_info || name_info.app_uid !== await entity.get('uid') ) { + // Throw error because the name really is taken + throw APIError.create('app_name_already_in_use', null, { + name: await entity.get('name') + }); + } + + console.log('REMOVING NAME', name_info.id); + // Remove the old name from the old-app-name service + await svc_oldAppName.remove_name(name_info.id); } else { entity.del('name'); } @@ -147,6 +158,21 @@ class AppES extends BaseES { } } + const has_new_name = + extra.old_entity && ( + await entity.get('name') !== await extra.old_entity.get('name') + ); + + if ( has_new_name ) { + const svc_event = this.context.get('services').get('event'); + const event = { + app_uid: await entity.get('uid'), + new_name: await entity.get('name'), + old_name: await extra.old_entity.get('name'), + }; + await svc_event.emit('app.rename', event); + } + // Associate app with subdomain (if applicable) if ( subdomain_id ) { await this.db.write( diff --git a/src/backend/src/services/OldAppNameService.js b/src/backend/src/services/OldAppNameService.js new file mode 100644 index 000000000..138488b92 --- /dev/null +++ b/src/backend/src/services/OldAppNameService.js @@ -0,0 +1,63 @@ +const BaseService = require("./BaseService"); +const { DB_READ } = require("./database/consts"); + +const N_MONTHS = 4; + +class OldAppNameService extends BaseService { + _init () { + this.db = this.services.get('database').get(DB_READ, 'old-app-name'); + } + + async ['__on_boot.consolidation'] () { + const svc_event = this.services.get('event'); + svc_event.on('app.rename', async (_, { app_uid, old_name }) => { + this.log.noticeme('GOT EVENT', { app_uid, old_name }); + await this.db.write( + 'INSERT INTO `old_app_names` (`app_uid`, `name`) VALUES (?, ?)', + [app_uid, old_name] + ); + }); + } + + async check_app_name (name) { + const rows = await this.db.read( + 'SELECT * FROM `old_app_names` WHERE `name` = ?', + [name] + ); + + if ( rows.length === 0 ) return; + + // Check if the app has been renamed in the last N months + const [row] = rows; + const timestamp = new Date(row.timestamp); + + const age = Date.now() - timestamp.getTime(); + + const n_ms = N_MONTHS * 30 * 24 * 60 * 60 * 1000 + if ( age > n_ms ) { + // Remove record + await this.db.write( + 'DELETE FROM `old_app_names` WHERE `id` = ?', + [row.id] + ); + // Return undefined + return; + } + + return { + id: row.id, + app_uid: row.app_uid, + }; + } + + async remove_name (id) { + await this.db.write( + 'DELETE FROM `old_app_names` WHERE `id` = ?', + [id] + ); + } +} + +module.exports = { + OldAppNameService, +}; diff --git a/src/backend/src/services/database/SqliteDatabaseAccessService.js b/src/backend/src/services/database/SqliteDatabaseAccessService.js index 32b5a04cb..015713018 100644 --- a/src/backend/src/services/database/SqliteDatabaseAccessService.js +++ b/src/backend/src/services/database/SqliteDatabaseAccessService.js @@ -152,6 +152,9 @@ class SqliteDatabaseAccessService extends BaseDatabaseAccessService { [30, [ '0033_ai-usage.sql', ]], + [31, [ + '0034_app-redirect.sql', + ]], ]; // Database upgrade logic diff --git a/src/backend/src/services/database/sqlite_setup/0034_app-redirect.sql b/src/backend/src/services/database/sqlite_setup/0034_app-redirect.sql new file mode 100644 index 000000000..651be6c06 --- /dev/null +++ b/src/backend/src/services/database/sqlite_setup/0034_app-redirect.sql @@ -0,0 +1,10 @@ +CREATE TABLE `old_app_names` ( + `id` INTEGER PRIMARY KEY AUTOINCREMENT, + `app_uid` char(40) NOT NULL, + `name` varchar(100) NOT NULL UNIQUE, + `timestamp` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + + FOREIGN KEY (`app_uid`) REFERENCES `apps`(`uid`) ON DELETE CASCADE +); + +CREATE INDEX `idx_old_app_names_name` ON `old_app_names` (`name`);