doc2vec 2.9.2 → 2.10.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.
package/README.md CHANGED
@@ -218,6 +218,7 @@ Configuration is managed through two files:
218
218
  * `start_date`: (Optional) Only process tickets/articles updated since this date (e.g., `'2025-01-01'`).
219
219
  * `ticket_status`: (Optional) Filter tickets by status (defaults to `['new', 'open', 'pending', 'hold', 'solved']`).
220
220
  * `ticket_priority`: (Optional) Filter tickets by priority (defaults to all priorities).
221
+ * `excluded_organizations`: (Optional) An array of Zendesk organization names whose tickets should be skipped. The sync will abort if any name cannot be resolved.
221
222
 
222
223
  For S3 buckets (`type: 's3'`):
223
224
  * `bucket`: The S3 bucket name.
@@ -359,6 +360,7 @@ Configuration is managed through two files:
359
360
  start_date: '2025-01-01'
360
361
  ticket_status: ['open', 'pending']
361
362
  ticket_priority: ['high']
363
+ excluded_organizations: ['Acme Corp', 'Internal Testing']
362
364
  max_size: 1048576
363
365
  database_config:
364
366
  type: 'sqlite'
package/dist/doc2vec.js CHANGED
@@ -1195,6 +1195,8 @@ class Doc2Vec {
1195
1195
  // Status filter applied client-side (includes 'closed' by default so tickets
1196
1196
  // transitioning to closed get updated rather than left stale)
1197
1197
  const statusFilter = new Set(config.ticket_status || ['new', 'open', 'pending', 'hold', 'solved', 'closed']);
1198
+ const excludedOrgNames = new Set((config.excluded_organizations || []).map(n => n.toLowerCase()));
1199
+ const excludedOrgIds = new Set();
1198
1200
  const fetchWithRetry = async (url, retries = 3) => {
1199
1201
  for (let attempt = 0; attempt < retries; attempt++) {
1200
1202
  try {
@@ -1271,6 +1273,11 @@ class Doc2Vec {
1271
1273
  }
1272
1274
  return;
1273
1275
  }
1276
+ // Skip tickets belonging to excluded organizations
1277
+ if (ticket.organization_id && excludedOrgIds.has(ticket.organization_id)) {
1278
+ logger.debug(`Ticket #${ticketId} belongs to excluded organization ${ticket.organization_id} — skipping`);
1279
+ return;
1280
+ }
1274
1281
  // Skip tickets whose status is outside the configured filter
1275
1282
  if (!statusFilter.has(ticket.status)) {
1276
1283
  logger.debug(`Ticket #${ticketId} has status '${ticket.status}' outside configured filter — skipping`);
@@ -1296,18 +1303,37 @@ class Doc2Vec {
1296
1303
  // so stale chunks from previous versions are never left behind.
1297
1304
  await this.processChunksForUrl(chunks, url, dbConnection, logger);
1298
1305
  };
1306
+ if (excludedOrgNames.size > 0) {
1307
+ logger.info(`Resolving ${excludedOrgNames.size} excluded organization name(s) to IDs`);
1308
+ const resolvedNames = new Set();
1309
+ let orgsUrl = `${baseUrl}/organizations.json?page[size]=100`;
1310
+ while (orgsUrl) {
1311
+ const orgsData = await fetchWithRetry(orgsUrl);
1312
+ for (const org of orgsData?.organizations || []) {
1313
+ const orgName = (org.name || '').toLowerCase();
1314
+ if (excludedOrgNames.has(orgName)) {
1315
+ excludedOrgIds.add(org.id);
1316
+ resolvedNames.add(orgName);
1317
+ }
1318
+ }
1319
+ orgsUrl = orgsData?.meta?.has_more ? orgsData?.links?.next : null;
1320
+ }
1321
+ logger.info(`Excluding tickets from ${excludedOrgIds.size} organization(s): ${[...excludedOrgIds].join(', ')}`);
1322
+ const unresolved = [...excludedOrgNames].filter(name => !resolvedNames.has(name));
1323
+ if (unresolved.length > 0) {
1324
+ throw new Error(`Cannot resolve excluded organization(s): ${unresolved.join(', ')}. Aborting to avoid syncing data for them.`);
1325
+ }
1326
+ }
1299
1327
  logger.info(`Fetching Zendesk tickets updated since ${lastRunDate}`);
1300
1328
  // Build query parameters — use the status filter for the search query
1301
1329
  const statusList = Array.from(statusFilter);
1302
- const query = `updated>${lastRunDate.split('T')[0]} status:${statusList.join(',status:')}`;
1303
- let nextPage = `${baseUrl}/search.json?query=${encodeURIComponent(query)}&sort_by=updated_at&sort_order=asc`;
1330
+ const statusClause = `status:${statusList.join(',status:')}`;
1331
+ // Zendesk /search.json caps results at 1000 (10 pages * 100). Bisect the date range
1332
+ // into smaller windows whenever a window would exceed that cap, so we never hit page 11.
1304
1333
  let totalTickets = 0;
1305
1334
  let failedTickets = 0;
1306
- while (nextPage) {
1307
- const data = await fetchWithRetry(nextPage);
1308
- const tickets = data.results || [];
1309
- logger.info(`Processing batch of ${tickets.length} tickets`);
1310
- for (const ticket of tickets) {
1335
+ const processResults = async (results) => {
1336
+ for (const ticket of results) {
1311
1337
  try {
1312
1338
  await processTicket(ticket);
1313
1339
  totalTickets++;
@@ -1317,11 +1343,39 @@ class Doc2Vec {
1317
1343
  logger.error(`Failed to process ticket #${ticket.id}, will retry next run: ${error.message}`);
1318
1344
  }
1319
1345
  }
1320
- nextPage = data.next_page || null;
1321
- if (nextPage) {
1346
+ };
1347
+ // Work queue of [start, end) date windows (ISO strings). Seed with [lastRunDate, syncStartDate).
1348
+ const windows = [[lastRunDate, syncStartDate]];
1349
+ const MAX_PER_WINDOW = 1000;
1350
+ while (windows.length > 0) {
1351
+ const [wStart, wEnd] = windows.shift();
1352
+ const q = `updated>${wStart} updated<${wEnd} ${statusClause}`;
1353
+ const firstUrl = `${baseUrl}/search.json?query=${encodeURIComponent(q)}&sort_by=updated_at&sort_order=asc`;
1354
+ logger.debug(`Searching window ${wStart} .. ${wEnd}`);
1355
+ const firstData = await fetchWithRetry(firstUrl);
1356
+ const count = firstData?.count ?? 0;
1357
+ if (count > MAX_PER_WINDOW) {
1358
+ const startMs = new Date(wStart).getTime();
1359
+ const endMs = new Date(wEnd).getTime();
1360
+ if (endMs - startMs <= 1000) {
1361
+ logger.warn(`Window ${wStart} .. ${wEnd} has ${count} tickets but is already ≤1s wide — processing first 1000 only, some may be missed`);
1362
+ }
1363
+ else {
1364
+ const midIso = new Date(startMs + Math.floor((endMs - startMs) / 2)).toISOString();
1365
+ logger.debug(`Window has ${count} tickets (>${MAX_PER_WINDOW}) — bisecting at ${midIso}`);
1366
+ windows.unshift([wStart, midIso], [midIso, wEnd]);
1367
+ continue;
1368
+ }
1369
+ }
1370
+ logger.info(`Processing window ${wStart}..${wEnd}: ${count} tickets`);
1371
+ await processResults(firstData.results || []);
1372
+ let nextPage = firstData.next_page || null;
1373
+ while (nextPage) {
1322
1374
  logger.debug(`Fetching next page: ${nextPage}`);
1323
- // Rate limiting: wait between requests
1324
1375
  await new Promise(res => setTimeout(res, 1000));
1376
+ const data = await fetchWithRetry(nextPage);
1377
+ await processResults(data.results || []);
1378
+ nextPage = data.next_page || null;
1325
1379
  }
1326
1380
  }
1327
1381
  // Only advance the watermark when all tickets succeeded
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "doc2vec",
3
- "version": "2.9.2",
3
+ "version": "2.10.1",
4
4
  "type": "commonjs",
5
5
  "description": "",
6
6
  "main": "dist/doc2vec.js",