From 7ada99acfc6e1d056a3a2bf4befb9521677c929d Mon Sep 17 00:00:00 2001 From: Maurice Date: Sat, 24 Oct 2026 23:22:09 +0000 Subject: [PATCH] fix(demo): the demo's addon set and the demo account's settings are written without awaiting in a loop One UPDATE per shelf and state and one upsert for the switches, inside the same transaction, and the demo account's settings in one upsert. The two new log lines go through the logger. --- server/src/db/repositories/DemoRepository.ts | 40 +++++++++++++------- server/src/demo/demo-reset.ts | 3 +- server/src/demo/demo-seed.ts | 8 +++- 3 files changed, 34 insertions(+), 17 deletions(-) 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 }; }