Skip to content

Commit bbf66fe

Browse files
committed
fix(microsoft): reserve ADX table envelopes up front, log skipped Outlook attachments, tighten SharePoint upload path
1 parent 7ef695e commit bbf66fe

8 files changed

Lines changed: 64 additions & 23 deletions

File tree

‎apps/sim/blocks/blocks/azure_devops.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,7 @@ export const AzureDevOpsBlock: BlockConfig = {
241241
{ label: 'Cancelling', id: 'cancelling' },
242242
{ label: 'Postponed', id: 'postponed' },
243243
{ label: 'Not Started', id: 'notStarted' },
244+
{ label: 'No Status', id: 'none' },
244245
],
245246
condition: { field: 'operation', value: 'azure_devops_list_builds' },
246247
mode: 'advanced',

‎apps/sim/lib/internal/azure-data-explorer/client.ts‎

Lines changed: 35 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -233,29 +233,40 @@ interface ProjectionBudget {
233233
bytes: number
234234
}
235235

236+
function projectColumns(table: KustoTable): AzureDataExplorerResultTable['columns'] {
237+
return (table.Columns ?? []).map((column) => ({
238+
name: column.ColumnName ?? '',
239+
type: column.ColumnType ?? null,
240+
dataType: column.DataType ?? null,
241+
}))
242+
}
243+
244+
/** Serialized size of a table's projection without its rows, reserved before any rows are kept. */
245+
function envelopeBytes(table: KustoTable | null): number {
246+
if (!table) return Buffer.byteLength(JSON.stringify(EMPTY_PROJECTION))
247+
const rowTotal = (table.Rows ?? []).length
248+
return Buffer.byteLength(
249+
JSON.stringify({
250+
tableName: table.TableName ?? null,
251+
columns: projectColumns(table),
252+
rows: [],
253+
records: [],
254+
rowCount: rowTotal,
255+
totalRowCount: rowTotal,
256+
truncated: false,
257+
})
258+
)
259+
}
260+
236261
function projectTable(
237262
table: KustoTable | null,
238263
budget: ProjectionBudget
239264
): AzureDataExplorerResultTable {
240265
if (!table) return EMPTY_PROJECTION
241-
const columns = (table.Columns ?? []).map((column) => ({
242-
name: column.ColumnName ?? '',
243-
type: column.ColumnType ?? null,
244-
dataType: column.DataType ?? null,
245-
}))
266+
const columns = projectColumns(table)
246267
const allRows = table.Rows ?? []
247268
const rows: unknown[][] = []
248269
const records: Array<Record<string, unknown>> = []
249-
const envelope: AzureDataExplorerResultTable = {
250-
tableName: table.TableName ?? null,
251-
columns,
252-
rows: [],
253-
records: [],
254-
rowCount: allRows.length,
255-
totalRowCount: allRows.length,
256-
truncated: false,
257-
}
258-
budget.bytes -= Buffer.byteLength(JSON.stringify(envelope))
259270
for (const row of allRows) {
260271
if (budget.rows <= 0) break
261272
const record: Record<string, unknown> = {}
@@ -288,9 +299,16 @@ function projectResults(
288299
tables: KustoTable[],
289300
contents: TableOfContents | null
290301
): AzureDataExplorerTable {
291-
const budget: ProjectionBudget = { rows: MAX_PROJECTED_ROWS, bytes: MAX_PROJECTED_BYTES }
292-
const primary = projectTable(selectPrimaryTable(tables, contents), budget)
302+
const primaryTable = selectPrimaryTable(tables, contents)
293303
const additionalOrdinals = contents?.resultOrdinals.slice(1) ?? []
304+
const reserved = [primaryTable, ...additionalOrdinals.map((ordinal) => tables[ordinal] ?? null)]
305+
.map(envelopeBytes)
306+
.reduce((total, bytes) => total + bytes, 0)
307+
const budget: ProjectionBudget = {
308+
rows: MAX_PROJECTED_ROWS,
309+
bytes: MAX_PROJECTED_BYTES - reserved,
310+
}
311+
const primary = projectTable(primaryTable, budget)
294312
if (additionalOrdinals.length === 0) return primary
295313
return {
296314
...primary,

‎apps/sim/lib/internal/sharepoint/operations.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,9 @@ function buildUploadUrl(
9393
const normalizedPath = folderPath.startsWith('/') ? folderPath : `/${folderPath}`
9494
const cleanPath = normalizedPath.endsWith('/') ? normalizedPath.slice(0, -1) : normalizedPath
9595
const uploadPath = folderPath ? `${cleanPath}/${fileName}` : `/${fileName}`
96+
if (uploadPath.split('/').some((segment) => segment.trim() === '.' || segment.trim() === '..')) {
97+
throw new Error('Upload path cannot contain "." or ".." segments')
98+
}
9699
const encodedPath = uploadPath
97100
.split('/')
98101
.map((segment) => (segment ? encodeURIComponent(segment) : ''))

‎apps/sim/lib/webhooks/polling/outlook.ts‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -423,6 +423,12 @@ async function processOutlookEmails(
423423
fetchImpl: (url, init) => fetchWithRetry(url, init),
424424
keepPartialOnLimit: true,
425425
preferMetadataSize: true,
426+
onSkip: (attachmentName, declaredSize) =>
427+
logger.warn('Skipping Outlook attachment over the per-email byte budget', {
428+
emailId: email.id,
429+
attachmentName,
430+
declaredSize,
431+
}),
426432
onFailure: (stage, status) =>
427433
logger.warn('Outlook attachment request failed', {
428434
emailId: email.id,

‎apps/sim/tools/azure_devops/utils.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@ function pathSegment(value: string, paramName: string): string {
3131
export const WORK_ITEMS_BATCH_SIZE = 200
3232

3333
/**
34-
* Fetches one Work Items - List chunk with `$expand=all`, bounded to the inline
35-
* tool response size and cancelled with the tool call.
34+
* Fetches one Work Items - List chunk with `$expand=all`, bounded to the buffered
35+
* transfer cap (100 MB) and cancelled with the tool call.
3636
*/
3737
export async function fetchWorkItemsChunk(
3838
params: { organization: string; project: string; accessToken: string },

‎apps/sim/tools/generated/tool-outputs.ts‎

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

‎apps/sim/tools/onedrive/utils.ts‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,27 +96,37 @@ export const ONEDRIVE_FILE_OUTPUT_PROPERTIES: Record<string, OutputProperty> = {
9696
type: 'number',
9797
description: 'Number of direct children (folders only)',
9898
optional: true,
99+
nullable: true,
99100
},
100101
description: {
101102
type: 'string',
102103
description: 'User-visible description (OneDrive Personal only)',
103104
optional: true,
105+
nullable: true,
106+
},
107+
eTag: {
108+
type: 'string',
109+
description: 'eTag for the item (metadata and content)',
110+
optional: true,
111+
nullable: true,
104112
},
105-
eTag: { type: 'string', description: 'eTag for the item (metadata and content)', optional: true },
106113
parentReference: {
107114
type: 'object',
108115
description: 'Parent location (id, driveId, driveType, name, path, siteId)',
109116
optional: true,
117+
nullable: true,
110118
},
111119
createdBy: {
112120
type: 'object',
113121
description: 'Identity that created the item (id, displayName)',
114122
optional: true,
123+
nullable: true,
115124
},
116125
lastModifiedBy: {
117126
type: 'object',
118127
description: 'Identity that last modified the item (id, displayName)',
119128
optional: true,
129+
nullable: true,
120130
},
121131
}
122132

‎apps/sim/tools/outlook/read.ts‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ export interface OutlookAttachmentDownloadOptions {
3434
preferMetadataSize?: boolean
3535
/** Called when Graph answers a metadata or content request with a non-OK status. */
3636
onFailure?: (stage: 'metadata' | 'content', status: number) => void
37+
/** Called for each attachment skipped because it exceeds the byte budget (with keepPartialOnLimit). */
38+
onSkip?: (attachmentName: string, declaredSize: number | undefined) => void
3739
}
3840

3941
/** Fetch metadata separately from raw file bytes, sharing the budget across messages. */
@@ -46,9 +48,10 @@ export async function downloadAttachments(
4648
const fetchImpl = options.fetchImpl ?? ((url: string, init: RequestInit) => fetch(url, init))
4749
const attachments: OutlookAttachment[] = []
4850
const path = `https://graph.microsoft.com/v1.0/me/messages/${encodeURIComponent(messageId)}/attachments`
49-
const handleError = (error: unknown) => {
51+
const handleError = (error: unknown, attachment?: OutlookDownloadAttachmentMetadata) => {
5052
if (options.keepPartialOnLimit && isPayloadSizeLimitError(error)) {
5153
budget.signal?.throwIfAborted()
54+
options.onSkip?.(attachment?.name || 'attachment', attachment?.size)
5255
return
5356
}
5457
rethrowAttachmentDownloadError(error, budget.signal)
@@ -98,7 +101,7 @@ export async function downloadAttachments(
98101
: buffer.byteLength,
99102
})
100103
} catch (error) {
101-
handleError(error)
104+
handleError(error, attachment)
102105
}
103106
}
104107
} catch (error) {

0 commit comments

Comments
 (0)