From aa04dfabb4eeaca78698bc782ba0f14982b9bed0 Mon Sep 17 00:00:00 2001 From: Daniel Salazar Date: Wed, 25 Feb 2026 13:26:16 -0800 Subject: [PATCH] feat: add is_private to apps (#2546) --- .../src/modules/data-access/AppService.js | 3 ++ .../modules/data-access/AppService.test.js | 7 +++ src/backend/src/om/mappings/app.js | 4 ++ .../database/SqliteDatabaseAccessService.js | 49 ++++++++++--------- .../sqlite_setup/0046_is-private-apps.sql | 1 + 5 files changed, 41 insertions(+), 23 deletions(-) create mode 100644 src/backend/src/services/database/sqlite_setup/0046_is-private-apps.sql diff --git a/src/backend/src/modules/data-access/AppService.js b/src/backend/src/modules/data-access/AppService.js index 821abe7e9..82e950b40 100644 --- a/src/backend/src/modules/data-access/AppService.js +++ b/src/backend/src/modules/data-access/AppService.js @@ -276,6 +276,7 @@ export default class AppService extends BaseService { 'approved_for_opening_items', 'approved_for_incentive_program', 'godmode', + 'is_private', ]; static WRITE_ALL_OWNER_PERMISSION = 'system:es:write-all-owners'; @@ -379,6 +380,7 @@ export default class AppService extends BaseService { app.description = row.description; app.godmode = as_bool(row.godmode); app.icon = row.icon; + app.is_private = as_bool(row.is_private); app.index_url = row.index_url; app.maximize_on_start = as_bool(row.maximize_on_start); app.metadata = row.metadata; @@ -500,6 +502,7 @@ export default class AppService extends BaseService { app.description = row.description; app.godmode = as_bool(row.godmode); app.icon = row.icon; + app.is_private = as_bool(row.is_private); app.index_url = row.index_url; app.maximize_on_start = as_bool(row.maximize_on_start); app.metadata = row.metadata; diff --git a/src/backend/src/modules/data-access/AppService.test.js b/src/backend/src/modules/data-access/AppService.test.js index 66517e0ce..f90df9942 100644 --- a/src/backend/src/modules/data-access/AppService.test.js +++ b/src/backend/src/modules/data-access/AppService.test.js @@ -78,6 +78,7 @@ describe('AppService', () => { approved_for_opening_items: 1, background: 0, godmode: 0, + is_private: 0, maximize_on_start: 0, protected: 0, owner_user_id: 1, @@ -253,6 +254,7 @@ describe('AppService', () => { approved_for_opening_items: 0, background: '0', godmode: 1, + is_private: '1', maximize_on_start: '1', protected: 0, }); @@ -266,6 +268,7 @@ describe('AppService', () => { expect(result.approved_for_opening_items).toBe(false); expect(result.background).toBe(false); expect(result.godmode).toBe(true); + expect(result.is_private).toBe(true); expect(result.maximize_on_start).toBe(true); expect(result.protected).toBe(false); }); @@ -829,6 +832,7 @@ describe('AppService', () => { index_url: 'https://example.com', approved_for_listing: true, // read_only field godmode: true, // read_only field + is_private: true, // read_only field }, }); @@ -836,6 +840,7 @@ describe('AppService', () => { const writeCall = mockDbWrite.write.mock.calls[0]; expect(writeCall[0]).not.toContain('approved_for_listing'); expect(writeCall[0]).not.toContain('godmode'); + expect(writeCall[0]).not.toContain('is_private'); }); it('should handle name conflict with dedupe_name option', async () => { @@ -1253,12 +1258,14 @@ describe('AppService', () => { title: 'Updated', approved_for_listing: true, godmode: true, + is_private: true, }, }); const writeCall = mockDbWrite.write.mock.calls[0]; expect(writeCall[0]).not.toContain('approved_for_listing'); expect(writeCall[0]).not.toContain('godmode'); + expect(writeCall[0]).not.toContain('is_private'); }); it('should handle name change with conflict', async () => { diff --git a/src/backend/src/om/mappings/app.js b/src/backend/src/om/mappings/app.js index ab621ce40..8716b306d 100644 --- a/src/backend/src/om/mappings/app.js +++ b/src/backend/src/om/mappings/app.js @@ -121,6 +121,10 @@ module.exports = { protected: { type: 'flag', }, + is_private: { + type: 'flag', + read_only: true, + }, // OPERATIONS last_review: { diff --git a/src/backend/src/services/database/SqliteDatabaseAccessService.js b/src/backend/src/services/database/SqliteDatabaseAccessService.js index 7f10d7278..4348c31a1 100644 --- a/src/backend/src/services/database/SqliteDatabaseAccessService.js +++ b/src/backend/src/services/database/SqliteDatabaseAccessService.js @@ -173,6 +173,9 @@ class SqliteDatabaseAccessService extends BaseDatabaseAccessService { [41, [ '0045_user_oidc_providers.sql', ]], + [42, [ + '0046_is-private-apps.sql', + ]], ]; // Database upgrade logic @@ -218,31 +221,31 @@ class SqliteDatabaseAccessService extends BaseDatabaseAccessService { const basename = path_.basename(filename); const contents = fs.readFileSync(filename, 'utf8'); switch ( path_.extname(filename) ) { - case '.sql': - { - const stmts = contents.split(/;\s*\n/); - for ( let i = 0; i < stmts.length; i++ ) { - if ( stmts[i].trim() === '' ) continue; - const stmt = `${stmts[i] };`; - try { - this.db.exec(stmt); - } catch ( e ) { - throw new CompositeError(`failed to apply: ${basename} at line ${i}`, e); + case '.sql': + { + const stmts = contents.split(/;\s*\n/); + for ( let i = 0; i < stmts.length; i++ ) { + if ( stmts[i].trim() === '' ) continue; + const stmt = `${stmts[i] };`; + try { + this.db.exec(stmt); + } catch ( e ) { + throw new CompositeError(`failed to apply: ${basename} at line ${i}`, e); + } } + break; } - break; - } - case '.js': - try { - await this.run_js_migration_({ - filename, contents, - }); - } catch ( e ) { - throw new CompositeError(`failed to apply: ${basename}`, e); - } - break; - default: - throw new Error(`unrecognized migration type: ${filename}`); + case '.js': + try { + await this.run_js_migration_({ + filename, contents, + }); + } catch ( e ) { + throw new CompositeError(`failed to apply: ${basename}`, e); + } + break; + default: + throw new Error(`unrecognized migration type: ${filename}`); } } diff --git a/src/backend/src/services/database/sqlite_setup/0046_is-private-apps.sql b/src/backend/src/services/database/sqlite_setup/0046_is-private-apps.sql new file mode 100644 index 000000000..e79069509 --- /dev/null +++ b/src/backend/src/services/database/sqlite_setup/0046_is-private-apps.sql @@ -0,0 +1 @@ +ALTER TABLE apps ADD COLUMN "is_private" tinyint(1) DEFAULT '0';