{"version":3,"file":"pullChunkedCustomSiloOutstandingIdentifiers-BUi0WXb9.mjs","names":[],"sources":["../src/lib/graphql/fetchRequestDataSiloActiveCount.ts","../src/lib/cron/pullCronPageOfIdentifiers.ts","../src/lib/cron/pullChunkedCustomSiloOutstandingIdentifiers.ts"],"sourcesContent":["import { makeGraphQLRequest, REDUCED_REQUESTS_FOR_DATA_SILO_COUNT } from '@transcend-io/sdk';\nimport { GraphQLClient } from 'graphql-request';\n\nimport { logger } from '../../logger.js';\n\n/**\n * Get number of open requests for a data silo\n *\n * @param client - GraphQL client\n * @param options - Filter options\n * @returns List of request identifiers\n */\nexport async function fetchRequestDataSiloActiveCount(\n  client: GraphQLClient,\n  {\n    dataSiloId,\n  }: {\n    /** Data silo ID */\n    dataSiloId: string;\n  },\n): Promise<number> {\n  const {\n    listReducedRequestsForDataSilo: { totalCount },\n  } = await makeGraphQLRequest<{\n    /** Requests */\n    listReducedRequestsForDataSilo: {\n      /** Total count */\n      totalCount: number;\n    };\n  }>(client, REDUCED_REQUESTS_FOR_DATA_SILO_COUNT, {\n    variables: {\n      input: {\n        dataSiloId,\n        isResolved: false,\n      },\n    },\n    logger,\n  });\n\n  return totalCount;\n}\n","import { RequestAction } from '@transcend-io/privacy-types';\nimport { withTransientRetry } from '@transcend-io/sdk';\nimport { decodeCodec } from '@transcend-io/type-utils';\nimport type { Got } from 'got';\nimport * as t from 'io-ts';\n\nimport { logger } from '../../logger.js';\n\n/** Consent partition metadata when a DSR is scoped to a partition */\nexport const CronPartition = t.type({\n  /** ID of the partition */\n  id: t.string,\n  /** The human readable name of the partition */\n  name: t.string,\n  /** The unique identifying partition value */\n  partition: t.string,\n});\n\n/** Type override */\nexport type CronPartition = t.TypeOf<typeof CronPartition>;\n\nexport const CronIdentifier = t.intersection([\n  t.type({\n    /** The identifier value */\n    identifier: t.string,\n    /** The type of identifier */\n    type: t.string,\n    /** The core identifier of the request */\n    coreIdentifier: t.string,\n    /** The ID of the underlying data silo */\n    dataSiloId: t.string,\n    /** The ID of the underlying request */\n    requestId: t.string,\n    /** The request nonce */\n    nonce: t.string,\n    /** The time the request was created */\n    requestCreatedAt: t.string,\n    /** The number of days until the request is overdue */\n    daysUntilOverdue: t.number,\n    /** Request attributes */\n    attributes: t.array(\n      t.type({\n        key: t.string,\n        values: t.array(t.string),\n      }),\n    ),\n  }),\n  t.partial({\n    /** The consent partition that scopes this request, when applicable */\n    partition: t.union([CronPartition, t.null]),\n  }),\n]);\n\n/** Type override */\nexport type CronIdentifier = t.TypeOf<typeof CronIdentifier>;\n\n/**\n * Pull a offset of identifiers for a cron job\n *\n * @see https://docs.transcend.io/docs/api-reference/GET/v1/data-silo/(id)/pending-requests/(type)\n * @param sombra - Sombra instance configured to make requests\n * @param options - Additional options\n * @returns Successfully submitted request\n */\nexport async function pullCronPageOfIdentifiers(\n  sombra: Got,\n  {\n    dataSiloId,\n    limit = 100,\n    offset = 0,\n    requestType,\n  }: {\n    /** Data Silo ID */\n    dataSiloId: string;\n    /** Type of request */\n    requestType: RequestAction;\n    /** Number of identifiers to pull in */\n    limit?: number;\n    /** Page to pull in */\n    offset?: number;\n  },\n): Promise<CronIdentifier[]> {\n  try {\n    // `GET pending-requests` is a read and therefore safe to retry on transient\n    // gateway / network errors. Customers running `transcend cron\n    // pull-identifiers` against large silos would otherwise see the command\n    // abort on a single 502 from the Sombra reverse tunnel.\n    const response = await withTransientRetry(\n      'pullCronPageOfIdentifiers',\n      () =>\n        sombra\n          .get(`v1/data-silo/${dataSiloId}/pending-requests/${requestType}`, {\n            searchParams: {\n              offset,\n              limit,\n            },\n          })\n          .json(),\n      { logger, maxAttempts: 6, baseDelayMs: 500 },\n    );\n\n    const { items } = decodeCodec(\n      t.type({\n        items: t.array(CronIdentifier),\n      }),\n      response,\n    );\n    return items;\n  } catch (err) {\n    throw new Error(`Received an error from server: ${err?.response?.body || err?.message}`);\n  }\n}\n","import { RequestAction } from '@transcend-io/privacy-types';\nimport { buildTranscendGraphQLClient, createSombraGotInstance } from '@transcend-io/sdk';\nimport { mapSeries } from '@transcend-io/utils';\nimport cliProgress from 'cli-progress';\nimport colors from 'colors';\n\nimport { DEFAULT_TRANSCEND_API } from '../../constants.js';\nimport { logger } from '../../logger.js';\nimport { fetchRequestDataSiloActiveCount } from '../graphql/index.js';\nimport { pullCronPageOfIdentifiers, CronIdentifier } from './pullCronPageOfIdentifiers.js';\n\n/**\n * A CSV formatted identifier\n */\nexport type CsvFormattedIdentifier = {\n  [k in string]: string | null | boolean | number;\n};\n\nexport interface CronIdentifierWithAction extends CronIdentifier {\n  /** The request action that the identifier relates to */\n  action: RequestAction;\n}\n\n/**\n * Pull the set of identifiers outstanding for a cron or AVC integration\n *\n * This function is designed to be used in a loop, and will call the onSave callback\n * with a chunk of identifiers when the savePageSize is reached.\n *\n * @param options - Options\n * @returns The identifiers and identifiers formatted for CSV\n */\nexport async function pullChunkedCustomSiloOutstandingIdentifiers({\n  dataSiloId,\n  auth,\n  sombraAuth,\n  actions,\n  apiPageSize = 100,\n  savePageSize = 1000,\n  onSave,\n  transcendUrl = DEFAULT_TRANSCEND_API,\n  skipRequestCount = false,\n}: {\n  /** Transcend API key authentication */\n  auth: string;\n  /** Data Silo ID to pull down jobs for */\n  dataSiloId: string;\n  /** The request actions to fetch */\n  actions: RequestAction[];\n  /** How many identifiers to pull in a single call to the backend */\n  apiPageSize: number;\n  /** How many identifiers to save at a time (usually to a CSV file, should be a multiple of apiPageSize) */\n  savePageSize: number;\n  /** Callback function called when a chunk of identifiers is ready to be saved */\n  onSave: (chunk: CsvFormattedIdentifier[]) => Promise<void>;\n  /** API URL for Transcend backend */\n  transcendUrl?: string;\n  /** Sombra API key authentication */\n  sombraAuth?: string;\n  /** Skip request count */\n  skipRequestCount?: boolean;\n}): Promise<{\n  /** Raw Identifiers */\n  identifiers: CronIdentifierWithAction[];\n}> {\n  // Validate savePageSize\n  if (savePageSize % apiPageSize !== 0) {\n    throw new Error(\n      `savePageSize must be a multiple of apiPageSize. savePageSize: ${savePageSize}, apiPageSize: ${apiPageSize}`,\n    );\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\n  // Create GraphQL client to connect to Transcend backend\n  const client = buildTranscendGraphQLClient(transcendUrl, auth);\n\n  let totalRequestCount = 0;\n  if (!skipRequestCount) {\n    totalRequestCount = await fetchRequestDataSiloActiveCount(client, {\n      dataSiloId,\n    });\n  }\n\n  logger.info(\n    colors.magenta(\n      `Pulling ${skipRequestCount ? 'all' : totalRequestCount} outstanding request identifiers ` +\n        `for data silo: \"${dataSiloId}\" for requests of types \"${actions.join('\", \"')}\"`,\n    ),\n  );\n\n  // Time duration\n  const t0 = new Date().getTime();\n  // create a new progress bar instance and use shades_classic theme\n  const progressBar = new cliProgress.SingleBar({}, cliProgress.Presets.shades_classic);\n  const foundRequestIds = new Set<string>();\n\n  // identifiers found in total\n  const identifiers: CronIdentifierWithAction[] = [];\n  // current chunk of identifiers to be saved\n  let currentChunk: CsvFormattedIdentifier[] = [];\n\n  // map over each action\n  if (!skipRequestCount) {\n    progressBar.start(totalRequestCount, 0);\n  }\n  await mapSeries(actions, async (action) => {\n    let offset = 0;\n    let shouldContinue = true;\n\n    // Fetch a page of identifiers\n    while (shouldContinue) {\n      const pageIdentifiers = await pullCronPageOfIdentifiers(sombra, {\n        dataSiloId,\n        limit: apiPageSize,\n        offset,\n        requestType: action,\n      });\n\n      const identifiersWithAction: CronIdentifierWithAction[] = pageIdentifiers.map(\n        (identifier) => {\n          foundRequestIds.add(identifier.requestId);\n          return {\n            ...identifier,\n            action,\n          };\n        },\n      );\n\n      const csvFormattedIdentifiers = identifiersWithAction.map(\n        ({ attributes, partition, ...identifier }) => ({\n          ...identifier,\n          ...(partition\n            ? {\n                partitionId: partition.id,\n                partitionName: partition.name,\n                partitionKey: partition.partition,\n              }\n            : {}),\n          ...attributes.reduce(\n            (acc, val) =>\n              Object.assign(acc, {\n                [val.key]: val.values.join(','),\n              }),\n            {},\n          ),\n        }),\n      );\n\n      identifiers.push(...identifiersWithAction);\n      currentChunk.push(...csvFormattedIdentifiers);\n\n      // Check if we've reached the savePageSize and call the onSave callback\n      if (currentChunk.length >= savePageSize) {\n        await onSave(currentChunk);\n        currentChunk = [];\n      }\n\n      shouldContinue = pageIdentifiers.length === apiPageSize;\n      offset += apiPageSize;\n      if (!skipRequestCount) {\n        progressBar.update(foundRequestIds.size);\n      } else {\n        logger.info(\n          colors.magenta(\n            `Pulled ${pageIdentifiers.length} outstanding identifiers for ${foundRequestIds.size} requests`,\n          ),\n        );\n      }\n    }\n  });\n\n  // Save any remaining identifiers in the current chunk\n  if (currentChunk.length > 0) {\n    await onSave(currentChunk);\n  }\n\n  if (!skipRequestCount) {\n    progressBar.stop();\n  }\n  const t1 = new Date().getTime();\n  const totalTime = t1 - t0;\n\n  logger.info(\n    colors.green(\n      `Successfully pulled ${identifiers.length} outstanding identifiers from ${\n        foundRequestIds.size\n      } requests in \"${totalTime / 1000}\" seconds!`,\n    ),\n  );\n\n  return { identifiers };\n}\n"],"mappings":"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"}