{"version":3,"file":"impl-DvSnW_IA.mjs","names":[],"sources":["../src/lib/preference-management/parsePreferenceAndPurposeValuesFromCsv.ts","../src/lib/preference-management/parsePreferenceIdentifiersFromCsv.ts","../src/lib/preference-management/parsePreferenceTimestampsFromCsv.ts","../src/lib/preference-management/parsePreferenceManagementCsv.ts","../src/lib/preference-management/uploadPreferenceManagementPreferencesInteractive.ts","../src/commands/consent/upload-preferences/impl.ts"],"sourcesContent":["import { PreferenceTopicType } from '@transcend-io/privacy-types';\nimport { FileMetadataState, type PreferenceTopic } from '@transcend-io/sdk';\nimport { mapSeries, splitCsvToList } from '@transcend-io/utils';\nimport colors from 'colors';\nimport inquirer from 'inquirer';\nimport { uniq, difference } from 'lodash-es';\n\nimport { logger } from '../../logger.js';\n\n/* eslint-disable no-param-reassign */\n\n/**\n * Parse out the purpose.enabled and preference values from a CSV file\n *\n * @param preferences - List of preferences\n * @param currentState - The current file metadata state for parsing this list\n * @param options - Options\n * @returns The updated file metadata state\n */\nexport async function parsePreferenceAndPurposeValuesFromCsv(\n  preferences: Record<string, string>[],\n  currentState: FileMetadataState,\n  {\n    purposeSlugs,\n    preferenceTopics,\n    forceTriggerWorkflows,\n  }: {\n    /** The purpose slugs that are allowed to be updated */\n    purposeSlugs: string[];\n    /** The preference topics */\n    preferenceTopics: PreferenceTopic[];\n    /** Force workflow triggers */\n    forceTriggerWorkflows: boolean;\n  },\n): Promise<FileMetadataState> {\n  // Determine columns to map\n  const columnNames = uniq(preferences.map((x) => Object.keys(x)).flat());\n\n  // Determine the columns that could potentially be used for identifier\n  const otherColumns = difference(columnNames, [\n    ...(currentState.identifierColumn ? [currentState.identifierColumn] : []),\n    ...(currentState.timestampColum ? [currentState.timestampColum] : []),\n  ]);\n  if (otherColumns.length === 0) {\n    if (forceTriggerWorkflows) {\n      return currentState;\n    }\n    throw new Error('No other columns to process');\n  }\n\n  // The purpose and preferences to map to\n  const purposeNames = [\n    ...purposeSlugs,\n    ...preferenceTopics.map((x) => `${x.purpose.trackingType}->${x.slug}`),\n  ];\n\n  // Ensure all columns are accounted for\n  await mapSeries(otherColumns, async (col) => {\n    // Determine the unique values to map in this column\n    const uniqueValues = uniq(preferences.map((x) => x[col]));\n\n    // Map the column to a purpose\n    let purposeMapping = currentState.columnToPurposeName[col];\n    if (purposeMapping) {\n      logger.info(\n        colors.magenta(`Column \"${col}\" is associated with purpose \"${purposeMapping.purpose}\"`),\n      );\n    } else {\n      const { purposeName } = await inquirer.prompt<{\n        /** purpose name */\n        purposeName: string;\n      }>([\n        {\n          name: 'purposeName',\n          message: `Choose the purpose that column ${col} is associated with`,\n          type: 'list',\n          default: purposeNames.find((x) => x.startsWith(purposeSlugs[0])),\n          choices: purposeNames,\n        },\n      ]);\n      const [purposeSlug, preferenceSlug] = purposeName.split('->');\n      purposeMapping = {\n        purpose: purposeSlug,\n        preference: preferenceSlug || null,\n        valueMapping: {},\n      };\n    }\n\n    // map each value to the purpose value\n    await mapSeries(uniqueValues, async (value) => {\n      if (purposeMapping.valueMapping[value] !== undefined) {\n        logger.info(\n          colors.magenta(\n            `Value \"${value}\" is associated with purpose value \"${purposeMapping.valueMapping[value]}\"`,\n          ),\n        );\n        return;\n      }\n      // if preference is null, this column is just for the purpose\n      if (purposeMapping.preference === null) {\n        const { purposeValue } = await inquirer.prompt<{\n          /** purpose value */\n          purposeValue: boolean;\n        }>([\n          {\n            name: 'purposeValue',\n            message: `Choose the purpose value for value \"${value}\" associated with purpose \"${purposeMapping.purpose}\"`,\n            type: 'confirm',\n            default: value !== 'false',\n          },\n        ]);\n        purposeMapping.valueMapping[value] = purposeValue;\n      }\n\n      // if preference is not null, this column is for a specific preference\n      if (purposeMapping.preference !== null) {\n        const preferenceTopic = preferenceTopics.find((x) => x.slug === purposeMapping.preference);\n        if (!preferenceTopic) {\n          logger.error(colors.red(`Preference topic \"${purposeMapping.preference}\" not found`));\n          return;\n        }\n        const preferenceOptions = preferenceTopic.preferenceOptionValues.map(({ slug }) => slug);\n\n        if (preferenceTopic.type === PreferenceTopicType.Boolean) {\n          const { preferenceValue } = await inquirer.prompt<{\n            /** purpose value */\n            preferenceValue: boolean;\n          }>([\n            {\n              name: 'preferenceValue',\n              message:\n                // eslint-disable-next-line max-len\n                `Choose the preference value for \"${preferenceTopic.slug}\" value \"${value}\" associated with purpose \"${purposeMapping.purpose}\"`,\n              type: 'confirm',\n              default: value !== 'false',\n            },\n          ]);\n          purposeMapping.valueMapping[value] = preferenceValue;\n          return;\n        }\n\n        if (preferenceTopic.type === PreferenceTopicType.Select) {\n          const { preferenceValue } = await inquirer.prompt<{\n            /** purpose value */\n            preferenceValue: boolean;\n          }>([\n            {\n              name: 'preferenceValue',\n              // eslint-disable-next-line max-len\n              message: `Choose the preference value for \"${preferenceTopic.slug}\" value \"${value}\" associated with purpose \"${purposeMapping.purpose}\"`,\n              type: 'list',\n              choices: preferenceOptions,\n              default: preferenceOptions.find((x) => x === value),\n            },\n          ]);\n          purposeMapping.valueMapping[value] = preferenceValue;\n          return;\n        }\n\n        if (preferenceTopic.type === PreferenceTopicType.MultiSelect) {\n          const parsedValues = splitCsvToList(value);\n          // need to do this serially\n          await mapSeries(parsedValues, async (parsedValue) => {\n            // if we already have a value, skip re-processing it again\n            if (purposeMapping.valueMapping[parsedValue] !== undefined) {\n              return;\n            }\n            const { preferenceValue } = await inquirer.prompt<{\n              /** purpose value */\n              preferenceValue: boolean;\n            }>([\n              {\n                name: 'preferenceValue',\n                // eslint-disable-next-line max-len\n                message: `Choose the preference value for \"${preferenceTopic.slug}\" value \"${parsedValue}\" associated with purpose \"${purposeMapping.purpose}\"`,\n                type: 'list',\n                choices: preferenceOptions,\n                default: preferenceOptions.find((x) => x === parsedValue),\n              },\n            ]);\n            purposeMapping.valueMapping[parsedValue] = preferenceValue;\n          });\n          return;\n        }\n\n        throw new Error(`Unknown preference topic type: ${preferenceTopic.type}`);\n      }\n    });\n\n    currentState.columnToPurposeName[col] = purposeMapping;\n  });\n\n  return currentState;\n}\n/* eslint-enable no-param-reassign */\n","import { FileMetadataState } from '@transcend-io/sdk';\nimport colors from 'colors';\nimport inquirer from 'inquirer';\nimport { uniq, groupBy, difference } from 'lodash-es';\n\nimport { logger } from '../../logger.js';\nimport { inquirerConfirmBoolean } from '../helpers/index.js';\n\n/* eslint-disable no-param-reassign */\n\n/**\n * Parse identifiers from a CSV list of preferences\n *\n * Ensures that all rows have a valid identifier\n * and that all identifiers are unique.\n *\n * @param preferences - List of preferences\n * @param currentState - The current file metadata state for parsing this list\n * @returns The updated file metadata state\n */\nexport async function parsePreferenceIdentifiersFromCsv(\n  preferences: Record<string, string>[],\n  currentState: FileMetadataState,\n): Promise<{\n  /** The updated state */\n  currentState: FileMetadataState;\n  /** The updated preferences */\n  preferences: Record<string, string>[];\n}> {\n  // Determine columns to map\n  const columnNames = uniq(preferences.map((x) => Object.keys(x)).flat());\n\n  // Determine the columns that could potentially be used for identifier\n  const remainingColumnsForIdentifier = difference(columnNames, [\n    ...(currentState.identifierColumn ? [currentState.identifierColumn] : []),\n    ...Object.keys(currentState.columnToPurposeName),\n  ]);\n\n  // Determine the identifier column to work off of\n  if (!currentState.identifierColumn) {\n    const { identifierName } = await inquirer.prompt<{\n      /** Identifier name */\n      identifierName: string;\n    }>([\n      {\n        name: 'identifierName',\n        message:\n          'Choose the column that will be used as the identifier to upload consent preferences by',\n        type: 'list',\n        default:\n          remainingColumnsForIdentifier.find((col) => col.toLowerCase().includes('email')) ||\n          remainingColumnsForIdentifier[0],\n        choices: remainingColumnsForIdentifier,\n      },\n    ]);\n    currentState.identifierColumn = identifierName;\n  }\n  logger.info(colors.magenta(`Using identifier column \"${currentState.identifierColumn}\"`));\n\n  // Validate that the identifier column is present for all rows and unique\n  const identifierColumnsMissing = preferences\n    .map((pref, ind) => (pref[currentState.identifierColumn!] ? null : [ind]))\n    .filter((x): x is number[] => !!x)\n    .flat();\n  if (identifierColumnsMissing.length > 0) {\n    const msg = `The identifier column \"${\n      currentState.identifierColumn\n    }\" is missing a value for the following rows: ${identifierColumnsMissing.join(', ')}`;\n    logger.warn(colors.yellow(msg));\n\n    // Ask user if they would like to skip rows missing an identifier\n    const skip = await inquirerConfirmBoolean({\n      message: 'Would you like to skip rows missing an identifier?',\n    });\n    if (!skip) {\n      throw new Error(msg);\n    }\n\n    // Filter out rows missing an identifier\n    const previous = preferences.length;\n    preferences = preferences.filter((pref) => pref[currentState.identifierColumn!]);\n    logger.info(\n      colors.yellow(`Skipped ${previous - preferences.length} rows missing an identifier`),\n    );\n  }\n  logger.info(\n    colors.magenta(\n      `The identifier column \"${currentState.identifierColumn}\" is present for all rows`,\n    ),\n  );\n\n  // Validate that all identifiers are unique\n  const rowsByUserId = groupBy(preferences, currentState.identifierColumn);\n  const duplicateIdentifiers = Object.entries(rowsByUserId).filter(([, rows]) => rows.length > 1);\n  if (duplicateIdentifiers.length > 0) {\n    const msg = `The identifier column \"${\n      currentState.identifierColumn\n    }\" has duplicate values for the following rows: ${duplicateIdentifiers\n      .slice(0, 10)\n      .map(([userId, rows]) => `${userId} (${rows.length})`)\n      .join('\\n')}`;\n    logger.warn(colors.yellow(msg));\n\n    // Ask user if they would like to take the most recent update\n    // for each duplicate identifier\n    const skip = await inquirerConfirmBoolean({\n      message: 'Would you like to automatically take the latest update?',\n    });\n    if (!skip) {\n      throw new Error(msg);\n    }\n    preferences = Object.entries(rowsByUserId)\n      .map(([, rows]) => {\n        const sorted = rows.sort(\n          (a, b) =>\n            new Date(b[currentState.timestampColum!]).getTime() -\n            new Date(a[currentState.timestampColum!]).getTime(),\n        );\n        return sorted[0];\n      })\n      .filter((x) => x);\n  }\n\n  return { currentState, preferences };\n}\n/* eslint-enable no-param-reassign */\n","import { FileMetadataState } from '@transcend-io/sdk';\nimport colors from 'colors';\nimport inquirer from 'inquirer';\nimport { uniq, difference } from 'lodash-es';\n\nimport { logger } from '../../logger.js';\n\nexport const NONE_PREFERENCE_MAP = '[NONE]';\n\n/* eslint-disable no-param-reassign */\n\n/**\n * Parse timestamps from a CSV list of preferences\n *\n * When timestamp is requested, this script\n * ensures that all rows have a valid timestamp.\n *\n * Error is throw if timestamp is missing\n *\n * @param preferences - List of preferences\n * @param currentState - The current file metadata state for parsing this list\n * @returns The updated file metadata state\n */\nexport async function parsePreferenceTimestampsFromCsv(\n  preferences: Record<string, string>[],\n  currentState: FileMetadataState,\n): Promise<FileMetadataState> {\n  // Determine columns to map\n  const columnNames = uniq(preferences.map((x) => Object.keys(x)).flat());\n\n  // Determine the columns that could potentially be used for timestamp\n  const remainingColumnsForTimestamp = difference(columnNames, [\n    ...(currentState.identifierColumn ? [currentState.identifierColumn] : []),\n    ...Object.keys(currentState.columnToPurposeName),\n  ]);\n\n  // Determine the timestamp column to work off of\n  if (!currentState.timestampColum) {\n    const { timestampName } = await inquirer.prompt<{\n      /** timestamp name */\n      timestampName: string;\n    }>([\n      {\n        name: 'timestampName',\n        message: 'Choose the column that will be used as the timestamp of last preference update',\n        type: 'list',\n        default:\n          remainingColumnsForTimestamp.find((col) => col.toLowerCase().includes('date')) ||\n          remainingColumnsForTimestamp.find((col) => col.toLowerCase().includes('time')) ||\n          remainingColumnsForTimestamp[0],\n        choices: [...remainingColumnsForTimestamp, NONE_PREFERENCE_MAP],\n      },\n    ]);\n    currentState.timestampColum = timestampName;\n  }\n  logger.info(colors.magenta(`Using timestamp column \"${currentState.timestampColum}\"`));\n\n  // Validate that all rows have valid timestamp\n  if (currentState.timestampColum !== NONE_PREFERENCE_MAP) {\n    const timestampColumnsMissing = preferences\n      .map((pref, ind) => (pref[currentState.timestampColum!] ? null : [ind]))\n      .filter((x): x is number[] => !!x)\n      .flat();\n    if (timestampColumnsMissing.length > 0) {\n      throw new Error(\n        `The timestamp column \"${\n          currentState.timestampColum\n        }\" is missing a value for the following rows: ${timestampColumnsMissing.join('\\n')}`,\n      );\n    }\n    logger.info(\n      colors.magenta(\n        `The timestamp column \"${currentState.timestampColum}\" is present for all row`,\n      ),\n    );\n  }\n  return currentState;\n}\n/* eslint-enable no-param-reassign */\n","import { PersistedState } from '@transcend-io/persisted-state';\nimport {\n  checkIfPendingPreferenceUpdatesAreNoOp,\n  checkIfPendingPreferenceUpdatesCauseConflict,\n  FileMetadataState,\n  getPreferencesForIdentifiers,\n  getPreferenceUpdatesFromRow,\n  PreferenceState,\n  type PreferenceTopic,\n} from '@transcend-io/sdk';\nimport cliProgress from 'cli-progress';\nimport colors from 'colors';\nimport type { Got } from 'got';\nimport * as t from 'io-ts';\nimport { keyBy } from 'lodash-es';\n\nimport { logger } from '../../logger.js';\nimport { readCsv } from '../requests/index.js';\nimport { parsePreferenceAndPurposeValuesFromCsv } from './parsePreferenceAndPurposeValuesFromCsv.js';\nimport { parsePreferenceIdentifiersFromCsv } from './parsePreferenceIdentifiersFromCsv.js';\nimport { parsePreferenceTimestampsFromCsv } from './parsePreferenceTimestampsFromCsv.js';\n\n/**\n * Parse a file into the cache\n *\n *\n * @param options - Options\n * @param cache - The cache to store the parsed file in\n * @returns The cache with the parsed file\n */\nexport async function parsePreferenceManagementCsvWithCache(\n  {\n    file,\n    sombra,\n    purposeSlugs,\n    preferenceTopics,\n    partitionKey,\n    skipExistingRecordCheck,\n    forceTriggerWorkflows,\n  }: {\n    /** File to parse */\n    file: string;\n    /** The purpose slugs that are allowed to be updated */\n    purposeSlugs: string[];\n    /** The preference topics */\n    preferenceTopics: PreferenceTopic[];\n    /** Sombra got instance */\n    sombra: Got;\n    /** Partition key */\n    partitionKey: string;\n    /** Whether to skip the check for existing records. SHOULD ONLY BE USED FOR INITIAL UPLOAD */\n    skipExistingRecordCheck: boolean;\n    /** Whether to force workflow triggers */\n    forceTriggerWorkflows: boolean;\n  },\n  cache: PersistedState<typeof PreferenceState>,\n): Promise<void> {\n  // Start the timer\n  const t0 = new Date().getTime();\n\n  // Get the current metadata\n  const fileMetadata = cache.getValue('fileMetadata');\n\n  // Read in the file\n  logger.info(colors.magenta(`Reading in file: \"${file}\"`));\n  let preferences = readCsv(file, t.record(t.string, t.string));\n\n  // start building the cache, can use previous cache as well\n  let currentState: FileMetadataState = {\n    columnToPurposeName: {},\n    pendingSafeUpdates: {},\n    pendingConflictUpdates: {},\n    skippedUpdates: {},\n    // Load in the last fetched time\n    ...((fileMetadata[file] || {}) as Partial<FileMetadataState>),\n    lastFetchedAt: new Date().toISOString(),\n  };\n\n  // Validate that all timestamps are present in the file\n  currentState = await parsePreferenceTimestampsFromCsv(preferences, currentState);\n  fileMetadata[file] = currentState;\n  await cache.setValue(fileMetadata, 'fileMetadata');\n\n  // Validate that all identifiers are present and unique\n  const result = await parsePreferenceIdentifiersFromCsv(preferences, currentState);\n  currentState = result.currentState;\n  preferences = result.preferences;\n  fileMetadata[file] = currentState;\n  await cache.setValue(fileMetadata, 'fileMetadata');\n\n  // Ensure all other columns are mapped to purpose and preference\n  // slug values\n  currentState = await parsePreferenceAndPurposeValuesFromCsv(preferences, currentState, {\n    preferenceTopics,\n    purposeSlugs,\n    forceTriggerWorkflows,\n  });\n  fileMetadata[file] = currentState;\n  await cache.setValue(fileMetadata, 'fileMetadata');\n\n  // Grab existing preference store records\n  const identifiers = preferences.map((pref) => pref[currentState.identifierColumn!]);\n  const progressBar = new cliProgress.SingleBar({}, cliProgress.Presets.shades_classic);\n  if (!skipExistingRecordCheck) {\n    progressBar.start(identifiers.length, 0);\n  }\n  const existingConsentRecords = skipExistingRecordCheck\n    ? []\n    : await getPreferencesForIdentifiers(sombra, {\n        identifiers: identifiers.map((x) => ({ value: x })),\n        partitionKey,\n        logger,\n        onProgress: (completed, total) => progressBar.update(completed, { total }),\n      });\n  progressBar.stop();\n  const consentRecordByIdentifier = keyBy(existingConsentRecords, 'userId');\n\n  // Clear out previous updates\n  currentState.pendingConflictUpdates = {};\n  currentState.pendingSafeUpdates = {};\n  currentState.skippedUpdates = {};\n\n  // Process each row\n  preferences.forEach((pref) => {\n    // Grab unique Id for the user\n    const userId = pref[currentState.identifierColumn!];\n\n    // determine updates for user\n    const pendingUpdates = getPreferenceUpdatesFromRow({\n      row: pref,\n      columnToPurposeName: currentState.columnToPurposeName,\n      preferenceTopics,\n      purposeSlugs,\n    });\n\n    // Grab current state of the update\n    const currentConsentRecord = consentRecordByIdentifier[userId];\n    if (forceTriggerWorkflows && !currentConsentRecord) {\n      throw new Error(\n        `No existing consent record found for user with id: ${userId}.\n        When 'forceTriggerWorkflows' is set all the user identifiers should contain a consent record`,\n      );\n    }\n    // Check if the update can be skipped\n    // this is the case if a record exists, and the purpose\n    // and preference values are all in sync\n    if (\n      currentConsentRecord &&\n      checkIfPendingPreferenceUpdatesAreNoOp({\n        currentConsentRecord,\n        pendingUpdates,\n        preferenceTopics,\n      }) &&\n      !forceTriggerWorkflows\n    ) {\n      currentState.skippedUpdates[userId] = pref;\n      return;\n    }\n\n    // Determine if there are any conflicts\n    if (\n      currentConsentRecord &&\n      checkIfPendingPreferenceUpdatesCauseConflict({\n        currentConsentRecord,\n        pendingUpdates,\n        preferenceTopics,\n      })\n    ) {\n      currentState.pendingConflictUpdates[userId] = {\n        row: pref,\n        record: currentConsentRecord,\n      };\n      return;\n    }\n\n    // Add to pending updates\n    currentState.pendingSafeUpdates[userId] = pref;\n  });\n\n  // Read in the file\n  fileMetadata[file] = currentState;\n  await cache.setValue(fileMetadata, 'fileMetadata');\n  const t1 = new Date().getTime();\n  logger.info(colors.green(`Successfully pre-processed file: \"${file}\" in ${(t1 - t0) / 1000}s`));\n}\n","import { PersistedState } from '@transcend-io/persisted-state';\nimport { PreferenceUpdateItem } from '@transcend-io/privacy-types';\nimport {\n  buildTranscendGraphQLClient,\n  createSombraGotInstance,\n  fetchAllPurposes,\n  fetchAllPreferenceTopics,\n  getPreferenceUpdatesFromRow,\n  PreferenceState,\n} from '@transcend-io/sdk';\nimport { apply } from '@transcend-io/type-utils';\nimport { map } from '@transcend-io/utils';\nimport cliProgress from 'cli-progress';\nimport colors from 'colors';\nimport { chunk } from 'lodash-es';\n\nimport { logger } from '../../logger.js';\nimport { parseAttributesFromString } from '../requests/index.js';\nimport { parsePreferenceManagementCsvWithCache } from './parsePreferenceManagementCsv.js';\nimport { NONE_PREFERENCE_MAP } from './parsePreferenceTimestampsFromCsv.js';\n\n/**\n * Upload a set of consent preferences\n *\n * @param options - Options\n */\nexport async function uploadPreferenceManagementPreferencesInteractive({\n  auth,\n  sombraAuth,\n  receiptFilepath,\n  file,\n  partition,\n  isSilent = true,\n  dryRun = false,\n  skipWorkflowTriggers = false,\n  skipConflictUpdates = false,\n  skipExistingRecordCheck = false,\n  attributes = [],\n  transcendUrl,\n  forceTriggerWorkflows = false,\n}: {\n  /** The Transcend API key */\n  auth: string;\n  /** Sombra API key authentication */\n  sombraAuth?: string;\n  /** Partition key */\n  partition: string;\n  /** File where to store receipt and continue from where left off */\n  receiptFilepath: string;\n  /** The file to process */\n  file: string;\n  /** API URL for Transcend backend */\n  transcendUrl: string;\n  /** Whether to do a dry run */\n  dryRun?: boolean;\n  /** Whether to upload as isSilent */\n  isSilent?: boolean;\n  /** Attributes string pre-parse. In format Key:Value */\n  attributes?: string[];\n  /** Skip workflow triggers */\n  skipWorkflowTriggers?: boolean;\n  /**\n   * When true, only update preferences that do not conflict with existing\n   * preferences. When false, update all preferences in CSV based on timestamp.\n   */\n  skipConflictUpdates?: boolean;\n  /** Whether to skip the check for existing records. SHOULD ONLY BE USED FOR INITIAL UPLOAD */\n  skipExistingRecordCheck?: boolean;\n  /** Whether to force trigger workflows */\n  forceTriggerWorkflows?: boolean;\n}): Promise<void> {\n  // Parse out the extra attributes to apply to all requests uploaded\n  const parsedAttributes = parseAttributesFromString(attributes);\n\n  // Create a new state file to store the requests from this run\n  const preferenceState = new PersistedState(receiptFilepath, PreferenceState, {\n    fileMetadata: {},\n    failingUpdates: {},\n    pendingUpdates: {},\n  });\n  const failingRequests = preferenceState.getValue('failingUpdates');\n  const pendingRequests = preferenceState.getValue('pendingUpdates');\n  let fileMetadata = preferenceState.getValue('fileMetadata');\n\n  logger.info(\n    colors.magenta(\n      'Restored cache, there are: \\n' +\n        `${Object.values(failingRequests).length} failing requests to be retried\\n` +\n        `${Object.values(pendingRequests).length} pending requests to be processed\\n` +\n        `The following files are stored in cache and will be used:\\n${Object.keys(fileMetadata)\n          .map((x) => x)\n          .join('\\n')}\\n` +\n        `The following file will be processed: ${file}\\n`,\n    ),\n  );\n\n  // Create GraphQL client to connect to Transcend backend\n  const client = buildTranscendGraphQLClient(transcendUrl, auth);\n\n  const [sombra, purposes, preferenceTopics] = await Promise.all([\n    // Create sombra instance to communicate with\n    createSombraGotInstance(transcendUrl, auth, {\n      logger,\n      sombraApiKey: sombraAuth,\n      sombraUrl: process.env.SOMBRA_URL,\n    }),\n    // get all purposes and topics\n    fetchAllPurposes(client, { logger }),\n    fetchAllPreferenceTopics(client, { logger }),\n  ]);\n\n  // Process the file\n  await parsePreferenceManagementCsvWithCache(\n    {\n      file,\n      purposeSlugs: purposes.map((x) => x.trackingType),\n      preferenceTopics,\n      sombra,\n      partitionKey: partition,\n      skipExistingRecordCheck,\n      forceTriggerWorkflows,\n    },\n    preferenceState,\n  );\n\n  // Construct the pending updates\n  const pendingUpdates: Record<string, PreferenceUpdateItem> = {};\n  fileMetadata = preferenceState.getValue('fileMetadata');\n  const metadata = fileMetadata[file];\n\n  logger.info(\n    colors.magenta(\n      `Found ${Object.entries(metadata.pendingSafeUpdates).length} safe updates in ${file}`,\n    ),\n  );\n  logger.info(\n    colors.magenta(\n      `Found ${Object.entries(metadata.pendingConflictUpdates).length} conflict updates in ${file}`,\n    ),\n  );\n  logger.info(\n    colors.magenta(\n      `Found ${Object.entries(metadata.skippedUpdates).length} skipped updates in ${file}`,\n    ),\n  );\n\n  // Update either safe updates only or safe + conflict\n  Object.entries({\n    ...metadata.pendingSafeUpdates,\n    ...(skipConflictUpdates ? {} : apply(metadata.pendingConflictUpdates, ({ row }) => row)),\n  }).forEach(([userId, update]) => {\n    // Determine timestamp\n    const timestamp =\n      metadata.timestampColum === NONE_PREFERENCE_MAP\n        ? new Date()\n        : new Date(update[metadata.timestampColum!]);\n\n    // Determine updates\n    const updates = getPreferenceUpdatesFromRow({\n      row: update,\n      columnToPurposeName: metadata.columnToPurposeName,\n      preferenceTopics,\n      purposeSlugs: purposes.map((x) => x.trackingType),\n    });\n    pendingUpdates[userId] = {\n      userId,\n      partition,\n      timestamp: timestamp.toISOString(),\n      purposes: Object.entries(updates).map(([purpose, value]) => ({\n        ...value,\n        purpose,\n        workflowSettings: {\n          attributes: parsedAttributes,\n          isSilent,\n          skipWorkflowTrigger: skipWorkflowTriggers,\n          ...(forceTriggerWorkflows ? { forceTriggerWorkflow: forceTriggerWorkflows } : {}),\n        },\n      })),\n    };\n  });\n  await preferenceState.setValue(pendingUpdates, 'pendingUpdates');\n  await preferenceState.setValue({}, 'failingUpdates');\n\n  // Exist early if dry run\n  if (dryRun) {\n    logger.info(\n      colors.green(\n        `Dry run complete, exiting. ${\n          Object.values(pendingUpdates).length\n        } pending updates. Check file: ${receiptFilepath}`,\n      ),\n    );\n    return;\n  }\n\n  logger.info(\n    colors.magenta(\n      `Uploading ${Object.values(pendingUpdates).length} preferences to partition: ${partition}`,\n    ),\n  );\n\n  // Time duration\n  const t0 = new Date().getTime();\n\n  // create a new progress bar instance and use shades_classic theme\n  const progressBar = new cliProgress.SingleBar({}, cliProgress.Presets.shades_classic);\n\n  // Build a GraphQL client\n  let total = 0;\n  const updatesToRun = Object.entries(pendingUpdates);\n  const chunkedUpdates = chunk(updatesToRun, skipWorkflowTriggers ? 100 : 10);\n  progressBar.start(updatesToRun.length, 0);\n  await map(\n    chunkedUpdates,\n    async (currentChunk) => {\n      // Make the request\n      try {\n        await sombra\n          .put('v1/preferences', {\n            json: {\n              records: currentChunk.map(([, update]) => update),\n              skipWorkflowTriggers,\n            },\n          })\n          .json();\n      } catch (err) {\n        try {\n          const parsed = JSON.parse(err?.response?.body || '{}');\n          if (parsed.error) {\n            logger.error(colors.red(`Error: ${parsed.error}`));\n          }\n        } catch {\n          // continue\n        }\n        logger.error(\n          colors.red(\n            `Failed to upload ${currentChunk.length} user preferences to partition ${partition}: ${\n              err?.response?.body || err?.message\n            }`,\n          ),\n        );\n        const failingUpdates = preferenceState.getValue('failingUpdates');\n        currentChunk.forEach(([userId, update]) => {\n          failingUpdates[userId] = {\n            uploadedAt: new Date().toISOString(),\n            update,\n            error: err?.response?.body || err?.message || 'Unknown error',\n          };\n        });\n        await preferenceState.setValue(failingUpdates, 'failingUpdates');\n      }\n\n      total += currentChunk.length;\n      progressBar.update(total);\n    },\n    {\n      concurrency: 40,\n    },\n  );\n\n  progressBar.stop();\n  const t1 = new Date().getTime();\n  const totalTime = t1 - t0;\n  logger.info(\n    colors.green(\n      `Successfully uploaded ${\n        updatesToRun.length\n      } user preferences to partition ${partition} in \"${totalTime / 1000}\" seconds!`,\n    ),\n  );\n}\n","import { readdirSync } from 'node:fs';\nimport { basename, join } from 'node:path';\n\nimport { map, splitCsvToList } from '@transcend-io/utils';\nimport colors from 'colors';\n\nimport type { LocalContext } from '../../../context.js';\nimport { doneInputValidation } from '../../../lib/cli/done-input-validation.js';\nimport { uploadPreferenceManagementPreferencesInteractive } from '../../../lib/preference-management/index.js';\nimport { logger } from '../../../logger.js';\n\nexport interface UploadPreferencesCommandFlags {\n  auth: string;\n  partition: string;\n  sombraAuth?: string;\n  transcendUrl: string;\n  file?: string;\n  directory?: string;\n  dryRun: boolean;\n  skipExistingRecordCheck: boolean;\n  receiptFileDir: string;\n  skipWorkflowTriggers: boolean;\n  forceTriggerWorkflows: boolean;\n  skipConflictUpdates: boolean;\n  isSilent: boolean;\n  attributes: string;\n  receiptFilepath: string;\n  concurrency: number;\n}\n\nexport async function uploadPreferences(\n  this: LocalContext,\n  {\n    auth,\n    partition,\n    sombraAuth,\n    transcendUrl,\n    file = '',\n    directory,\n    dryRun,\n    skipExistingRecordCheck,\n    receiptFileDir,\n    skipWorkflowTriggers,\n    forceTriggerWorkflows,\n    skipConflictUpdates,\n    isSilent,\n    attributes,\n    concurrency,\n  }: UploadPreferencesCommandFlags,\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\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.info(\n    colors.green(\n      `Processing ${files.length} consent preferences files for partition: ${partition}`,\n    ),\n  );\n  logger.debug(`Files to process: ${files.join(', ')}`);\n\n  if (skipExistingRecordCheck) {\n    logger.info(colors.bgYellow(`Skipping existing record check: ${skipExistingRecordCheck}`));\n  }\n\n  await map(\n    files,\n    async (filePath) => {\n      const fileName = basename(filePath).replace('.csv', '');\n      await uploadPreferenceManagementPreferencesInteractive({\n        receiptFilepath: join(receiptFileDir, `${fileName}-receipts.json`),\n        auth,\n        sombraAuth,\n        file: filePath,\n        partition,\n        transcendUrl,\n        skipConflictUpdates,\n        skipWorkflowTriggers,\n        skipExistingRecordCheck,\n        isSilent,\n        dryRun,\n        attributes: splitCsvToList(attributes),\n        forceTriggerWorkflows,\n      });\n    },\n    { concurrency },\n  );\n}\n"],"mappings":"wkCAmBA,eAAsB,EACpB,EACA,EACA,CACE,eACA,mBACA,yBAS0B,CAK5B,IAAM,EAAe,EAHD,EAAK,EAAY,IAAK,GAAM,OAAO,KAAK,EAAE,CAAC,CAAC,MAAM,CAG3B,CAAE,CAC3C,GAAI,EAAa,iBAAmB,CAAC,EAAa,iBAAiB,CAAG,EAAE,CACxE,GAAI,EAAa,eAAiB,CAAC,EAAa,eAAe,CAAG,EAAE,CACrE,CAAC,CACF,GAAI,EAAa,SAAW,EAAG,CAC7B,GAAI,EACF,OAAO,EAET,MAAU,MAAM,8BAA8B,CAIhD,IAAM,EAAe,CACnB,GAAG,EACH,GAAG,EAAiB,IAAK,GAAM,GAAG,EAAE,QAAQ,aAAa,IAAI,EAAE,OAAO,CACvE,CA0ID,OAvIA,MAAM,EAAU,EAAc,KAAO,IAAQ,CAE3C,IAAM,EAAe,EAAK,EAAY,IAAK,GAAM,EAAE,GAAK,CAAC,CAGrD,EAAiB,EAAa,oBAAoB,GACtD,GAAI,EACF,EAAO,KACL,EAAO,QAAQ,WAAW,EAAI,gCAAgC,EAAe,QAAQ,GAAG,CACzF,KACI,CACL,GAAM,CAAE,eAAgB,MAAM,EAAS,OAGpC,CACD,CACE,KAAM,cACN,QAAS,kCAAkC,EAAI,qBAC/C,KAAM,OACN,QAAS,EAAa,KAAM,GAAM,EAAE,WAAW,EAAa,GAAG,CAAC,CAChE,QAAS,EACV,CACF,CAAC,CACI,CAAC,EAAa,GAAkB,EAAY,MAAM,KAAK,CAC7D,EAAiB,CACf,QAAS,EACT,WAAY,GAAkB,KAC9B,aAAc,EAAE,CACjB,CAIH,MAAM,EAAU,EAAc,KAAO,IAAU,CAC7C,GAAI,EAAe,aAAa,KAAW,IAAA,GAAW,CACpD,EAAO,KACL,EAAO,QACL,UAAU,EAAM,sCAAsC,EAAe,aAAa,GAAO,GAC1F,CACF,CACD,OAGF,GAAI,EAAe,aAAe,KAAM,CACtC,GAAM,CAAE,gBAAiB,MAAM,EAAS,OAGrC,CACD,CACE,KAAM,eACN,QAAS,uCAAuC,EAAM,6BAA6B,EAAe,QAAQ,GAC1G,KAAM,UACN,QAAS,IAAU,QACpB,CACF,CAAC,CACF,EAAe,aAAa,GAAS,EAIvC,GAAI,EAAe,aAAe,KAAM,CACtC,IAAM,EAAkB,EAAiB,KAAM,GAAM,EAAE,OAAS,EAAe,WAAW,CAC1F,GAAI,CAAC,EAAiB,CACpB,EAAO,MAAM,EAAO,IAAI,qBAAqB,EAAe,WAAW,aAAa,CAAC,CACrF,OAEF,IAAM,EAAoB,EAAgB,uBAAuB,KAAK,CAAE,UAAW,EAAK,CAExF,GAAI,EAAgB,OAAS,EAAoB,QAAS,CACxD,GAAM,CAAE,mBAAoB,MAAM,EAAS,OAGxC,CACD,CACE,KAAM,kBACN,QAEE,oCAAoC,EAAgB,KAAK,WAAW,EAAM,6BAA6B,EAAe,QAAQ,GAChI,KAAM,UACN,QAAS,IAAU,QACpB,CACF,CAAC,CACF,EAAe,aAAa,GAAS,EACrC,OAGF,GAAI,EAAgB,OAAS,EAAoB,OAAQ,CACvD,GAAM,CAAE,mBAAoB,MAAM,EAAS,OAGxC,CACD,CACE,KAAM,kBAEN,QAAS,oCAAoC,EAAgB,KAAK,WAAW,EAAM,6BAA6B,EAAe,QAAQ,GACvI,KAAM,OACN,QAAS,EACT,QAAS,EAAkB,KAAM,GAAM,IAAM,EAAM,CACpD,CACF,CAAC,CACF,EAAe,aAAa,GAAS,EACrC,OAGF,GAAI,EAAgB,OAAS,EAAoB,YAAa,CAG5D,MAAM,EAFe,EAAe,EAER,CAAE,KAAO,IAAgB,CAEnD,GAAI,EAAe,aAAa,KAAiB,IAAA,GAC/C,OAEF,GAAM,CAAE,mBAAoB,MAAM,EAAS,OAGxC,CACD,CACE,KAAM,kBAEN,QAAS,oCAAoC,EAAgB,KAAK,WAAW,EAAY,6BAA6B,EAAe,QAAQ,GAC7I,KAAM,OACN,QAAS,EACT,QAAS,EAAkB,KAAM,GAAM,IAAM,EAAY,CAC1D,CACF,CAAC,CACF,EAAe,aAAa,GAAe,GAC3C,CACF,OAGF,MAAU,MAAM,kCAAkC,EAAgB,OAAO,GAE3E,CAEF,EAAa,oBAAoB,GAAO,GACxC,CAEK,EC5KT,eAAsB,EACpB,EACA,EAMC,CAKD,IAAM,EAAgC,EAHlB,EAAK,EAAY,IAAK,GAAM,OAAO,KAAK,EAAE,CAAC,CAAC,MAAM,CAGV,CAAE,CAC5D,GAAI,EAAa,iBAAmB,CAAC,EAAa,iBAAiB,CAAG,EAAE,CACxE,GAAG,OAAO,KAAK,EAAa,oBAAoB,CACjD,CAAC,CAGF,GAAI,CAAC,EAAa,iBAAkB,CAClC,GAAM,CAAE,kBAAmB,MAAM,EAAS,OAGvC,CACD,CACE,KAAM,iBACN,QACE,yFACF,KAAM,OACN,QACE,EAA8B,KAAM,GAAQ,EAAI,aAAa,CAAC,SAAS,QAAQ,CAAC,EAChF,EAA8B,GAChC,QAAS,EACV,CACF,CAAC,CACF,EAAa,iBAAmB,EAElC,EAAO,KAAK,EAAO,QAAQ,4BAA4B,EAAa,iBAAiB,GAAG,CAAC,CAGzF,IAAM,EAA2B,EAC9B,KAAK,EAAM,IAAS,EAAK,EAAa,kBAAqB,KAAO,CAAC,EAAI,CAAE,CACzE,OAAQ,GAAqB,CAAC,CAAC,EAAE,CACjC,MAAM,CACT,GAAI,EAAyB,OAAS,EAAG,CACvC,IAAM,EAAM,0BACV,EAAa,iBACd,+CAA+C,EAAyB,KAAK,KAAK,GAOnF,GANA,EAAO,KAAK,EAAO,OAAO,EAAI,CAAC,CAM3B,CAAC,MAHc,EAAuB,CACxC,QAAS,qDACV,CAAC,CAEA,MAAU,MAAM,EAAI,CAItB,IAAM,EAAW,EAAY,OAC7B,EAAc,EAAY,OAAQ,GAAS,EAAK,EAAa,kBAAmB,CAChF,EAAO,KACL,EAAO,OAAO,WAAW,EAAW,EAAY,OAAO,6BAA6B,CACrF,CAEH,EAAO,KACL,EAAO,QACL,0BAA0B,EAAa,iBAAiB,2BACzD,CACF,CAGD,IAAM,EAAe,EAAQ,EAAa,EAAa,iBAAiB,CAClE,EAAuB,OAAO,QAAQ,EAAa,CAAC,QAAQ,EAAG,KAAU,EAAK,OAAS,EAAE,CAC/F,GAAI,EAAqB,OAAS,EAAG,CACnC,IAAM,EAAM,0BACV,EAAa,iBACd,iDAAiD,EAC/C,MAAM,EAAG,GAAG,CACZ,KAAK,CAAC,EAAQ,KAAU,GAAG,EAAO,IAAI,EAAK,OAAO,GAAG,CACrD,KAAK;EAAK,GAQb,GAPA,EAAO,KAAK,EAAO,OAAO,EAAI,CAAC,CAO3B,CAAC,MAHc,EAAuB,CACxC,QAAS,0DACV,CAAC,CAEA,MAAU,MAAM,EAAI,CAEtB,EAAc,OAAO,QAAQ,EAAa,CACvC,KAAK,EAAG,KACQ,EAAK,MACjB,EAAG,IACF,IAAI,KAAK,EAAE,EAAa,gBAAiB,CAAC,SAAS,CACnD,IAAI,KAAK,EAAE,EAAa,gBAAiB,CAAC,SAAS,CAE1C,CAAC,GACd,CACD,OAAQ,GAAM,EAAE,CAGrB,MAAO,CAAE,eAAc,cAAa,CCpGtC,eAAsB,EACpB,EACA,EAC4B,CAK5B,IAAM,EAA+B,EAHjB,EAAK,EAAY,IAAK,GAAM,OAAO,KAAK,EAAE,CAAC,CAAC,MAAM,CAGX,CAAE,CAC3D,GAAI,EAAa,iBAAmB,CAAC,EAAa,iBAAiB,CAAG,EAAE,CACxE,GAAG,OAAO,KAAK,EAAa,oBAAoB,CACjD,CAAC,CAGF,GAAI,CAAC,EAAa,eAAgB,CAChC,GAAM,CAAE,iBAAkB,MAAM,EAAS,OAGtC,CACD,CACE,KAAM,gBACN,QAAS,iFACT,KAAM,OACN,QACE,EAA6B,KAAM,GAAQ,EAAI,aAAa,CAAC,SAAS,OAAO,CAAC,EAC9E,EAA6B,KAAM,GAAQ,EAAI,aAAa,CAAC,SAAS,OAAO,CAAC,EAC9E,EAA6B,GAC/B,QAAS,CAAC,GAAG,EAA8B,SAAoB,CAChE,CACF,CAAC,CACF,EAAa,eAAiB,EAKhC,GAHA,EAAO,KAAK,EAAO,QAAQ,2BAA2B,EAAa,eAAe,GAAG,CAAC,CAGlF,EAAa,iBAAA,SAAwC,CACvD,IAAM,EAA0B,EAC7B,KAAK,EAAM,IAAS,EAAK,EAAa,gBAAmB,KAAO,CAAC,EAAI,CAAE,CACvE,OAAQ,GAAqB,CAAC,CAAC,EAAE,CACjC,MAAM,CACT,GAAI,EAAwB,OAAS,EACnC,MAAU,MACR,yBACE,EAAa,eACd,+CAA+C,EAAwB,KAAK;EAAK,GACnF,CAEH,EAAO,KACL,EAAO,QACL,yBAAyB,EAAa,eAAe,0BACtD,CACF,CAEH,OAAO,EC9CT,eAAsB,EACpB,CACE,OACA,SACA,eACA,mBACA,eACA,0BACA,yBAiBF,EACe,CAEf,IAAM,EAAK,IAAI,MAAM,CAAC,SAAS,CAGzB,EAAe,EAAM,SAAS,eAAe,CAGnD,EAAO,KAAK,EAAO,QAAQ,qBAAqB,EAAK,GAAG,CAAC,CACzD,IAAI,EAAc,EAAQ,EAAM,EAAE,OAAO,EAAE,OAAQ,EAAE,OAAO,CAAC,CAGzD,EAAkC,CACpC,oBAAqB,EAAE,CACvB,mBAAoB,EAAE,CACtB,uBAAwB,EAAE,CAC1B,eAAgB,EAAE,CAElB,GAAK,EAAa,IAAS,EAAE,CAC7B,cAAe,IAAI,MAAM,CAAC,aAAa,CACxC,CAGD,EAAe,MAAM,EAAiC,EAAa,EAAa,CAChF,EAAa,GAAQ,EACrB,MAAM,EAAM,SAAS,EAAc,eAAe,CAGlD,IAAM,EAAS,MAAM,EAAkC,EAAa,EAAa,CACjF,EAAe,EAAO,aACtB,EAAc,EAAO,YACrB,EAAa,GAAQ,EACrB,MAAM,EAAM,SAAS,EAAc,eAAe,CAIlD,EAAe,MAAM,EAAuC,EAAa,EAAc,CACrF,mBACA,eACA,wBACD,CAAC,CACF,EAAa,GAAQ,EACrB,MAAM,EAAM,SAAS,EAAc,eAAe,CAGlD,IAAM,EAAc,EAAY,IAAK,GAAS,EAAK,EAAa,kBAAmB,CAC7E,EAAc,IAAI,EAAY,UAAU,EAAE,CAAE,EAAY,QAAQ,eAAe,CAChF,GACH,EAAY,MAAM,EAAY,OAAQ,EAAE,CAE1C,IAAM,EAAyB,EAC3B,EAAE,CACF,MAAM,EAA6B,EAAQ,CACzC,YAAa,EAAY,IAAK,IAAO,CAAE,MAAO,EAAG,EAAE,CACnD,eACA,SACA,YAAa,EAAW,IAAU,EAAY,OAAO,EAAW,CAAE,QAAO,CAAC,CAC3E,CAAC,CACN,EAAY,MAAM,CAClB,IAAM,EAA4B,EAAM,EAAwB,SAAS,CAGzE,EAAa,uBAAyB,EAAE,CACxC,EAAa,mBAAqB,EAAE,CACpC,EAAa,eAAiB,EAAE,CAGhC,EAAY,QAAS,GAAS,CAE5B,IAAM,EAAS,EAAK,EAAa,kBAG3B,EAAiB,EAA4B,CACjD,IAAK,EACL,oBAAqB,EAAa,oBAClC,mBACA,eACD,CAAC,CAGI,EAAuB,EAA0B,GACvD,GAAI,GAAyB,CAAC,EAC5B,MAAU,MACR,sDAAsD,EAAO;sGAE9D,CAKH,GACE,GACA,EAAuC,CACrC,uBACA,iBACA,mBACD,CAAC,EACF,CAAC,EACD,CACA,EAAa,eAAe,GAAU,EACtC,OAIF,GACE,GACA,EAA6C,CAC3C,uBACA,iBACA,mBACD,CAAC,CACF,CACA,EAAa,uBAAuB,GAAU,CAC5C,IAAK,EACL,OAAQ,EACT,CACD,OAIF,EAAa,mBAAmB,GAAU,GAC1C,CAGF,EAAa,GAAQ,EACrB,MAAM,EAAM,SAAS,EAAc,eAAe,CAClD,IAAM,EAAK,IAAI,MAAM,CAAC,SAAS,CAC/B,EAAO,KAAK,EAAO,MAAM,qCAAqC,EAAK,QAAQ,EAAK,GAAM,IAAK,GAAG,CAAC,CC7JjG,eAAsB,EAAiD,CACrE,OACA,aACA,kBACA,OACA,YACA,WAAW,GACX,SAAS,GACT,uBAAuB,GACvB,sBAAsB,GACtB,0BAA0B,GAC1B,aAAa,EAAE,CACf,eACA,wBAAwB,IA+BR,CAEhB,IAAM,EAAmB,EAA0B,EAAW,CAGxD,EAAkB,IAAI,EAAe,EAAiB,EAAiB,CAC3E,aAAc,EAAE,CAChB,eAAgB,EAAE,CAClB,eAAgB,EAAE,CACnB,CAAC,CACI,EAAkB,EAAgB,SAAS,iBAAiB,CAC5D,EAAkB,EAAgB,SAAS,iBAAiB,CAC9D,EAAe,EAAgB,SAAS,eAAe,CAE3D,EAAO,KACL,EAAO,QACL;EACK,OAAO,OAAO,EAAgB,CAAC,OAAO,mCACtC,OAAO,OAAO,EAAgB,CAAC,OAAO,gGACqB,OAAO,KAAK,EAAa,CACpF,IAAK,GAAM,EAAE,CACb,KAAK;EAAK,CAAC,0CAC2B,EAAK,IACjD,CACF,CAGD,IAAM,EAAS,EAA4B,EAAc,EAAK,CAExD,CAAC,EAAQ,EAAU,GAAoB,MAAM,QAAQ,IAAI,CAE7D,EAAwB,EAAc,EAAM,CAC1C,SACA,aAAc,EACd,UAAW,QAAQ,IAAI,WACxB,CAAC,CAEF,EAAiB,EAAQ,CAAE,SAAQ,CAAC,CACpC,EAAyB,EAAQ,CAAE,SAAQ,CAAC,CAC7C,CAAC,CAGF,MAAM,EACJ,CACE,OACA,aAAc,EAAS,IAAK,GAAM,EAAE,aAAa,CACjD,mBACA,SACA,aAAc,EACd,0BACA,wBACD,CACD,EACD,CAGD,IAAM,EAAuD,EAAE,CAC/D,EAAe,EAAgB,SAAS,eAAe,CACvD,IAAM,EAAW,EAAa,GAwD9B,GAtDA,EAAO,KACL,EAAO,QACL,SAAS,OAAO,QAAQ,EAAS,mBAAmB,CAAC,OAAO,mBAAmB,IAChF,CACF,CACD,EAAO,KACL,EAAO,QACL,SAAS,OAAO,QAAQ,EAAS,uBAAuB,CAAC,OAAO,uBAAuB,IACxF,CACF,CACD,EAAO,KACL,EAAO,QACL,SAAS,OAAO,QAAQ,EAAS,eAAe,CAAC,OAAO,sBAAsB,IAC/E,CACF,CAGD,OAAO,QAAQ,CACb,GAAG,EAAS,mBACZ,GAAI,EAAsB,EAAE,CAAG,EAAM,EAAS,wBAAyB,CAAE,SAAU,EAAI,CACxF,CAAC,CAAC,SAAS,CAAC,EAAQ,KAAY,CAE/B,IAAM,EACJ,EAAS,iBAAA,SACL,IAAI,KACJ,IAAI,KAAK,EAAO,EAAS,gBAAiB,CAG1C,EAAU,EAA4B,CAC1C,IAAK,EACL,oBAAqB,EAAS,oBAC9B,mBACA,aAAc,EAAS,IAAK,GAAM,EAAE,aAAa,CAClD,CAAC,CACF,EAAe,GAAU,CACvB,SACA,YACA,UAAW,EAAU,aAAa,CAClC,SAAU,OAAO,QAAQ,EAAQ,CAAC,KAAK,CAAC,EAAS,MAAY,CAC3D,GAAG,EACH,UACA,iBAAkB,CAChB,WAAY,EACZ,WACA,oBAAqB,EACrB,GAAI,EAAwB,CAAE,qBAAsB,EAAuB,CAAG,EAAE,CACjF,CACF,EAAE,CACJ,EACD,CACF,MAAM,EAAgB,SAAS,EAAgB,iBAAiB,CAChE,MAAM,EAAgB,SAAS,EAAE,CAAE,iBAAiB,CAGhD,EAAQ,CACV,EAAO,KACL,EAAO,MACL,8BACE,OAAO,OAAO,EAAe,CAAC,OAC/B,gCAAgC,IAClC,CACF,CACD,OAGF,EAAO,KACL,EAAO,QACL,aAAa,OAAO,OAAO,EAAe,CAAC,OAAO,6BAA6B,IAChF,CACF,CAGD,IAAM,EAAK,IAAI,MAAM,CAAC,SAAS,CAGzB,EAAc,IAAI,EAAY,UAAU,EAAE,CAAE,EAAY,QAAQ,eAAe,CAGjF,EAAQ,EACN,EAAe,OAAO,QAAQ,EAAe,CAC7C,EAAiB,EAAM,EAAc,EAAuB,IAAM,GAAG,CAC3E,EAAY,MAAM,EAAa,OAAQ,EAAE,CACzC,MAAM,EACJ,EACA,KAAO,IAAiB,CAEtB,GAAI,CACF,MAAM,EACH,IAAI,iBAAkB,CACrB,KAAM,CACJ,QAAS,EAAa,KAAK,EAAG,KAAY,EAAO,CACjD,uBACD,CACF,CAAC,CACD,MAAM,OACF,EAAK,CACZ,GAAI,CACF,IAAM,EAAS,KAAK,MAAM,GAAK,UAAU,MAAQ,KAAK,CAClD,EAAO,OACT,EAAO,MAAM,EAAO,IAAI,UAAU,EAAO,QAAQ,CAAC,MAE9C,EAGR,EAAO,MACL,EAAO,IACL,oBAAoB,EAAa,OAAO,iCAAiC,EAAU,IACjF,GAAK,UAAU,MAAQ,GAAK,UAE/B,CACF,CACD,IAAM,EAAiB,EAAgB,SAAS,iBAAiB,CACjE,EAAa,SAAS,CAAC,EAAQ,KAAY,CACzC,EAAe,GAAU,CACvB,WAAY,IAAI,MAAM,CAAC,aAAa,CACpC,SACA,MAAO,GAAK,UAAU,MAAQ,GAAK,SAAW,gBAC/C,EACD,CACF,MAAM,EAAgB,SAAS,EAAgB,iBAAiB,CAGlE,GAAS,EAAa,OACtB,EAAY,OAAO,EAAM,EAE3B,CACE,YAAa,GACd,CACF,CAED,EAAY,MAAM,CAElB,IAAM,EADK,IAAI,MAAM,CAAC,SACF,CAAG,EACvB,EAAO,KACL,EAAO,MACL,yBACE,EAAa,OACd,iCAAiC,EAAU,OAAO,EAAY,IAAK,YACrE,CACF,CC/OH,eAAsB,EAEpB,CACE,OACA,YACA,aACA,eACA,OAAO,GACP,YACA,SACA,0BACA,iBACA,uBACA,wBACA,sBACA,WACA,aACA,eAEa,CACT,GAAe,IACnB,EAAO,MACL,EAAO,IAAI,uEAAuE,CACnF,CACD,KAAK,QAAQ,KAAK,EAAE,EAGlB,CAAC,GAAQ,CAAC,IACZ,EAAO,MACL,EAAO,IACL,uHACD,CACF,CACD,KAAK,QAAQ,KAAK,EAAE,EAGtB,EAAoB,KAAK,QAAQ,KAAK,CAEtC,IAAM,EAAkB,EAAE,CAE1B,GAAI,EACF,GAAI,CAEF,IAAM,EADmB,EAAY,EACJ,CAAC,OAAQ,GAAS,EAAK,SAAS,OAAO,CAAC,CAErE,EAAS,SAAW,IACtB,EAAO,MAAM,EAAO,IAAI,oCAAoC,IAAY,CAAC,CACzE,KAAK,QAAQ,KAAK,EAAE,EAItB,EAAM,KAAK,GAAG,EAAS,IAAK,GAAS,EAAK,EAAW,EAAK,CAAC,CAAC,OACrD,EAAK,CACZ,EAAO,MAAM,EAAO,IAAI,6BAA6B,IAAY,CAAC,CAClE,EAAO,MAAM,EAAO,IAAK,EAAc,QAAQ,CAAC,CAChD,KAAK,QAAQ,KAAK,EAAE,MAGtB,GAAI,CAEG,EAAK,SAAS,OAAO,GACxB,EAAO,MAAM,EAAO,IAAI,0BAA0B,CAAC,CACnD,KAAK,QAAQ,KAAK,EAAE,EAEtB,EAAM,KAAK,EAAK,OACT,EAAK,CACZ,EAAO,MAAM,EAAO,IAAI,0BAA0B,IAAO,CAAC,CAC1D,EAAO,MAAM,EAAO,IAAK,EAAc,QAAQ,CAAC,CAChD,KAAK,QAAQ,KAAK,EAAE,CAIxB,EAAO,KACL,EAAO,MACL,cAAc,EAAM,OAAO,4CAA4C,IACxE,CACF,CACD,EAAO,MAAM,qBAAqB,EAAM,KAAK,KAAK,GAAG,CAEjD,GACF,EAAO,KAAK,EAAO,SAAS,mCAAmC,IAA0B,CAAC,CAG5F,MAAM,EACJ,EACA,KAAO,IAAa,CAElB,MAAM,EAAiD,CACrD,gBAAiB,EAAK,EAAgB,GAFvB,EAAS,EAAS,CAAC,QAAQ,OAAQ,GAED,CAAC,gBAAgB,CAClE,OACA,aACA,KAAM,EACN,YACA,eACA,sBACA,uBACA,0BACA,WACA,SACA,WAAY,EAAe,EAAW,CACtC,wBACD,CAAC,EAEJ,CAAE,cAAa,CAChB"}