@ministryofjustice/hmpps-digital-prison-reporting-frontend 7.3.0 → 7.3.1

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.
@@ -34,12 +34,14 @@ class ReportIdMigrationService {
34
34
  return;
35
35
  }
36
36
  await this.ensureConnected();
37
- const migrationComplete = await this.redisClient.get(ReportIdMigrationService.MIGRATION_KEY);
38
- if (migrationComplete) {
39
- logger.default.info('DPR report ID migration already completed');
40
- return;
41
- }
37
+ // TEMPORARILY DISABLE DURING TESTING
38
+ // const migrationComplete = await this.redisClient.get(ReportIdMigrationService.MIGRATION_KEY)
39
+ // if (migrationComplete) {
40
+ // logger.info('DPR report ID migration already completed')
41
+ // return
42
+ // }
42
43
  logger.default.info('Starting DPR report ID migration');
44
+ await this.debugLogs();
43
45
  // 1. get all the user configs from redis
44
46
  const configs = await this.getAllConfigs();
45
47
  let updatedConfigs = 0;
@@ -219,6 +221,27 @@ class ReportIdMigrationService {
219
221
  await this.redisClient.connect();
220
222
  }
221
223
  }
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
+ }
222
245
  }
223
246
 
224
247
  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 const migrationComplete = await this.redisClient.get(ReportIdMigrationService.MIGRATION_KEY)\n if (migrationComplete) {\n logger.info('DPR report ID migration already completed')\n\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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"}
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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"}
@@ -57,6 +57,7 @@ declare class ReportIdMigrationService {
57
57
  */
58
58
  private getMigratedReportId;
59
59
  private ensureConnected;
60
+ private debugLogs;
60
61
  }
61
62
 
62
63
  export { ReportIdMigrationService };
@@ -32,12 +32,14 @@ class ReportIdMigrationService {
32
32
  return;
33
33
  }
34
34
  await this.ensureConnected();
35
- const migrationComplete = await this.redisClient.get(ReportIdMigrationService.MIGRATION_KEY);
36
- if (migrationComplete) {
37
- logger.info('DPR report ID migration already completed');
38
- return;
39
- }
35
+ // TEMPORARILY DISABLE DURING TESTING
36
+ // const migrationComplete = await this.redisClient.get(ReportIdMigrationService.MIGRATION_KEY)
37
+ // if (migrationComplete) {
38
+ // logger.info('DPR report ID migration already completed')
39
+ // return
40
+ // }
40
41
  logger.info('Starting DPR report ID migration');
42
+ await this.debugLogs();
41
43
  // 1. get all the user configs from redis
42
44
  const configs = await this.getAllConfigs();
43
45
  let updatedConfigs = 0;
@@ -217,6 +219,27 @@ class ReportIdMigrationService {
217
219
  await this.redisClient.connect();
218
220
  }
219
221
  }
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
+ }
220
243
  }
221
244
 
222
245
  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 const migrationComplete = await this.redisClient.get(ReportIdMigrationService.MIGRATION_KEY)\n if (migrationComplete) {\n logger.info('DPR report ID migration already completed')\n\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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"}
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":"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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.0",
3
+ "version": "7.3.1",
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",
@@ -56,4 +56,5 @@ export declare class ReportIdMigrationService {
56
56
  */
57
57
  private getMigratedReportId;
58
58
  private ensureConnected;
59
+ private debugLogs;
59
60
  }
@@ -5,7 +5,7 @@ export declare const buildFilterValidator: (field: FieldDefinition) => z.ZodType
5
5
  /**
6
6
  * DATE FIELDS
7
7
  */
8
- export declare const buildDateField: (field: FieldDefinition) => z.ZodPipe<z.ZodTransform<unknown, unknown>, z.ZodOptional<z.ZodString>>;
8
+ export declare const buildDateField: (field: FieldDefinition) => z.ZodPreprocess<z.ZodOptional<z.ZodString>>;
9
9
  /**
10
10
  * DATE RANGE FIELDS
11
11
  */