{"version":3,"file":"impl-CkoQHhc8.mjs","names":[],"sources":["../src/lib/preference-management/bulkDeletePreferenceRecords.ts","../src/commands/consent/delete-preference-records/impl.ts"],"sourcesContent":["import {\n  DeletePreferenceRecordCliCsvRow,\n  DeletePreferenceRecordsResponse,\n  withTransientRetry,\n} from '@transcend-io/sdk';\nimport { decodeCodec } from '@transcend-io/type-utils';\nimport { map } from '@transcend-io/utils';\nimport colors from 'colors';\nimport type { Got } from 'got';\nimport { chunk } from 'lodash-es';\n\nimport { logger } from '../../logger.js';\nimport { readCsv } from '../requests/index.js';\n\ninterface FailedResult extends DeletePreferenceRecordCliCsvRow {\n  /** Error message describing the failure */\n  error: string;\n}\n\ninterface DeletePreferenceRecordsRepositoryOptions {\n  /** The partition to delete from */\n  partition: string;\n  /** Chunk of identifiers to delete */\n  identifierChunk: DeletePreferenceRecordCliCsvRow[];\n  /** the timestamp for the deletion operation */\n  timestamp: Date;\n}\n\n/**\n * Options for deleting preference records\n */\ntype DeletePreferenceRecordsOptions = Omit<\n  DeletePreferenceRecordsRepositoryOptions,\n  'identifierChunk'\n> & {\n  /** The file path to read CSV rows from */\n  filePath: string;\n  /** Maximum items to include in each deletion chunk */\n  maxItemsInChunk: number;\n  /** Maximum concurrency for deletion requests */\n  maxConcurrency: number;\n};\n\n/**\n *\n * Delete a chunk of preference records\n *\n * @param sombra - Sombra instance (must include auth headers)\n * @param options - Options for deletion\n * @param options.partition - The partition to delete from\n * @param options.identifierChunk - Chunk of identifiers to delete\n * @param options.timestamp - The timestamp for the deletion operation\n * @returns List of failed deletions\n */\nasync function deletePreferenceRecordsRepository(\n  sombra: Got,\n  { partition, identifierChunk: chunk, timestamp }: DeletePreferenceRecordsRepositoryOptions,\n): Promise<FailedResult[]> {\n  try {\n    const response = await withTransientRetry(\n      'Delete Preference Records',\n      () =>\n        sombra\n          .post(`v1/preferences/${partition}/delete`, {\n            json: {\n              records: chunk.map((record) => ({\n                anchorIdentifier: record,\n                timestamp: timestamp.toISOString(),\n              })),\n            },\n          })\n          .json(),\n      {\n        logger,\n        maxAttempts: 3,\n        onRetry: (attempt, _err, msg) => {\n          logger.warn(\n            colors.yellow(`Attempt ${attempt} to delete preference records failed: ${msg}`),\n          );\n        },\n      },\n    );\n    const { failures } = decodeCodec(DeletePreferenceRecordsResponse, response);\n    if (failures.length > 0) {\n      return failures.map(({ index, error }) => ({\n        ...chunk[index],\n        error,\n      }));\n    }\n    return [];\n  } catch (err) {\n    return chunk.map((record) => ({\n      ...record,\n      error: (err as Error).message,\n    }));\n  }\n}\n\n/**\n * Delete consent preferences for the managed consent database (delete endpoint)\n *\n * Uses POST /v1/preferences/{partition}/delete.\n *\n *\n * @param sombra - Sombra instance (must include auth headers)\n * @param options - Query options\n * @returns All nodes (only when onItems is not provided)\n */\nexport async function bulkDeletePreferenceRecords(\n  sombra: Got,\n  {\n    partition,\n    filePath,\n    timestamp,\n    maxItemsInChunk,\n    maxConcurrency,\n  }: DeletePreferenceRecordsOptions,\n): Promise<FailedResult[]> {\n  const anchorIdentifiers = readCsv(filePath, DeletePreferenceRecordCliCsvRow);\n  const chunks = chunk(anchorIdentifiers, maxItemsInChunk);\n\n  const failedResults = await map(\n    chunks,\n    async (identifierChunk) => {\n      const failedResults = await deletePreferenceRecordsRepository(sombra, {\n        partition,\n        identifierChunk,\n        timestamp,\n      });\n      return failedResults;\n    },\n    { concurrency: maxConcurrency },\n  );\n  return failedResults.flat();\n}\n","import { readdirSync } from 'node:fs';\nimport { join } from 'node:path';\n\nimport { createSombraGotInstance } from '@transcend-io/sdk';\nimport { map } from '@transcend-io/utils';\nimport cliProgress from 'cli-progress';\nimport colors from 'colors';\n\nimport type { LocalContext } from '../../../context.js';\nimport { doneInputValidation } from '../../../lib/cli/done-input-validation.js';\nimport { writeCsv } from '../../../lib/helpers/index.js';\nimport { bulkDeletePreferenceRecords } from '../../../lib/preference-management/index.js';\nimport { logger } from '../../../logger.js';\n\nexport interface DeletePreferenceRecordsCommandFlags {\n  /** Transcend API key for authentication */\n  auth: string;\n  /** Partition ID to delete preference records from */\n  partition: string;\n  /** Optional Sombra internal key for self-hosted instances */\n  sombraAuth?: string;\n  /** Path to the CSV file used to identify preference records to delete */\n  file?: string;\n  /** Path to the directory of CSV files to load preferences from */\n  directory?: string;\n  /** Base URL for the Transcend API */\n  transcendUrl: string;\n  /** The timestamp when the deletion operation is made. Used for logging purposes. */\n  timestamp: Date;\n  /** Maximum items to include in each deletion chunk */\n  maxItemsInChunk: number;\n  /** Maximum concurrency for deletion requests */\n  maxConcurrency: number;\n  /** Directory to write receipts of failed deletions to */\n  receiptDirectory: string;\n  /** Number of files to process concurrently when deleting preference records from multiple files */\n  fileConcurrency: number;\n}\n\nexport async function deletePreferenceRecords(\n  this: LocalContext,\n  {\n    auth,\n    partition,\n    sombraAuth,\n    file = '',\n    directory,\n    transcendUrl,\n    timestamp,\n    maxConcurrency,\n    maxItemsInChunk,\n    receiptDirectory,\n    fileConcurrency,\n  }: DeletePreferenceRecordsCommandFlags,\n): Promise<void> {\n  if (!!directory && !!file) {\n    logger.error(\n      colors.red('Cannot provide both a directory and a file. Please provide only one.'),\n    );\n    this.process.exit(1);\n  }\n\n  if (!file && !directory) {\n    logger.error(\n      colors.red(\n        'A file or directory must be provided. Please provide one using --file=./preferences.csv or --directory=./preferences',\n      ),\n    );\n    this.process.exit(1);\n  }\n  doneInputValidation(this.process.exit);\n\n  const files: string[] = [];\n\n  if (directory) {\n    try {\n      const filesInDirectory = readdirSync(directory);\n      const csvFiles = filesInDirectory.filter((file) => file.endsWith('.csv'));\n\n      if (csvFiles.length === 0) {\n        logger.error(colors.red(`No CSV files found in directory: ${directory}`));\n        this.process.exit(1);\n      }\n\n      // Add full paths for each CSV file\n      files.push(...csvFiles.map((file) => join(directory, file)));\n    } catch (err) {\n      logger.error(colors.red(`Failed to read directory: ${directory}`));\n      logger.error(colors.red((err as Error).message));\n      this.process.exit(1);\n    }\n  } else {\n    try {\n      // Verify file exists and is a CSV\n      if (!file.endsWith('.csv')) {\n        logger.error(colors.red('File must be a CSV file'));\n        this.process.exit(1);\n      }\n      files.push(file);\n    } catch (err) {\n      logger.error(colors.red(`Failed to access file: ${file}`));\n      logger.error(colors.red((err as Error).message));\n      this.process.exit(1);\n    }\n  }\n\n  logger.debug(\n    colors.green(\n      `Processing ${files.length} consent preferences files for partition: ${partition}`,\n    ),\n  );\n  logger.debug(`\\nFiles to process: ${files.join(', ')}\\n`);\n\n  // Create sombra instance to communicate with\n  const sombra = await createSombraGotInstance(transcendUrl, auth, {\n    logger,\n    sombraApiKey: sombraAuth,\n    sombraUrl: process.env.SOMBRA_URL,\n  });\n  const globalProgressBar = new cliProgress.SingleBar(\n    {\n      format: `Deletion Progress |${colors.cyan('[{bar}]')}| Duration: ${colors.red(\n        '{duration_formatted}',\n      )} | {value}/{total} Files Processed `,\n    },\n    cliProgress.Presets.shades_classic,\n  );\n  globalProgressBar.start(files.length, 0);\n\n  // Process batch of files with concurrency\n  const failedResultsArrays = await map(\n    files,\n    async (filePath) => {\n      const result = await bulkDeletePreferenceRecords(sombra, {\n        partition,\n        filePath,\n        timestamp,\n        maxItemsInChunk,\n        maxConcurrency,\n      });\n      globalProgressBar.increment();\n      return result;\n    },\n    { concurrency: fileConcurrency },\n  );\n  globalProgressBar.stop();\n  const failedResults = failedResultsArrays.flat();\n\n  // Check for failed results and write receipt if any\n  let receiptPath = '';\n  if (failedResults.length > 0) {\n    receiptPath = join(receiptDirectory, `deletion-failures-${Date.now()}.csv`);\n    writeCsv(receiptPath, failedResults, true);\n  }\n\n  logger.info(colors.green('\\n\\n ================================== \\n\\n'));\n  logger.info(colors.green('\\n#### Deletion Summary Report #####\\n'));\n  logger.info(\n    colors.green(\n      `📁 Total Files Processed: ${files.length} \\n` +\n        `❌ Errors: ${failedResults.length} \\n` +\n        `📝 Receipt Path: ${receiptPath || 'N/A'}`,\n    ),\n  );\n  logger.info(colors.green('\\n\\n==================================\\n\\n'));\n}\n"],"mappings":"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"}