From f5d796ea77ec887d02239c5278443fc21517b842 Mon Sep 17 00:00:00 2001 From: Ali BARIN Date: Mon, 7 Oct 2024 09:11:55 +0000 Subject: [PATCH] feat(app-config): persist relational virtual attrs --- ...ersist_virtual_attributes_in_app_config.js | 33 +++++++++++++++++ .../backend/src/models/app-auth-client.js | 30 ++++++++++++++++ packages/backend/src/models/app-config.js | 36 ++++++++++++------- .../backend/src/models/app-config.test.js | 16 ++++----- 4 files changed, 94 insertions(+), 21 deletions(-) create mode 100644 packages/backend/src/db/migrations/20241002121145_persist_virtual_attributes_in_app_config.js diff --git a/packages/backend/src/db/migrations/20241002121145_persist_virtual_attributes_in_app_config.js b/packages/backend/src/db/migrations/20241002121145_persist_virtual_attributes_in_app_config.js new file mode 100644 index 00000000..2559c66d --- /dev/null +++ b/packages/backend/src/db/migrations/20241002121145_persist_virtual_attributes_in_app_config.js @@ -0,0 +1,33 @@ +export async function up(knex) { + await knex.schema.alterTable('app_configs', (table) => { + table.boolean('can_connect').defaultTo(false); + }); + + const appConfigs = await knex('app_configs').select('*'); + + for (const appConfig of appConfigs) { + const appAuthClients = await knex('app_auth_clients').where( + 'app_key', + appConfig.key + ); + + const hasSomeActiveAppAuthClients = !!appAuthClients?.some( + (appAuthClient) => appAuthClient.active + ); + const shared = appConfig.shared; + const active = appConfig.disabled === false; + + const canConnectConditions = [hasSomeActiveAppAuthClients, shared, active]; + const canConnect = canConnectConditions.every(Boolean); + + await knex('app_configs') + .where('id', appConfig.id) + .update({ can_connect: canConnect }); + } +} + +export async function down(knex) { + await knex.schema.alterTable('app_configs', (table) => { + table.dropColumn('can_connect'); + }); +} diff --git a/packages/backend/src/models/app-auth-client.js b/packages/backend/src/models/app-auth-client.js index e4e9753e..ab209713 100644 --- a/packages/backend/src/models/app-auth-client.js +++ b/packages/backend/src/models/app-auth-client.js @@ -2,6 +2,7 @@ import AES from 'crypto-js/aes.js'; import enc from 'crypto-js/enc-utf8.js'; import appConfig from '../config/app.js'; import Base from './base.js'; +import AppConfig from './app-config.js'; class AppAuthClient extends Base { static tableName = 'app_auth_clients'; @@ -21,6 +22,17 @@ class AppAuthClient extends Base { }, }; + static relationMappings = () => ({ + appConfig: { + relation: Base.HasOneRelation, + modelClass: AppConfig, + join: { + from: 'app_auth_clients.app_key', + to: 'app_configs.key', + }, + }, + }); + encryptData() { if (!this.eligibleForEncryption()) return; @@ -48,6 +60,12 @@ class AppAuthClient extends Base { return this.authDefaults ? true : false; } + async triggerAppConfigUpdate() { + const appConfig = await this.$relatedQuery('appConfig').select('*'); + + await appConfig.$query().patch({}); + } + // TODO: Make another abstraction like beforeSave instead of using // beforeInsert and beforeUpdate separately for the same operation. async $beforeInsert(queryContext) { @@ -55,11 +73,23 @@ class AppAuthClient extends Base { this.encryptData(); } + async $afterInsert(queryContext) { + await super.$afterInsert(queryContext); + + await this.triggerAppConfigUpdate(); + } + async $beforeUpdate(opt, queryContext) { await super.$beforeUpdate(opt, queryContext); this.encryptData(); } + async $afterUpdate(queryContext) { + await super.$afterUpdate(queryContext); + + await this.triggerAppConfigUpdate(); + } + async $afterFind() { this.decryptData(); } diff --git a/packages/backend/src/models/app-config.js b/packages/backend/src/models/app-config.js index 885cbbb1..e72ef1e3 100644 --- a/packages/backend/src/models/app-config.js +++ b/packages/backend/src/models/app-config.js @@ -12,6 +12,7 @@ class AppConfig extends Base { properties: { id: { type: 'string', format: 'uuid' }, key: { type: 'string' }, + canConnect: { type: 'boolean', default: false }, allowCustomConnection: { type: 'boolean', default: false }, shared: { type: 'boolean', default: false }, disabled: { type: 'boolean', default: false }, @@ -32,30 +33,39 @@ class AppConfig extends Base { }); static get virtualAttributes() { - return ['canConnect', 'canCustomConnect']; + return ['canCustomConnect']; } get canCustomConnect() { return !this.disabled && this.allowCustomConnection; } - get canConnect() { - const hasSomeActiveAppAuthClients = !!this.appAuthClients?.some( - (appAuthClient) => appAuthClient.active - ); - const shared = this.shared; - const active = this.disabled === false; - - const conditions = [hasSomeActiveAppAuthClients, shared, active]; - - return conditions.every(Boolean); - } - async getApp() { if (!this.key) return null; return await App.findOneByKey(this.key); } + + async updateCanConnectValue() { + const appAuthClients = await this.$relatedQuery('appAuthClients'); + const hasSomeActiveAppAuthClients = !!appAuthClients?.some( + (appAuthClient) => appAuthClient.active + ); + const shared = this.shared; + const active = this.disabled === false; + + const conditions = [hasSomeActiveAppAuthClients, shared, active]; + + this.canConnect = conditions.every(Boolean); + + return this; + } + + async $beforeUpdate(opt, queryContext) { + await super.$beforeUpdate(opt, queryContext); + + await opt.old.updateCanConnectValue(); + } } export default AppConfig; diff --git a/packages/backend/src/models/app-config.test.js b/packages/backend/src/models/app-config.test.js index 7c14f19e..1c9f7651 100644 --- a/packages/backend/src/models/app-config.test.js +++ b/packages/backend/src/models/app-config.test.js @@ -36,14 +36,6 @@ describe('AppConfig model', () => { expect(AppConfig.virtualAttributes).toMatchSnapshot(); }); - it('getApp should return associated application', async () => { - const appConfig = await createAppConfig({ key: 'deepl' }); - - const app = await appConfig.getApp(); - - expect(app.key).toBe('deepl'); - }); - describe('canCustomConnect', () => { it('should return true when app is enabled and allows custom connection', async () => { const appConfig = await createAppConfig({ @@ -112,4 +104,12 @@ describe('AppConfig model', () => { expect(appConfig.canConnect).toBe(false); }); }); + + it('getApp should return associated application', async () => { + const appConfig = await createAppConfig({ key: 'deepl' }); + + const app = await appConfig.getApp(); + + expect(app.key).toBe('deepl'); + }); });