Skip to content

Commit 9bcd613

Browse files
committed
fix(connectors): ado probes past the wiql 20k cap before flagging; document custom-wiql full-listing behavior
1 parent fa66527 commit 9bcd613

1 file changed

Lines changed: 41 additions & 10 deletions

File tree

apps/sim/connectors/azure-devops/azure-devops.ts

Lines changed: 41 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -520,11 +520,17 @@ function readWorkItemFilters(sourceConfig: Record<string, unknown>): WorkItemFil
520520

521521
/**
522522
* Builds the WIQL query for the configured work-item filters. User-supplied
523-
* values are escaped against WIQL string-literal injection. When a custom WIQL
524-
* query is provided it is used verbatim and the structured filters are ignored.
525-
* `lastSyncAt` narrows results to items changed since the previous sync.
523+
* values are escaped against WIQL string-literal injection. `lastSyncAt`
524+
* narrows results to items changed since the previous sync, and `idAfter`
525+
* restricts to items with a greater id (used to probe past the 20,000-item
526+
* WIQL cap).
527+
*
528+
* A custom WIQL query is used verbatim: neither the incremental changed-date
529+
* filter nor the probe condition can be injected into arbitrary user WIQL
530+
* safely, so custom queries always run as full listings on every sync. Change
531+
* detection still short-circuits unchanged items via the content hash.
526532
*/
527-
function buildWiql(filters: WorkItemFilters, lastSyncAt?: Date): string {
533+
function buildWiql(filters: WorkItemFilters, lastSyncAt?: Date, idAfter?: number): string {
528534
if (filters.customWiql) return filters.customWiql
529535

530536
const clauses: string[] = ['[System.TeamProject] = @project']
@@ -543,6 +549,9 @@ function buildWiql(filters: WorkItemFilters, lastSyncAt?: Date): string {
543549
if (lastSyncAt) {
544550
clauses.push(`[System.ChangedDate] >= '${lastSyncAt.toISOString()}'`)
545551
}
552+
if (idAfter !== undefined) {
553+
clauses.push(`[System.Id] > ${idAfter}`)
554+
}
546555

547556
return `SELECT [System.Id] FROM workitems WHERE ${clauses.join(' AND ')} ORDER BY [System.ChangedDate] DESC`
548557
}
@@ -556,9 +565,10 @@ async function queryWorkItemIds(
556565
accessToken: string,
557566
organization: string,
558567
project: string,
559-
wiql: string
568+
wiql: string,
569+
top: number = WIQL_MAX_RESULTS
560570
): Promise<number[]> {
561-
const url = `${ADO_BASE_URL}/${encodeURIComponent(organization)}/${encodeURIComponent(project)}/_apis/wit/wiql?$top=${WIQL_MAX_RESULTS}&api-version=${WIQL_API_VERSION}`
571+
const url = `${ADO_BASE_URL}/${encodeURIComponent(organization)}/${encodeURIComponent(project)}/_apis/wit/wiql?$top=${top}&api-version=${WIQL_API_VERSION}`
562572
const response = await fetchWithRetry(url, {
563573
method: 'POST',
564574
headers: {
@@ -1250,10 +1260,31 @@ async function listWorkItems(
12501260
const wiql = buildWiql(filters, lastSyncAt)
12511261
ids = await queryWorkItemIds(accessToken, organization, project, wiql)
12521262
if (syncContext) syncContext.workItemIds = ids
1253-
}
12541263

1255-
if (ids.length >= WIQL_MAX_RESULTS && syncContext) {
1256-
syncContext.listingCapped = true
1264+
if (ids.length >= WIQL_MAX_RESULTS && syncContext) {
1265+
/**
1266+
* The WIQL result filled the 20,000-item cap. Distinguish an exact fit
1267+
* from genuine truncation: for structured filters, probe for any
1268+
* matching item with an id beyond the largest returned one and only
1269+
* flag the listing incomplete when one exists — otherwise deletion
1270+
* reconciliation would be disabled forever for a project with exactly
1271+
* 20,000 matching items. Custom WIQL cannot be probed (no safe clause
1272+
* injection), so it is flagged conservatively.
1273+
*/
1274+
let truncated = true
1275+
if (!filters.customWiql) {
1276+
let maxId = 0
1277+
for (const id of ids) {
1278+
if (id > maxId) maxId = id
1279+
}
1280+
const probeWiql = buildWiql(filters, lastSyncAt, maxId)
1281+
const beyond = await queryWorkItemIds(accessToken, organization, project, probeWiql, 1)
1282+
truncated = beyond.length > 0
1283+
}
1284+
if (truncated) {
1285+
syncContext.listingCapped = true
1286+
}
1287+
}
12571288
}
12581289

12591290
if (ids.length === 0) {
@@ -1405,7 +1436,7 @@ export const azureDevopsConnector: ConnectorConfig = {
14051436
mode: 'advanced',
14061437
placeholder: 'SELECT [System.Id] FROM workitems WHERE ...',
14071438
description:
1408-
'Advanced: a full WIQL query selecting [System.Id]. Overrides the type, state, area path, and tag filters when set.',
1439+
'Advanced: a full WIQL query selecting [System.Id]. Overrides the type, state, area path, and tag filters when set. Custom queries always run as full listings on every sync (the incremental changed-date filter is not applied).',
14091440
},
14101441
{
14111442
id: 'repositoryName',

0 commit comments

Comments
 (0)