diff --git a/server/src/db/repositories/DemoRepository.ts b/server/src/db/repositories/DemoRepository.ts index 0a0a4a9db..f8b89576e 100644 --- a/server/src/db/repositories/DemoRepository.ts +++ b/server/src/db/repositories/DemoRepository.ts @@ -420,29 +420,41 @@ export class DemoRepository { /** * Writes the demo's addon states in one transaction, so a reset that fails - * halfway does not leave half the shelf switched. A named row the database - * does not hold is an update that matches nothing, never an insert: the - * seeders own which rows exist. + * halfway does not leave half the shelf switched. One UPDATE per shelf and + * state (generic inherited `nativeUpdate` over `id IN (...)`, the statement + * `setEnabled` issues for one row) and one upsert for all the switches + * (`AppSettingsRepository#setValue`'s), however long the lists are. A named + * row the database does not hold, like an empty list, is an update that + * matches nothing, never an insert: the seeders own which rows exist. */ async applyAddonStates(states: DemoAddonStates): Promise { this.validateRequestContext(); + const switches = Object.entries(states.appSettings).map(([key, value]) => ({ key, value })); await this.em.transactional(async () => { const addons = this.em.getRepository(Addons); - for (const id of states.addons.on) await addons.setEnabled(id, true); - for (const id of states.addons.off) await addons.setEnabled(id, false); + await addons.nativeUpdate({ id: { $in: [...states.addons.on] } }, { enabled: true }); + await addons.nativeUpdate({ id: { $in: [...states.addons.off] } }, { enabled: false }); const photos = this.em.getRepository(PhotoProviders); - for (const id of states.photoProviders.on) await photos.setEnabled(id, 1); - for (const id of states.photoProviders.off) await photos.setEnabled(id, 0); + await photos.nativeUpdate({ id: { $in: [...states.photoProviders.on] } }, { enabled: 1 }); + await photos.nativeUpdate({ id: { $in: [...states.photoProviders.off] } }, { enabled: 0 }); const documents = this.em.getRepository(DocumentProviders); - for (const id of states.documentProviders.on) await documents.setEnabled(id, 1); - for (const id of states.documentProviders.off) await documents.setEnabled(id, 0); - const settings = this.em.getRepository(AppSettings); - for (const [key, value] of Object.entries(states.appSettings)) await settings.setValue(key, value); + await documents.nativeUpdate({ id: { $in: [...states.documentProviders.on] } }, { enabled: 1 }); + await documents.nativeUpdate({ id: { $in: [...states.documentProviders.off] } }, { enabled: 0 }); + await this.em + .getRepository(AppSettings) + .upsertMany(switches, { onConflictFields: ['key'], onConflictAction: 'merge' }); }); } - /** A user's own settings row, written as given (the demo account's fixed language). Composes `SettingsRepository#upsertForUser`. */ - async setUserSetting(userId: number, key: string, value: string): Promise { - await this.em.getRepository(Settings).upsertForUser(userId, key, value); + /** + * A user's own settings rows, written as given in one statement (the demo + * account's fixed language). The upsert `SettingsRepository#upsertForUser` + * issues for one key, for every row at once. + */ + async setUserSettings(userId: number, rows: readonly { key: string; value: string }[]): Promise { + await this.em.getRepository(Settings).upsertMany( + rows.map(({ key, value }) => ({ user: userId, key, value })), + { onConflictFields: ['user', 'key'], onConflictAction: 'merge' }, + ); } } diff --git a/server/src/demo/demo-reset.ts b/server/src/demo/demo-reset.ts index f28dd98dd..df2724322 100644 --- a/server/src/demo/demo-reset.ts +++ b/server/src/demo/demo-reset.ts @@ -8,6 +8,7 @@ import { } from '../db/repositories/DemoRepository'; import type { CarriedUserSessionRow } from '../db/repositories/UserSessions.repository'; import { dbNow } from '../db/types'; +import { logError } from '../nest/audit/audit-log.logger'; import { DatabaseConnectionLostError, type DatabaseBackupStrategy } from '../nest/database/database-backup.interface'; import { applyDemoAddonPolicy } from './demo-addon-policy'; import { RequestContext, type EntityManager } from '@mikro-orm/core'; @@ -163,7 +164,7 @@ async function resetDemoUser(database: DatabaseBackupStrategy): Promise { try { await applyDemoAddonPolicy(demoFresh); } catch (e: unknown) { - console.error('[Demo Reset] Failed to apply the demo addon set:', e instanceof Error ? e.message : e); + logError(`[Demo Reset] Failed to apply the demo addon set: ${e instanceof Error ? e.message : String(e)}`); } }); diff --git a/server/src/demo/demo-seed.ts b/server/src/demo/demo-seed.ts index 853c8f755..cd0fd9efc 100644 --- a/server/src/demo/demo-seed.ts +++ b/server/src/demo/demo-seed.ts @@ -1,5 +1,6 @@ import { readEnv } from '../app-config'; import { DemoRepository, type NewDemoPlaceRow } from '../db/repositories/DemoRepository'; +import { logInfo } from '../nest/audit/audit-log.logger'; import { DEMO_ACCOUNT_SETTINGS, DEMO_PASS } from '../nest/common/demo'; import { applyDemoAddonPolicy } from './demo-addon-policy'; // Static like in demo-reset.job.ts: the module top is inert, everything that @@ -59,7 +60,10 @@ async function seedDemoData(): Promise<{ adminId: number; demoId: number }> { // (the hourly reset included), so neither an admin's toggle nor an old // baseline outlasts the hour. await applyDemoAddonPolicy(demo); - for (const [key, value] of Object.entries(DEMO_ACCOUNT_SETTINGS)) await demo.setUserSetting(demoId, key, value); + await demo.setUserSettings( + demoId, + Object.entries(DEMO_ACCOUNT_SETTINGS).map(([key, value]) => ({ key, value })), + ); // Check if admin already has example trips const adminTripCount = await demo.countTripsByUser(adminId); @@ -75,7 +79,7 @@ async function seedDemoData(): Promise<{ adminId: number; demoId: number }> { // and the mark below would save them into the next baseline. const demoTripCount = await demo.countTripsByUser(demoId); if (demoTripCount > 0) { - console.log('[Demo] The demo account owns its own trips, skipping the example trips'); + logInfo('[Demo] The demo account owns its own trips, skipping the example trips'); return { adminId, demoId }; }