Skip to content

Commit 8355b80

Browse files
committed
fix(connectors): ado auth-failure deletion guard, jsm last-page slice flag, google-forms response cap in hash
1 parent 6b66855 commit 8355b80

3 files changed

Lines changed: 73 additions & 12 deletions

File tree

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

Lines changed: 45 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -287,7 +287,8 @@ async function listWikis(
287287
accessToken: string,
288288
organization: string,
289289
project: string,
290-
retryOptions?: Parameters<typeof fetchWithRetry>[2]
290+
retryOptions?: Parameters<typeof fetchWithRetry>[2],
291+
syncContext?: Record<string, unknown>
291292
): Promise<WikiV2[]> {
292293
const url = `${ADO_BASE_URL}/${encodeURIComponent(organization)}/${encodeURIComponent(project)}/_apis/wiki/wikis?api-version=${WIKIS_LIST_API_VERSION}`
293294
const response = await fetchWithRetry(
@@ -300,6 +301,15 @@ async function listWikis(
300301
)
301302
if (!response.ok) {
302303
if (response.status === 401 || response.status === 403 || response.status === 404) {
304+
/**
305+
* 401/403 mean the wikis still exist but this PAT cannot read them right
306+
* now — flag the listing as incomplete so reconciliation does not delete
307+
* previously synced wiki pages. A 404 means the wiki feature/content is
308+
* genuinely absent, so reconciliation stays enabled.
309+
*/
310+
if ((response.status === 401 || response.status === 403) && syncContext) {
311+
syncContext.listingCapped = true
312+
}
303313
logger.warn('Azure DevOps wikis unavailable; skipping wiki listing', {
304314
organization,
305315
project,
@@ -327,7 +337,7 @@ async function resolveWikis(
327337
): Promise<WikiV2[]> {
328338
const cached = syncContext?.wikis as WikiV2[] | undefined
329339
if (cached) return cached
330-
const wikis = await listWikis(accessToken, organization, project)
340+
const wikis = await listWikis(accessToken, organization, project, undefined, syncContext)
331341
if (syncContext) syncContext.wikis = wikis
332342
return wikis
333343
}
@@ -642,7 +652,8 @@ async function listRepositories(
642652
accessToken: string,
643653
organization: string,
644654
project: string,
645-
retryOptions?: Parameters<typeof fetchWithRetry>[2]
655+
retryOptions?: Parameters<typeof fetchWithRetry>[2],
656+
syncContext?: Record<string, unknown>
646657
): Promise<GitRepository[]> {
647658
const url = `${ADO_BASE_URL}/${encodeURIComponent(organization)}/${encodeURIComponent(project)}/_apis/git/repositories?api-version=${GIT_API_VERSION}`
648659
const response = await fetchWithRetry(
@@ -655,6 +666,15 @@ async function listRepositories(
655666
)
656667
if (!response.ok) {
657668
if (response.status === 401 || response.status === 403 || response.status === 404) {
669+
/**
670+
* 401/403 mean repositories still exist but this PAT cannot read them
671+
* right now — flag the listing as incomplete so reconciliation does not
672+
* delete previously synced repository files. A 404 means the Git feature
673+
* is genuinely absent, so reconciliation stays enabled.
674+
*/
675+
if ((response.status === 401 || response.status === 403) && syncContext) {
676+
syncContext.listingCapped = true
677+
}
658678
logger.warn('Azure DevOps repositories unavailable; skipping file listing', {
659679
organization,
660680
project,
@@ -686,7 +706,8 @@ async function resolveRepositories(
686706
syncContext?: Record<string, unknown>
687707
): Promise<GitRepository[]> {
688708
const cached = syncContext?.repositories as GitRepository[] | undefined
689-
const all = cached ?? (await listRepositories(accessToken, organization, project))
709+
const all =
710+
cached ?? (await listRepositories(accessToken, organization, project, undefined, syncContext))
690711
if (syncContext && !cached) syncContext.repositories = all
691712

692713
const needle = repositoryFilter.toLowerCase()
@@ -707,7 +728,8 @@ async function listRepositoryBlobs(
707728
organization: string,
708729
project: string,
709730
repoId: string,
710-
branch: string
731+
branch: string,
732+
syncContext?: Record<string, unknown>
711733
): Promise<GitItem[]> {
712734
const params = new URLSearchParams({
713735
recursionLevel: 'Full',
@@ -722,6 +744,16 @@ async function listRepositoryBlobs(
722744
})
723745
if (!response.ok) {
724746
if (response.status === 401 || response.status === 403 || response.status === 404) {
747+
/**
748+
* 401/403 mean the repository's files still exist but this PAT cannot
749+
* read them right now — flag the listing as incomplete so reconciliation
750+
* does not delete previously synced files. A 404 means the branch/repo
751+
* content is genuinely absent (empty repo, deleted branch), so
752+
* reconciliation stays enabled.
753+
*/
754+
if ((response.status === 401 || response.status === 403) && syncContext) {
755+
syncContext.listingCapped = true
756+
}
725757
logger.warn('Azure DevOps repository items unavailable; skipping repository', {
726758
repoId,
727759
branch,
@@ -824,7 +856,14 @@ async function resolveRepoFiles(
824856
})
825857
continue
826858
}
827-
const blobs = await listRepositoryBlobs(accessToken, organization, project, repo.id, branch)
859+
const blobs = await listRepositoryBlobs(
860+
accessToken,
861+
organization,
862+
project,
863+
repo.id,
864+
branch,
865+
syncContext
866+
)
828867
for (const item of blobs) {
829868
if (normalizedPrefix && !item.path.startsWith(normalizedPrefix)) continue
830869
if (!matchesExtension(item.path, filters.extensions)) continue

apps/sim/connectors/google-forms/google-forms.ts

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,18 @@ interface FormStubInput {
143143
revisionId?: string
144144
latestResponseTime?: string
145145
contentScope: ContentScope
146+
responseCap: number
147+
}
148+
149+
/**
150+
* Resolves the effective per-form response cap applied when rendering content:
151+
* the user-configured `maxResponsesPerForm` clamped to the hard
152+
* `MAX_RESPONSES_PER_FORM` ceiling. Part of the content hash so changing the
153+
* cap re-syncs every form (the rendered content depends on it).
154+
*/
155+
function resolveResponseCap(sourceConfig: Record<string, unknown>): number {
156+
const configured = parsePositiveInt(sourceConfig.maxResponsesPerForm)
157+
return configured > 0 ? Math.min(configured, MAX_RESPONSES_PER_FORM) : MAX_RESPONSES_PER_FORM
146158
}
147159

148160
/**
@@ -334,7 +346,10 @@ function latestResponseTime(responses: FormResponse[]): string | undefined {
334346
* forces a re-sync of every document.
335347
*/
336348
function formContentHash(input: FormStubInput): string {
337-
const responsePart = input.contentScope === 'both' ? (input.latestResponseTime ?? '') : 'none'
349+
const responsePart =
350+
input.contentScope === 'both'
351+
? `${input.latestResponseTime ?? ''}:${input.responseCap}`
352+
: 'none'
338353
return `gforms:${input.file.id}:${input.contentScope}:${input.revisionId ?? ''}:${responsePart}`
339354
}
340355

@@ -514,6 +529,7 @@ export const googleFormsConnector: ConnectorConfig = {
514529
): Promise<ExternalDocumentList> => {
515530
const maxForms = parsePositiveInt(sourceConfig.maxForms)
516531
const contentScope = resolveContentScope(sourceConfig.contentScope)
532+
const responseCap = resolveResponseCap(sourceConfig)
517533
const previouslyFetched = (syncContext?.totalDocsFetched as number) ?? 0
518534

519535
if (maxForms > 0 && previouslyFetched >= maxForms) {
@@ -586,6 +602,7 @@ export const googleFormsConnector: ConnectorConfig = {
586602
revisionId: form.revisionId,
587603
latestResponseTime: latest,
588604
contentScope,
605+
responseCap,
589606
})
590607
} catch (error) {
591608
skippedOnError = true
@@ -661,16 +678,14 @@ export const googleFormsConnector: ConnectorConfig = {
661678
const form = await fetchFormStructure(accessToken, file.id)
662679
if (!form) return null
663680

664-
const maxResponses = parsePositiveInt(sourceConfig.maxResponsesPerForm)
681+
const responseCap = resolveResponseCap(sourceConfig)
665682
const fetched =
666683
contentScope === 'both'
667684
? await fetchFormResponses(accessToken, file.id)
668685
: { responses: [], latestSubmittedTime: undefined }
669686
const responses = fetched.responses
670687
const cappedResponses =
671-
maxResponses > 0 && responses.length > maxResponses
672-
? responses.slice(0, maxResponses)
673-
: responses
688+
responses.length > responseCap ? responses.slice(0, responseCap) : responses
674689

675690
const content = renderFormDocument(form, cappedResponses)
676691
if (!content.trim()) return null
@@ -681,6 +696,7 @@ export const googleFormsConnector: ConnectorConfig = {
681696
revisionId: form.revisionId,
682697
latestResponseTime: fetched.latestSubmittedTime,
683698
contentScope,
699+
responseCap,
684700
})
685701
return { ...stub, content, contentDeferred: false }
686702
} catch (error) {

apps/sim/connectors/jsm/jsm.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -518,7 +518,9 @@ export const jsmConnector: ConnectorConfig = {
518518
const data = (await response.json()) as JsmPage<JsmRequest>
519519
let requests = data.values ?? []
520520

521+
let slicedSome = false
521522
if (maxRequests > 0 && requests.length > remaining) {
523+
slicedSome = true
522524
requests = requests.slice(0, remaining)
523525
}
524526

@@ -533,8 +535,12 @@ export const jsmConnector: ConnectorConfig = {
533535
* When `maxRequests` truncates the listing before the source is exhausted,
534536
* flag the run as capped so the sync engine skips deletion reconciliation —
535537
* otherwise unseen requests beyond the cap would be deleted on every sync.
538+
* `slicedSome` covers truncation on the final page: requests dropped from
539+
* this page still exist even when `isLastPage` is true. (The requested
540+
* `limit` never exceeds the remaining budget, so a slice should be
541+
* impossible — this is defense in depth against the API over-returning.)
536542
*/
537-
if (reachedCap && !data.isLastPage && syncContext) {
543+
if (((reachedCap && !data.isLastPage) || slicedSome) && syncContext) {
538544
syncContext.listingCapped = true
539545
}
540546

0 commit comments

Comments
 (0)