@ministryofjustice/hmpps-digital-prison-reporting-frontend 7.3.1 → 7.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cjs/dpr/services/reportIdMigrationService.js +1 -23
- package/cjs/dpr/services/reportIdMigrationService.js.map +1 -1
- package/dpr/services/reportIdMigrationService.d.ts +0 -1
- package/dpr/services/reportIdMigrationService.js +1 -23
- package/dpr/services/reportIdMigrationService.js.map +1 -1
- package/package.json +1 -1
- package/src/dpr/services/reportIdMigrationService.d.ts +0 -1
|
@@ -14,7 +14,7 @@ class ReportIdMigrationService {
|
|
|
14
14
|
redisClient;
|
|
15
15
|
enabled;
|
|
16
16
|
static MIGRATION_KEY = 'migration:dpr-report-ids:v1';
|
|
17
|
-
static USER_CONFIG_PREFIX = '
|
|
17
|
+
static USER_CONFIG_PREFIX = 'userConfig:';
|
|
18
18
|
static MAX_RETRIES = 3;
|
|
19
19
|
constructor(redisClient, migrationServiceConfig) {
|
|
20
20
|
this.redisClient = redisClient;
|
|
@@ -41,7 +41,6 @@ class ReportIdMigrationService {
|
|
|
41
41
|
// return
|
|
42
42
|
// }
|
|
43
43
|
logger.default.info('Starting DPR report ID migration');
|
|
44
|
-
await this.debugLogs();
|
|
45
44
|
// 1. get all the user configs from redis
|
|
46
45
|
const configs = await this.getAllConfigs();
|
|
47
46
|
let updatedConfigs = 0;
|
|
@@ -221,27 +220,6 @@ class ReportIdMigrationService {
|
|
|
221
220
|
await this.redisClient.connect();
|
|
222
221
|
}
|
|
223
222
|
}
|
|
224
|
-
async debugLogs() {
|
|
225
|
-
const matchAllResults = await this.redisClient.scan('0', {
|
|
226
|
-
MATCH: '*',
|
|
227
|
-
COUNT: 100,
|
|
228
|
-
});
|
|
229
|
-
logger.default.info('MIGRATION_DEBUG: matchAllResults', JSON.stringify({
|
|
230
|
-
cursor: matchAllResults.cursor,
|
|
231
|
-
keyCount: matchAllResults.keys.length,
|
|
232
|
-
keys: matchAllResults.keys.slice(0, 20),
|
|
233
|
-
}));
|
|
234
|
-
const matchPrefixResults = await this.redisClient.scan('0', {
|
|
235
|
-
MATCH: 'dprReportStoreUser:*',
|
|
236
|
-
COUNT: 100,
|
|
237
|
-
});
|
|
238
|
-
logger.default.info('MIGRATION_DEBUG: matchPrefixResults', JSON.stringify({ matchPrefixResults }));
|
|
239
|
-
const userKeysResults = await this.getUserConfigKeys();
|
|
240
|
-
logger.default.info('MIGRATION_DEBUG: userKeysResults', JSON.stringify({
|
|
241
|
-
keyCount: userKeysResults.length,
|
|
242
|
-
sampleKeys: userKeysResults.slice(0, 10),
|
|
243
|
-
}));
|
|
244
|
-
}
|
|
245
223
|
}
|
|
246
224
|
|
|
247
225
|
exports.ReportIdMigrationService = ReportIdMigrationService;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reportIdMigrationService.js","sources":["../../../../../../../../../src/dpr/services/reportIdMigrationService.ts"],"sourcesContent":["import { RedisClient } from '../data/reportDataStore'\nimport { MigrationServiceConfig } from '../data/types'\nimport { ReportStoreConfig } from '../types/ReportStore'\nimport logger from '../utils/logger'\n\ninterface ConfigToMigrate {\n key: string\n config: ReportStoreConfig\n}\n\n/**\n * One-off migration that updates report IDs stored in Redis\n * from: abc123\n * to: dpr_abc123\n *\n * The migration is idempotent and will only be marked as complete once\n * all user configs have been successfully processed.\n */\nexport class ReportIdMigrationService {\n enabled: boolean\n\n private static readonly MIGRATION_KEY = 'migration:dpr-report-ids:v1'\n\n private static readonly USER_CONFIG_PREFIX = 'dprReportStoreUser:'\n\n private static readonly MAX_RETRIES = 3\n\n constructor(\n private readonly redisClient: RedisClient,\n migrationServiceConfig?: MigrationServiceConfig | undefined,\n ) {\n this.enabled = migrationServiceConfig ? Boolean(migrationServiceConfig.enabled) : false\n if (!this.enabled) logger.info(`Migration service: disabled `)\n\n this.redisClient.on('error', error => {\n logger.error(error, 'Redis error')\n })\n }\n\n /**\n * Runs the migration.\n */\n public async migrate(): Promise<void> {\n if (!this.enabled) {\n logger.info('DPR report ID migration service is disabled')\n\n return\n }\n\n await this.ensureConnected()\n\n // TEMPORARILY DISABLE DURING TESTING\n // const migrationComplete = await this.redisClient.get(ReportIdMigrationService.MIGRATION_KEY)\n // if (migrationComplete) {\n // logger.info('DPR report ID migration already completed')\n // return\n // }\n\n logger.info('Starting DPR report ID migration')\n\n await this.debugLogs()\n\n // 1. get all the user configs from redis\n const configs: ConfigToMigrate[] = await this.getAllConfigs()\n\n let updatedConfigs = 0\n\n const initialFailures = await Promise.all(\n // 2. Loop over user configs\n configs.map(async config => {\n try {\n // 3. Migrate IDs for all a users sub configs\n const updated = await this.migrateSingleConfig(config)\n\n if (updated) {\n updatedConfigs += 1\n }\n\n return null\n } catch (error) {\n logger.error(error, `Failed to migrate config for key ${config.key}`)\n // 4. return failed configs so can be retried\n return config\n }\n }),\n )\n\n // 6. Retry any failed configs - max retries = 3\n const remainingFailures = await this.retryFailedConfigs(\n initialFailures.filter((config): config is ConfigToMigrate => config !== null),\n )\n\n if (remainingFailures.length > 0) {\n // Dont mark as complete if there are any remaining failures\n logger.error(\n {\n failureCount: remainingFailures.length,\n failedKeys: remainingFailures.map(config => config.key),\n },\n 'DPR report ID migration failed. Migration flag will not be set.',\n )\n\n // Leaving the flag unset allows the migration to retry on the next startup.\n return\n }\n\n // 7. DONE Successfully - Set the completed flag globally\n await this.redisClient.set(ReportIdMigrationService.MIGRATION_KEY, 'true')\n\n logger.info(\n {\n totalConfigs: configs.length,\n updatedConfigs,\n },\n 'DPR report ID migration completed successfully',\n )\n }\n\n /**\n * Retries failed migrations up to the configured maximum number\n * of attempts.\n */\n private async retryFailedConfigs(failedConfigs: ConfigToMigrate[], attempt = 1): Promise<ConfigToMigrate[]> {\n if (failedConfigs.length === 0 || attempt > ReportIdMigrationService.MAX_RETRIES) {\n return failedConfigs\n }\n\n logger.warn(\n {\n attempt,\n remainingFailures: failedConfigs.length,\n },\n 'Retrying failed DPR report migrations',\n )\n\n const retryResults = await Promise.all(\n failedConfigs.map(async config => {\n try {\n await this.migrateSingleConfig(config)\n\n return null\n } catch (error) {\n logger.error(error, `Retry failed for key ${config.key}`)\n\n return config\n }\n }),\n )\n\n const remainingFailures = retryResults.filter((config): config is ConfigToMigrate => config !== null)\n\n return this.retryFailedConfigs(remainingFailures, attempt + 1)\n }\n\n /**\n * Migrates a single user config and persists it if any report IDs\n * were updated.\n */\n private async migrateSingleConfig(configToMigrate: ConfigToMigrate): Promise<boolean> {\n const { key, config } = configToMigrate\n\n const { updatedConfig, updated } = this.migrateConfig(config)\n\n if (!updated) {\n return false\n }\n\n await this.redisClient.set(key, JSON.stringify(updatedConfig))\n\n return true\n }\n\n /**\n * Retrieves every stored report config from Redis together with its\n * associated Redis key.\n */\n private async getAllConfigs(): Promise<ConfigToMigrate[]> {\n const keys = await this.getUserConfigKeys()\n\n if (keys.length === 0) {\n return []\n }\n\n // Fetch all user configs in a single Redis call.\n const values = await this.redisClient.mGet(keys)\n\n return keys.flatMap((key, index) => {\n const value = values[index]\n\n if (!value) {\n return []\n }\n\n try {\n return [\n {\n key,\n config: JSON.parse(value) as ReportStoreConfig,\n },\n ]\n } catch (error) {\n logger.error(error, `Failed to parse config for key ${key}`)\n\n return []\n }\n })\n }\n\n /**\n * Retrieves all Redis keys containing user report store data.\n */\n private async getUserConfigKeys(): Promise<string[]> {\n const scan = async (cursor = '0', keys: string[] = []): Promise<string[]> => {\n const result = await this.redisClient.scan(cursor, {\n MATCH: `${ReportIdMigrationService.USER_CONFIG_PREFIX}*`,\n })\n\n const nextKeys = [...keys, ...result.keys]\n\n return result.cursor === '0' ? nextKeys : scan(result.cursor, nextKeys)\n }\n\n return scan()\n }\n\n /**\n * Updates all report IDs in a user config and returns the updated\n * config along with whether any changes were made.\n */\n private migrateConfig(config: ReportStoreConfig): {\n updatedConfig: ReportStoreConfig\n updated: boolean\n } {\n const requestedReports = this.updateReportIds(config.requestedReports)\n\n const recentlyViewedReports = this.updateReportIds(config.recentlyViewedReports)\n\n const bookmarks = this.updateReportIds(config.bookmarks)\n\n const downloadPermissions = this.updateReportIds(config.downloadPermissions ?? [])\n\n const defaultFilters = this.updateReportIds(config.defaultFilters ?? [])\n\n const updated = [requestedReports, recentlyViewedReports, bookmarks, downloadPermissions, defaultFilters].some(\n result => result.updated,\n )\n\n return {\n updated,\n updatedConfig: {\n ...config,\n requestedReports: requestedReports.items,\n recentlyViewedReports: recentlyViewedReports.items,\n bookmarks: bookmarks.items,\n downloadPermissions: downloadPermissions.items,\n defaultFilters: defaultFilters.items,\n },\n }\n }\n\n /**\n * Updates report IDs in a collection and returns the updated\n * collection together with whether any items changed.\n */\n private updateReportIds<T extends { reportId: string }>(\n items: T[],\n ): {\n items: T[]\n updated: boolean\n } {\n let updated = false\n\n const updatedItems = items.map(item => {\n const newId = this.getMigratedReportId(item.reportId)\n\n if (newId === item.reportId) {\n return item\n }\n\n updated = true\n\n return {\n ...item,\n reportId: newId,\n }\n })\n\n return {\n items: updatedItems,\n updated,\n }\n }\n\n /**\n * Converts a legacy report ID into the new DPR-prefixed format.\n *\n * The method is intentionally idempotent so that rerunning the\n * migration will never double-prefix IDs.\n */\n private getMigratedReportId(reportId: string): string {\n // Already migrated IDs are returned unchanged.\n return reportId.startsWith('dpr_') ? reportId : `dpr_${reportId}`\n }\n\n private async ensureConnected(): Promise<void> {\n if (!this.redisClient.isOpen) {\n await this.redisClient.connect()\n }\n }\n\n private async debugLogs() {\n const matchAllResults = await this.redisClient.scan('0', {\n MATCH: '*',\n COUNT: 100,\n })\n\n logger.info(\n 'MIGRATION_DEBUG: matchAllResults',\n JSON.stringify({\n cursor: matchAllResults.cursor,\n keyCount: matchAllResults.keys.length,\n keys: matchAllResults.keys.slice(0, 20),\n }),\n )\n\n const matchPrefixResults = await this.redisClient.scan('0', {\n MATCH: 'dprReportStoreUser:*',\n COUNT: 100,\n })\n\n logger.info('MIGRATION_DEBUG: matchPrefixResults', JSON.stringify({ matchPrefixResults }))\n\n const userKeysResults = await this.getUserConfigKeys()\n\n logger.info(\n 'MIGRATION_DEBUG: userKeysResults',\n JSON.stringify({\n keyCount: userKeysResults.length,\n sampleKeys: userKeysResults.slice(0, 10),\n }),\n )\n }\n}\n"],"names":["logger"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"reportIdMigrationService.js","sources":["../../../../../../../../../src/dpr/services/reportIdMigrationService.ts"],"sourcesContent":["import { RedisClient } from '../data/reportDataStore'\nimport { MigrationServiceConfig } from '../data/types'\nimport { ReportStoreConfig } from '../types/ReportStore'\nimport logger from '../utils/logger'\n\ninterface ConfigToMigrate {\n key: string\n config: ReportStoreConfig\n}\n\n/**\n * One-off migration that updates report IDs stored in Redis\n * from: abc123\n * to: dpr_abc123\n *\n * The migration is idempotent and will only be marked as complete once\n * all user configs have been successfully processed.\n */\nexport class ReportIdMigrationService {\n enabled: boolean\n\n private static readonly MIGRATION_KEY = 'migration:dpr-report-ids:v1'\n\n private static readonly USER_CONFIG_PREFIX = 'userConfig:'\n\n private static readonly MAX_RETRIES = 3\n\n constructor(\n private readonly redisClient: RedisClient,\n migrationServiceConfig?: MigrationServiceConfig | undefined,\n ) {\n this.enabled = migrationServiceConfig ? Boolean(migrationServiceConfig.enabled) : false\n if (!this.enabled) logger.info(`Migration service: disabled `)\n\n this.redisClient.on('error', error => {\n logger.error(error, 'Redis error')\n })\n }\n\n /**\n * Runs the migration.\n */\n public async migrate(): Promise<void> {\n if (!this.enabled) {\n logger.info('DPR report ID migration service is disabled')\n\n return\n }\n\n await this.ensureConnected()\n\n // TEMPORARILY DISABLE DURING TESTING\n // const migrationComplete = await this.redisClient.get(ReportIdMigrationService.MIGRATION_KEY)\n // if (migrationComplete) {\n // logger.info('DPR report ID migration already completed')\n // return\n // }\n\n logger.info('Starting DPR report ID migration')\n\n // 1. get all the user configs from redis\n const configs: ConfigToMigrate[] = await this.getAllConfigs()\n\n let updatedConfigs = 0\n\n const initialFailures = await Promise.all(\n // 2. Loop over user configs\n configs.map(async config => {\n try {\n // 3. Migrate IDs for all a users sub configs\n const updated = await this.migrateSingleConfig(config)\n\n if (updated) {\n updatedConfigs += 1\n }\n\n return null\n } catch (error) {\n logger.error(error, `Failed to migrate config for key ${config.key}`)\n // 4. return failed configs so can be retried\n return config\n }\n }),\n )\n\n // 6. Retry any failed configs - max retries = 3\n const remainingFailures = await this.retryFailedConfigs(\n initialFailures.filter((config): config is ConfigToMigrate => config !== null),\n )\n\n if (remainingFailures.length > 0) {\n // Dont mark as complete if there are any remaining failures\n logger.error(\n {\n failureCount: remainingFailures.length,\n failedKeys: remainingFailures.map(config => config.key),\n },\n 'DPR report ID migration failed. Migration flag will not be set.',\n )\n\n // Leaving the flag unset allows the migration to retry on the next startup.\n return\n }\n\n // 7. DONE Successfully - Set the completed flag globally\n await this.redisClient.set(ReportIdMigrationService.MIGRATION_KEY, 'true')\n\n logger.info(\n {\n totalConfigs: configs.length,\n updatedConfigs,\n },\n 'DPR report ID migration completed successfully',\n )\n }\n\n /**\n * Retries failed migrations up to the configured maximum number\n * of attempts.\n */\n private async retryFailedConfigs(failedConfigs: ConfigToMigrate[], attempt = 1): Promise<ConfigToMigrate[]> {\n if (failedConfigs.length === 0 || attempt > ReportIdMigrationService.MAX_RETRIES) {\n return failedConfigs\n }\n\n logger.warn(\n {\n attempt,\n remainingFailures: failedConfigs.length,\n },\n 'Retrying failed DPR report migrations',\n )\n\n const retryResults = await Promise.all(\n failedConfigs.map(async config => {\n try {\n await this.migrateSingleConfig(config)\n\n return null\n } catch (error) {\n logger.error(error, `Retry failed for key ${config.key}`)\n\n return config\n }\n }),\n )\n\n const remainingFailures = retryResults.filter((config): config is ConfigToMigrate => config !== null)\n\n return this.retryFailedConfigs(remainingFailures, attempt + 1)\n }\n\n /**\n * Migrates a single user config and persists it if any report IDs\n * were updated.\n */\n private async migrateSingleConfig(configToMigrate: ConfigToMigrate): Promise<boolean> {\n const { key, config } = configToMigrate\n\n const { updatedConfig, updated } = this.migrateConfig(config)\n\n if (!updated) {\n return false\n }\n\n await this.redisClient.set(key, JSON.stringify(updatedConfig))\n\n return true\n }\n\n /**\n * Retrieves every stored report config from Redis together with its\n * associated Redis key.\n */\n private async getAllConfigs(): Promise<ConfigToMigrate[]> {\n const keys = await this.getUserConfigKeys()\n\n if (keys.length === 0) {\n return []\n }\n\n // Fetch all user configs in a single Redis call.\n const values = await this.redisClient.mGet(keys)\n\n return keys.flatMap((key, index) => {\n const value = values[index]\n\n if (!value) {\n return []\n }\n\n try {\n return [\n {\n key,\n config: JSON.parse(value) as ReportStoreConfig,\n },\n ]\n } catch (error) {\n logger.error(error, `Failed to parse config for key ${key}`)\n\n return []\n }\n })\n }\n\n /**\n * Retrieves all Redis keys containing user report store data.\n */\n private async getUserConfigKeys(): Promise<string[]> {\n const scan = async (cursor = '0', keys: string[] = []): Promise<string[]> => {\n const result = await this.redisClient.scan(cursor, {\n MATCH: `${ReportIdMigrationService.USER_CONFIG_PREFIX}*`,\n })\n\n const nextKeys = [...keys, ...result.keys]\n\n return result.cursor === '0' ? nextKeys : scan(result.cursor, nextKeys)\n }\n\n return scan()\n }\n\n /**\n * Updates all report IDs in a user config and returns the updated\n * config along with whether any changes were made.\n */\n private migrateConfig(config: ReportStoreConfig): {\n updatedConfig: ReportStoreConfig\n updated: boolean\n } {\n const requestedReports = this.updateReportIds(config.requestedReports)\n\n const recentlyViewedReports = this.updateReportIds(config.recentlyViewedReports)\n\n const bookmarks = this.updateReportIds(config.bookmarks)\n\n const downloadPermissions = this.updateReportIds(config.downloadPermissions ?? [])\n\n const defaultFilters = this.updateReportIds(config.defaultFilters ?? [])\n\n const updated = [requestedReports, recentlyViewedReports, bookmarks, downloadPermissions, defaultFilters].some(\n result => result.updated,\n )\n\n return {\n updated,\n updatedConfig: {\n ...config,\n requestedReports: requestedReports.items,\n recentlyViewedReports: recentlyViewedReports.items,\n bookmarks: bookmarks.items,\n downloadPermissions: downloadPermissions.items,\n defaultFilters: defaultFilters.items,\n },\n }\n }\n\n /**\n * Updates report IDs in a collection and returns the updated\n * collection together with whether any items changed.\n */\n private updateReportIds<T extends { reportId: string }>(\n items: T[],\n ): {\n items: T[]\n updated: boolean\n } {\n let updated = false\n\n const updatedItems = items.map(item => {\n const newId = this.getMigratedReportId(item.reportId)\n\n if (newId === item.reportId) {\n return item\n }\n\n updated = true\n\n return {\n ...item,\n reportId: newId,\n }\n })\n\n return {\n items: updatedItems,\n updated,\n }\n }\n\n /**\n * Converts a legacy report ID into the new DPR-prefixed format.\n *\n * The method is intentionally idempotent so that rerunning the\n * migration will never double-prefix IDs.\n */\n private getMigratedReportId(reportId: string): string {\n // Already migrated IDs are returned unchanged.\n return reportId.startsWith('dpr_') ? reportId : `dpr_${reportId}`\n }\n\n private async ensureConnected(): Promise<void> {\n if (!this.redisClient.isOpen) {\n await this.redisClient.connect()\n }\n }\n}\n"],"names":["logger"],"mappings":"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"}
|
|
@@ -12,7 +12,7 @@ class ReportIdMigrationService {
|
|
|
12
12
|
redisClient;
|
|
13
13
|
enabled;
|
|
14
14
|
static MIGRATION_KEY = 'migration:dpr-report-ids:v1';
|
|
15
|
-
static USER_CONFIG_PREFIX = '
|
|
15
|
+
static USER_CONFIG_PREFIX = 'userConfig:';
|
|
16
16
|
static MAX_RETRIES = 3;
|
|
17
17
|
constructor(redisClient, migrationServiceConfig) {
|
|
18
18
|
this.redisClient = redisClient;
|
|
@@ -39,7 +39,6 @@ class ReportIdMigrationService {
|
|
|
39
39
|
// return
|
|
40
40
|
// }
|
|
41
41
|
logger.info('Starting DPR report ID migration');
|
|
42
|
-
await this.debugLogs();
|
|
43
42
|
// 1. get all the user configs from redis
|
|
44
43
|
const configs = await this.getAllConfigs();
|
|
45
44
|
let updatedConfigs = 0;
|
|
@@ -219,27 +218,6 @@ class ReportIdMigrationService {
|
|
|
219
218
|
await this.redisClient.connect();
|
|
220
219
|
}
|
|
221
220
|
}
|
|
222
|
-
async debugLogs() {
|
|
223
|
-
const matchAllResults = await this.redisClient.scan('0', {
|
|
224
|
-
MATCH: '*',
|
|
225
|
-
COUNT: 100,
|
|
226
|
-
});
|
|
227
|
-
logger.info('MIGRATION_DEBUG: matchAllResults', JSON.stringify({
|
|
228
|
-
cursor: matchAllResults.cursor,
|
|
229
|
-
keyCount: matchAllResults.keys.length,
|
|
230
|
-
keys: matchAllResults.keys.slice(0, 20),
|
|
231
|
-
}));
|
|
232
|
-
const matchPrefixResults = await this.redisClient.scan('0', {
|
|
233
|
-
MATCH: 'dprReportStoreUser:*',
|
|
234
|
-
COUNT: 100,
|
|
235
|
-
});
|
|
236
|
-
logger.info('MIGRATION_DEBUG: matchPrefixResults', JSON.stringify({ matchPrefixResults }));
|
|
237
|
-
const userKeysResults = await this.getUserConfigKeys();
|
|
238
|
-
logger.info('MIGRATION_DEBUG: userKeysResults', JSON.stringify({
|
|
239
|
-
keyCount: userKeysResults.length,
|
|
240
|
-
sampleKeys: userKeysResults.slice(0, 10),
|
|
241
|
-
}));
|
|
242
|
-
}
|
|
243
221
|
}
|
|
244
222
|
|
|
245
223
|
export { ReportIdMigrationService };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reportIdMigrationService.js","sources":["../../../../../../../../src/dpr/services/reportIdMigrationService.ts"],"sourcesContent":["import { RedisClient } from '../data/reportDataStore'\nimport { MigrationServiceConfig } from '../data/types'\nimport { ReportStoreConfig } from '../types/ReportStore'\nimport logger from '../utils/logger'\n\ninterface ConfigToMigrate {\n key: string\n config: ReportStoreConfig\n}\n\n/**\n * One-off migration that updates report IDs stored in Redis\n * from: abc123\n * to: dpr_abc123\n *\n * The migration is idempotent and will only be marked as complete once\n * all user configs have been successfully processed.\n */\nexport class ReportIdMigrationService {\n enabled: boolean\n\n private static readonly MIGRATION_KEY = 'migration:dpr-report-ids:v1'\n\n private static readonly USER_CONFIG_PREFIX = 'dprReportStoreUser:'\n\n private static readonly MAX_RETRIES = 3\n\n constructor(\n private readonly redisClient: RedisClient,\n migrationServiceConfig?: MigrationServiceConfig | undefined,\n ) {\n this.enabled = migrationServiceConfig ? Boolean(migrationServiceConfig.enabled) : false\n if (!this.enabled) logger.info(`Migration service: disabled `)\n\n this.redisClient.on('error', error => {\n logger.error(error, 'Redis error')\n })\n }\n\n /**\n * Runs the migration.\n */\n public async migrate(): Promise<void> {\n if (!this.enabled) {\n logger.info('DPR report ID migration service is disabled')\n\n return\n }\n\n await this.ensureConnected()\n\n // TEMPORARILY DISABLE DURING TESTING\n // const migrationComplete = await this.redisClient.get(ReportIdMigrationService.MIGRATION_KEY)\n // if (migrationComplete) {\n // logger.info('DPR report ID migration already completed')\n // return\n // }\n\n logger.info('Starting DPR report ID migration')\n\n await this.debugLogs()\n\n // 1. get all the user configs from redis\n const configs: ConfigToMigrate[] = await this.getAllConfigs()\n\n let updatedConfigs = 0\n\n const initialFailures = await Promise.all(\n // 2. Loop over user configs\n configs.map(async config => {\n try {\n // 3. Migrate IDs for all a users sub configs\n const updated = await this.migrateSingleConfig(config)\n\n if (updated) {\n updatedConfigs += 1\n }\n\n return null\n } catch (error) {\n logger.error(error, `Failed to migrate config for key ${config.key}`)\n // 4. return failed configs so can be retried\n return config\n }\n }),\n )\n\n // 6. Retry any failed configs - max retries = 3\n const remainingFailures = await this.retryFailedConfigs(\n initialFailures.filter((config): config is ConfigToMigrate => config !== null),\n )\n\n if (remainingFailures.length > 0) {\n // Dont mark as complete if there are any remaining failures\n logger.error(\n {\n failureCount: remainingFailures.length,\n failedKeys: remainingFailures.map(config => config.key),\n },\n 'DPR report ID migration failed. Migration flag will not be set.',\n )\n\n // Leaving the flag unset allows the migration to retry on the next startup.\n return\n }\n\n // 7. DONE Successfully - Set the completed flag globally\n await this.redisClient.set(ReportIdMigrationService.MIGRATION_KEY, 'true')\n\n logger.info(\n {\n totalConfigs: configs.length,\n updatedConfigs,\n },\n 'DPR report ID migration completed successfully',\n )\n }\n\n /**\n * Retries failed migrations up to the configured maximum number\n * of attempts.\n */\n private async retryFailedConfigs(failedConfigs: ConfigToMigrate[], attempt = 1): Promise<ConfigToMigrate[]> {\n if (failedConfigs.length === 0 || attempt > ReportIdMigrationService.MAX_RETRIES) {\n return failedConfigs\n }\n\n logger.warn(\n {\n attempt,\n remainingFailures: failedConfigs.length,\n },\n 'Retrying failed DPR report migrations',\n )\n\n const retryResults = await Promise.all(\n failedConfigs.map(async config => {\n try {\n await this.migrateSingleConfig(config)\n\n return null\n } catch (error) {\n logger.error(error, `Retry failed for key ${config.key}`)\n\n return config\n }\n }),\n )\n\n const remainingFailures = retryResults.filter((config): config is ConfigToMigrate => config !== null)\n\n return this.retryFailedConfigs(remainingFailures, attempt + 1)\n }\n\n /**\n * Migrates a single user config and persists it if any report IDs\n * were updated.\n */\n private async migrateSingleConfig(configToMigrate: ConfigToMigrate): Promise<boolean> {\n const { key, config } = configToMigrate\n\n const { updatedConfig, updated } = this.migrateConfig(config)\n\n if (!updated) {\n return false\n }\n\n await this.redisClient.set(key, JSON.stringify(updatedConfig))\n\n return true\n }\n\n /**\n * Retrieves every stored report config from Redis together with its\n * associated Redis key.\n */\n private async getAllConfigs(): Promise<ConfigToMigrate[]> {\n const keys = await this.getUserConfigKeys()\n\n if (keys.length === 0) {\n return []\n }\n\n // Fetch all user configs in a single Redis call.\n const values = await this.redisClient.mGet(keys)\n\n return keys.flatMap((key, index) => {\n const value = values[index]\n\n if (!value) {\n return []\n }\n\n try {\n return [\n {\n key,\n config: JSON.parse(value) as ReportStoreConfig,\n },\n ]\n } catch (error) {\n logger.error(error, `Failed to parse config for key ${key}`)\n\n return []\n }\n })\n }\n\n /**\n * Retrieves all Redis keys containing user report store data.\n */\n private async getUserConfigKeys(): Promise<string[]> {\n const scan = async (cursor = '0', keys: string[] = []): Promise<string[]> => {\n const result = await this.redisClient.scan(cursor, {\n MATCH: `${ReportIdMigrationService.USER_CONFIG_PREFIX}*`,\n })\n\n const nextKeys = [...keys, ...result.keys]\n\n return result.cursor === '0' ? nextKeys : scan(result.cursor, nextKeys)\n }\n\n return scan()\n }\n\n /**\n * Updates all report IDs in a user config and returns the updated\n * config along with whether any changes were made.\n */\n private migrateConfig(config: ReportStoreConfig): {\n updatedConfig: ReportStoreConfig\n updated: boolean\n } {\n const requestedReports = this.updateReportIds(config.requestedReports)\n\n const recentlyViewedReports = this.updateReportIds(config.recentlyViewedReports)\n\n const bookmarks = this.updateReportIds(config.bookmarks)\n\n const downloadPermissions = this.updateReportIds(config.downloadPermissions ?? [])\n\n const defaultFilters = this.updateReportIds(config.defaultFilters ?? [])\n\n const updated = [requestedReports, recentlyViewedReports, bookmarks, downloadPermissions, defaultFilters].some(\n result => result.updated,\n )\n\n return {\n updated,\n updatedConfig: {\n ...config,\n requestedReports: requestedReports.items,\n recentlyViewedReports: recentlyViewedReports.items,\n bookmarks: bookmarks.items,\n downloadPermissions: downloadPermissions.items,\n defaultFilters: defaultFilters.items,\n },\n }\n }\n\n /**\n * Updates report IDs in a collection and returns the updated\n * collection together with whether any items changed.\n */\n private updateReportIds<T extends { reportId: string }>(\n items: T[],\n ): {\n items: T[]\n updated: boolean\n } {\n let updated = false\n\n const updatedItems = items.map(item => {\n const newId = this.getMigratedReportId(item.reportId)\n\n if (newId === item.reportId) {\n return item\n }\n\n updated = true\n\n return {\n ...item,\n reportId: newId,\n }\n })\n\n return {\n items: updatedItems,\n updated,\n }\n }\n\n /**\n * Converts a legacy report ID into the new DPR-prefixed format.\n *\n * The method is intentionally idempotent so that rerunning the\n * migration will never double-prefix IDs.\n */\n private getMigratedReportId(reportId: string): string {\n // Already migrated IDs are returned unchanged.\n return reportId.startsWith('dpr_') ? reportId : `dpr_${reportId}`\n }\n\n private async ensureConnected(): Promise<void> {\n if (!this.redisClient.isOpen) {\n await this.redisClient.connect()\n }\n }\n\n private async debugLogs() {\n const matchAllResults = await this.redisClient.scan('0', {\n MATCH: '*',\n COUNT: 100,\n })\n\n logger.info(\n 'MIGRATION_DEBUG: matchAllResults',\n JSON.stringify({\n cursor: matchAllResults.cursor,\n keyCount: matchAllResults.keys.length,\n keys: matchAllResults.keys.slice(0, 20),\n }),\n )\n\n const matchPrefixResults = await this.redisClient.scan('0', {\n MATCH: 'dprReportStoreUser:*',\n COUNT: 100,\n })\n\n logger.info('MIGRATION_DEBUG: matchPrefixResults', JSON.stringify({ matchPrefixResults }))\n\n const userKeysResults = await this.getUserConfigKeys()\n\n logger.info(\n 'MIGRATION_DEBUG: userKeysResults',\n JSON.stringify({\n keyCount: userKeysResults.length,\n sampleKeys: userKeysResults.slice(0, 10),\n }),\n )\n }\n}\n"],"names":[],"mappings":";;AAUA;;;;;;;AAOG;MACU,wBAAwB,CAAA;AAUhB,IAAA,WAAA;AATnB,IAAA,OAAO;AAEC,IAAA,OAAgB,aAAa,GAAG,6BAA6B;AAE7D,IAAA,OAAgB,kBAAkB,GAAG,qBAAqB;AAE1D,IAAA,OAAgB,WAAW,GAAG,CAAC;IAEvC,WAAA,CACmB,WAAwB,EACzC,sBAA2D,EAAA;QAD1C,IAAA,CAAA,WAAW,GAAX,WAAW;AAG5B,QAAA,IAAI,CAAC,OAAO,GAAG,sBAAsB,GAAG,OAAO,CAAC,sBAAsB,CAAC,OAAO,CAAC,GAAG,KAAK;QACvF,IAAI,CAAC,IAAI,CAAC,OAAO;AAAE,YAAA,MAAM,CAAC,IAAI,CAAC,CAAA,4BAAA,CAA8B,CAAC;QAE9D,IAAI,CAAC,WAAW,CAAC,EAAE,CAAC,OAAO,EAAE,KAAK,IAAG;AACnC,YAAA,MAAM,CAAC,KAAK,CAAC,KAAK,EAAE,aAAa,CAAC;AACpC,QAAA,CAAC,CAAC;IACJ;AAEA;;AAEG;AACI,IAAA,MAAM,OAAO,GAAA;AAClB,QAAA,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE;AACjB,YAAA,MAAM,CAAC,IAAI,CAAC,6CAA6C,CAAC;YAE1D;QACF;AAEA,QAAA,MAAM,IAAI,CAAC,eAAe,EAAE;;;;;;;AAS5B,QAAA,MAAM,CAAC,IAAI,CAAC,kCAAkC,CAAC;AAE/C,QAAA,MAAM,IAAI,CAAC,SAAS,EAAE;;AAGtB,QAAA,MAAM,OAAO,GAAsB,MAAM,IAAI,CAAC,aAAa,EAAE;QAE7D,IAAI,cAAc,GAAG,CAAC;AAEtB,QAAA,MAAM,eAAe,GAAG,MAAM,OAAO,CAAC,GAAG;;AAEvC,QAAA,OAAO,CAAC,GAAG,CAAC,OAAM,MAAM,KAAG;AACzB,YAAA,IAAI;;gBAEF,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,mBAAmB,CAAC,MAAM,CAAC;gBAEtD,IAAI,OAAO,EAAE;oBACX,cAAc,IAAI,CAAC;gBACrB;AAEA,gBAAA,OAAO,IAAI;YACb;YAAE,OAAO,KAAK,EAAE;gBACd,MAAM,CAAC,KAAK,CAAC,KAAK,EAAE,CAAA,iCAAA,EAAoC,MAAM,CAAC,GAAG,CAAA,CAAE,CAAC;;AAErE,gBAAA,OAAO,MAAM;YACf;QACF,CAAC,CAAC,CACH;;QAGD,MAAM,iBAAiB,GAAG,MAAM,IAAI,CAAC,kBAAkB,CACrD,eAAe,CAAC,MAAM,CAAC,CAAC,MAAM,KAAgC,MAAM,KAAK,IAAI,CAAC,CAC/E;AAED,QAAA,IAAI,iBAAiB,CAAC,MAAM,GAAG,CAAC,EAAE;;YAEhC,MAAM,CAAC,KAAK,CACV;gBACE,YAAY,EAAE,iBAAiB,CAAC,MAAM;AACtC,gBAAA,UAAU,EAAE,iBAAiB,CAAC,GAAG,CAAC,MAAM,IAAI,MAAM,CAAC,GAAG,CAAC;aACxD,EACD,iEAAiE,CAClE;;YAGD;QACF;;AAGA,QAAA,MAAM,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,wBAAwB,CAAC,aAAa,EAAE,MAAM,CAAC;QAE1E,MAAM,CAAC,IAAI,CACT;YACE,YAAY,EAAE,OAAO,CAAC,MAAM;YAC5B,cAAc;SACf,EACD,gDAAgD,CACjD;IACH;AAEA;;;AAGG;AACK,IAAA,MAAM,kBAAkB,CAAC,aAAgC,EAAE,OAAO,GAAG,CAAC,EAAA;AAC5E,QAAA,IAAI,aAAa,CAAC,MAAM,KAAK,CAAC,IAAI,OAAO,GAAG,wBAAwB,CAAC,WAAW,EAAE;AAChF,YAAA,OAAO,aAAa;QACtB;QAEA,MAAM,CAAC,IAAI,CACT;YACE,OAAO;YACP,iBAAiB,EAAE,aAAa,CAAC,MAAM;SACxC,EACD,uCAAuC,CACxC;AAED,QAAA,MAAM,YAAY,GAAG,MAAM,OAAO,CAAC,GAAG,CACpC,aAAa,CAAC,GAAG,CAAC,OAAM,MAAM,KAAG;AAC/B,YAAA,IAAI;AACF,gBAAA,MAAM,IAAI,CAAC,mBAAmB,CAAC,MAAM,CAAC;AAEtC,gBAAA,OAAO,IAAI;YACb;YAAE,OAAO,KAAK,EAAE;gBACd,MAAM,CAAC,KAAK,CAAC,KAAK,EAAE,CAAA,qBAAA,EAAwB,MAAM,CAAC,GAAG,CAAA,CAAE,CAAC;AAEzD,gBAAA,OAAO,MAAM;YACf;QACF,CAAC,CAAC,CACH;AAED,QAAA,MAAM,iBAAiB,GAAG,YAAY,CAAC,MAAM,CAAC,CAAC,MAAM,KAAgC,MAAM,KAAK,IAAI,CAAC;QAErG,OAAO,IAAI,CAAC,kBAAkB,CAAC,iBAAiB,EAAE,OAAO,GAAG,CAAC,CAAC;IAChE;AAEA;;;AAGG;IACK,MAAM,mBAAmB,CAAC,eAAgC,EAAA;AAChE,QAAA,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,eAAe;AAEvC,QAAA,MAAM,EAAE,aAAa,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC,aAAa,CAAC,MAAM,CAAC;QAE7D,IAAI,CAAC,OAAO,EAAE;AACZ,YAAA,OAAO,KAAK;QACd;AAEA,QAAA,MAAM,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,SAAS,CAAC,aAAa,CAAC,CAAC;AAE9D,QAAA,OAAO,IAAI;IACb;AAEA;;;AAGG;AACK,IAAA,MAAM,aAAa,GAAA;AACzB,QAAA,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,iBAAiB,EAAE;AAE3C,QAAA,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE;AACrB,YAAA,OAAO,EAAE;QACX;;QAGA,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC;QAEhD,OAAO,IAAI,CAAC,OAAO,CAAC,CAAC,GAAG,EAAE,KAAK,KAAI;AACjC,YAAA,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC;YAE3B,IAAI,CAAC,KAAK,EAAE;AACV,gBAAA,OAAO,EAAE;YACX;AAEA,YAAA,IAAI;gBACF,OAAO;AACL,oBAAA;wBACE,GAAG;AACH,wBAAA,MAAM,EAAE,IAAI,CAAC,KAAK,CAAC,KAAK,CAAsB;AAC/C,qBAAA;iBACF;YACH;YAAE,OAAO,KAAK,EAAE;gBACd,MAAM,CAAC,KAAK,CAAC,KAAK,EAAE,CAAA,+BAAA,EAAkC,GAAG,CAAA,CAAE,CAAC;AAE5D,gBAAA,OAAO,EAAE;YACX;AACF,QAAA,CAAC,CAAC;IACJ;AAEA;;AAEG;AACK,IAAA,MAAM,iBAAiB,GAAA;QAC7B,MAAM,IAAI,GAAG,OAAO,MAAM,GAAG,GAAG,EAAE,IAAA,GAAiB,EAAE,KAAuB;YAC1E,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,MAAM,EAAE;AACjD,gBAAA,KAAK,EAAE,CAAA,EAAG,wBAAwB,CAAC,kBAAkB,CAAA,CAAA,CAAG;AACzD,aAAA,CAAC;YAEF,MAAM,QAAQ,GAAG,CAAC,GAAG,IAAI,EAAE,GAAG,MAAM,CAAC,IAAI,CAAC;YAE1C,OAAO,MAAM,CAAC,MAAM,KAAK,GAAG,GAAG,QAAQ,GAAG,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC;AACzE,QAAA,CAAC;QAED,OAAO,IAAI,EAAE;IACf;AAEA;;;AAGG;AACK,IAAA,aAAa,CAAC,MAAyB,EAAA;QAI7C,MAAM,gBAAgB,GAAG,IAAI,CAAC,eAAe,CAAC,MAAM,CAAC,gBAAgB,CAAC;QAEtE,MAAM,qBAAqB,GAAG,IAAI,CAAC,eAAe,CAAC,MAAM,CAAC,qBAAqB,CAAC;QAEhF,MAAM,SAAS,GAAG,IAAI,CAAC,eAAe,CAAC,MAAM,CAAC,SAAS,CAAC;AAExD,QAAA,MAAM,mBAAmB,GAAG,IAAI,CAAC,eAAe,CAAC,MAAM,CAAC,mBAAmB,IAAI,EAAE,CAAC;AAElF,QAAA,MAAM,cAAc,GAAG,IAAI,CAAC,eAAe,CAAC,MAAM,CAAC,cAAc,IAAI,EAAE,CAAC;QAExE,MAAM,OAAO,GAAG,CAAC,gBAAgB,EAAE,qBAAqB,EAAE,SAAS,EAAE,mBAAmB,EAAE,cAAc,CAAC,CAAC,IAAI,CAC5G,MAAM,IAAI,MAAM,CAAC,OAAO,CACzB;QAED,OAAO;YACL,OAAO;AACP,YAAA,aAAa,EAAE;AACb,gBAAA,GAAG,MAAM;gBACT,gBAAgB,EAAE,gBAAgB,CAAC,KAAK;gBACxC,qBAAqB,EAAE,qBAAqB,CAAC,KAAK;gBAClD,SAAS,EAAE,SAAS,CAAC,KAAK;gBAC1B,mBAAmB,EAAE,mBAAmB,CAAC,KAAK;gBAC9C,cAAc,EAAE,cAAc,CAAC,KAAK;AACrC,aAAA;SACF;IACH;AAEA;;;AAGG;AACK,IAAA,eAAe,CACrB,KAAU,EAAA;QAKV,IAAI,OAAO,GAAG,KAAK;QAEnB,MAAM,YAAY,GAAG,KAAK,CAAC,GAAG,CAAC,IAAI,IAAG;YACpC,MAAM,KAAK,GAAG,IAAI,CAAC,mBAAmB,CAAC,IAAI,CAAC,QAAQ,CAAC;AAErD,YAAA,IAAI,KAAK,KAAK,IAAI,CAAC,QAAQ,EAAE;AAC3B,gBAAA,OAAO,IAAI;YACb;YAEA,OAAO,GAAG,IAAI;YAEd,OAAO;AACL,gBAAA,GAAG,IAAI;AACP,gBAAA,QAAQ,EAAE,KAAK;aAChB;AACH,QAAA,CAAC,CAAC;QAEF,OAAO;AACL,YAAA,KAAK,EAAE,YAAY;YACnB,OAAO;SACR;IACH;AAEA;;;;;AAKG;AACK,IAAA,mBAAmB,CAAC,QAAgB,EAAA;;AAE1C,QAAA,OAAO,QAAQ,CAAC,UAAU,CAAC,MAAM,CAAC,GAAG,QAAQ,GAAG,CAAA,IAAA,EAAO,QAAQ,EAAE;IACnE;AAEQ,IAAA,MAAM,eAAe,GAAA;AAC3B,QAAA,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,MAAM,EAAE;AAC5B,YAAA,MAAM,IAAI,CAAC,WAAW,CAAC,OAAO,EAAE;QAClC;IACF;AAEQ,IAAA,MAAM,SAAS,GAAA;QACrB,MAAM,eAAe,GAAG,MAAM,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,GAAG,EAAE;AACvD,YAAA,KAAK,EAAE,GAAG;AACV,YAAA,KAAK,EAAE,GAAG;AACX,SAAA,CAAC;QAEF,MAAM,CAAC,IAAI,CACT,kCAAkC,EAClC,IAAI,CAAC,SAAS,CAAC;YACb,MAAM,EAAE,eAAe,CAAC,MAAM;AAC9B,YAAA,QAAQ,EAAE,eAAe,CAAC,IAAI,CAAC,MAAM;YACrC,IAAI,EAAE,eAAe,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC;AACxC,SAAA,CAAC,CACH;QAED,MAAM,kBAAkB,GAAG,MAAM,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,GAAG,EAAE;AAC1D,YAAA,KAAK,EAAE,sBAAsB;AAC7B,YAAA,KAAK,EAAE,GAAG;AACX,SAAA,CAAC;AAEF,QAAA,MAAM,CAAC,IAAI,CAAC,qCAAqC,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,kBAAkB,EAAE,CAAC,CAAC;AAE1F,QAAA,MAAM,eAAe,GAAG,MAAM,IAAI,CAAC,iBAAiB,EAAE;QAEtD,MAAM,CAAC,IAAI,CACT,kCAAkC,EAClC,IAAI,CAAC,SAAS,CAAC;YACb,QAAQ,EAAE,eAAe,CAAC,MAAM;YAChC,UAAU,EAAE,eAAe,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC;AACzC,SAAA,CAAC,CACH;IACH;;;;;"}
|
|
1
|
+
{"version":3,"file":"reportIdMigrationService.js","sources":["../../../../../../../../src/dpr/services/reportIdMigrationService.ts"],"sourcesContent":["import { RedisClient } from '../data/reportDataStore'\nimport { MigrationServiceConfig } from '../data/types'\nimport { ReportStoreConfig } from '../types/ReportStore'\nimport logger from '../utils/logger'\n\ninterface ConfigToMigrate {\n key: string\n config: ReportStoreConfig\n}\n\n/**\n * One-off migration that updates report IDs stored in Redis\n * from: abc123\n * to: dpr_abc123\n *\n * The migration is idempotent and will only be marked as complete once\n * all user configs have been successfully processed.\n */\nexport class ReportIdMigrationService {\n enabled: boolean\n\n private static readonly MIGRATION_KEY = 'migration:dpr-report-ids:v1'\n\n private static readonly USER_CONFIG_PREFIX = 'userConfig:'\n\n private static readonly MAX_RETRIES = 3\n\n constructor(\n private readonly redisClient: RedisClient,\n migrationServiceConfig?: MigrationServiceConfig | undefined,\n ) {\n this.enabled = migrationServiceConfig ? Boolean(migrationServiceConfig.enabled) : false\n if (!this.enabled) logger.info(`Migration service: disabled `)\n\n this.redisClient.on('error', error => {\n logger.error(error, 'Redis error')\n })\n }\n\n /**\n * Runs the migration.\n */\n public async migrate(): Promise<void> {\n if (!this.enabled) {\n logger.info('DPR report ID migration service is disabled')\n\n return\n }\n\n await this.ensureConnected()\n\n // TEMPORARILY DISABLE DURING TESTING\n // const migrationComplete = await this.redisClient.get(ReportIdMigrationService.MIGRATION_KEY)\n // if (migrationComplete) {\n // logger.info('DPR report ID migration already completed')\n // return\n // }\n\n logger.info('Starting DPR report ID migration')\n\n // 1. get all the user configs from redis\n const configs: ConfigToMigrate[] = await this.getAllConfigs()\n\n let updatedConfigs = 0\n\n const initialFailures = await Promise.all(\n // 2. Loop over user configs\n configs.map(async config => {\n try {\n // 3. Migrate IDs for all a users sub configs\n const updated = await this.migrateSingleConfig(config)\n\n if (updated) {\n updatedConfigs += 1\n }\n\n return null\n } catch (error) {\n logger.error(error, `Failed to migrate config for key ${config.key}`)\n // 4. return failed configs so can be retried\n return config\n }\n }),\n )\n\n // 6. Retry any failed configs - max retries = 3\n const remainingFailures = await this.retryFailedConfigs(\n initialFailures.filter((config): config is ConfigToMigrate => config !== null),\n )\n\n if (remainingFailures.length > 0) {\n // Dont mark as complete if there are any remaining failures\n logger.error(\n {\n failureCount: remainingFailures.length,\n failedKeys: remainingFailures.map(config => config.key),\n },\n 'DPR report ID migration failed. Migration flag will not be set.',\n )\n\n // Leaving the flag unset allows the migration to retry on the next startup.\n return\n }\n\n // 7. DONE Successfully - Set the completed flag globally\n await this.redisClient.set(ReportIdMigrationService.MIGRATION_KEY, 'true')\n\n logger.info(\n {\n totalConfigs: configs.length,\n updatedConfigs,\n },\n 'DPR report ID migration completed successfully',\n )\n }\n\n /**\n * Retries failed migrations up to the configured maximum number\n * of attempts.\n */\n private async retryFailedConfigs(failedConfigs: ConfigToMigrate[], attempt = 1): Promise<ConfigToMigrate[]> {\n if (failedConfigs.length === 0 || attempt > ReportIdMigrationService.MAX_RETRIES) {\n return failedConfigs\n }\n\n logger.warn(\n {\n attempt,\n remainingFailures: failedConfigs.length,\n },\n 'Retrying failed DPR report migrations',\n )\n\n const retryResults = await Promise.all(\n failedConfigs.map(async config => {\n try {\n await this.migrateSingleConfig(config)\n\n return null\n } catch (error) {\n logger.error(error, `Retry failed for key ${config.key}`)\n\n return config\n }\n }),\n )\n\n const remainingFailures = retryResults.filter((config): config is ConfigToMigrate => config !== null)\n\n return this.retryFailedConfigs(remainingFailures, attempt + 1)\n }\n\n /**\n * Migrates a single user config and persists it if any report IDs\n * were updated.\n */\n private async migrateSingleConfig(configToMigrate: ConfigToMigrate): Promise<boolean> {\n const { key, config } = configToMigrate\n\n const { updatedConfig, updated } = this.migrateConfig(config)\n\n if (!updated) {\n return false\n }\n\n await this.redisClient.set(key, JSON.stringify(updatedConfig))\n\n return true\n }\n\n /**\n * Retrieves every stored report config from Redis together with its\n * associated Redis key.\n */\n private async getAllConfigs(): Promise<ConfigToMigrate[]> {\n const keys = await this.getUserConfigKeys()\n\n if (keys.length === 0) {\n return []\n }\n\n // Fetch all user configs in a single Redis call.\n const values = await this.redisClient.mGet(keys)\n\n return keys.flatMap((key, index) => {\n const value = values[index]\n\n if (!value) {\n return []\n }\n\n try {\n return [\n {\n key,\n config: JSON.parse(value) as ReportStoreConfig,\n },\n ]\n } catch (error) {\n logger.error(error, `Failed to parse config for key ${key}`)\n\n return []\n }\n })\n }\n\n /**\n * Retrieves all Redis keys containing user report store data.\n */\n private async getUserConfigKeys(): Promise<string[]> {\n const scan = async (cursor = '0', keys: string[] = []): Promise<string[]> => {\n const result = await this.redisClient.scan(cursor, {\n MATCH: `${ReportIdMigrationService.USER_CONFIG_PREFIX}*`,\n })\n\n const nextKeys = [...keys, ...result.keys]\n\n return result.cursor === '0' ? nextKeys : scan(result.cursor, nextKeys)\n }\n\n return scan()\n }\n\n /**\n * Updates all report IDs in a user config and returns the updated\n * config along with whether any changes were made.\n */\n private migrateConfig(config: ReportStoreConfig): {\n updatedConfig: ReportStoreConfig\n updated: boolean\n } {\n const requestedReports = this.updateReportIds(config.requestedReports)\n\n const recentlyViewedReports = this.updateReportIds(config.recentlyViewedReports)\n\n const bookmarks = this.updateReportIds(config.bookmarks)\n\n const downloadPermissions = this.updateReportIds(config.downloadPermissions ?? [])\n\n const defaultFilters = this.updateReportIds(config.defaultFilters ?? [])\n\n const updated = [requestedReports, recentlyViewedReports, bookmarks, downloadPermissions, defaultFilters].some(\n result => result.updated,\n )\n\n return {\n updated,\n updatedConfig: {\n ...config,\n requestedReports: requestedReports.items,\n recentlyViewedReports: recentlyViewedReports.items,\n bookmarks: bookmarks.items,\n downloadPermissions: downloadPermissions.items,\n defaultFilters: defaultFilters.items,\n },\n }\n }\n\n /**\n * Updates report IDs in a collection and returns the updated\n * collection together with whether any items changed.\n */\n private updateReportIds<T extends { reportId: string }>(\n items: T[],\n ): {\n items: T[]\n updated: boolean\n } {\n let updated = false\n\n const updatedItems = items.map(item => {\n const newId = this.getMigratedReportId(item.reportId)\n\n if (newId === item.reportId) {\n return item\n }\n\n updated = true\n\n return {\n ...item,\n reportId: newId,\n }\n })\n\n return {\n items: updatedItems,\n updated,\n }\n }\n\n /**\n * Converts a legacy report ID into the new DPR-prefixed format.\n *\n * The method is intentionally idempotent so that rerunning the\n * migration will never double-prefix IDs.\n */\n private getMigratedReportId(reportId: string): string {\n // Already migrated IDs are returned unchanged.\n return reportId.startsWith('dpr_') ? reportId : `dpr_${reportId}`\n }\n\n private async ensureConnected(): Promise<void> {\n if (!this.redisClient.isOpen) {\n await this.redisClient.connect()\n }\n }\n}\n"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ministryofjustice/hmpps-digital-prison-reporting-frontend",
|
|
3
|
-
"version": "7.3.
|
|
3
|
+
"version": "7.3.2",
|
|
4
4
|
"description": "The Digital Prison Reporting Frontend contains templates and code to help display data effectively in UI applications.",
|
|
5
5
|
"main": "./cjs/index.js",
|
|
6
6
|
"module": "./index.js",
|