Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions src/app/(private)/hooks/useDataSourceListCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,6 @@ export function useDataSourceListCache() {
...ds,
...updater({
...ds,
organisationName: "",
organisationOverride: null,
}),
}
Expand All @@ -77,7 +76,6 @@ export function useDataSourceListCache() {
...old,
...updater({
...old,
organisationName: "",
organisationOverride: null,
}),
};
Expand Down
32 changes: 27 additions & 5 deletions src/server/adaptors/googlesheets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -339,8 +339,9 @@ export class GoogleSheetsAdaptor implements DataSourceAdaptor {
}

/**
* Checks if the webhook sheet contains formula errors.
* Returns true if #ERROR! or other error values are detected.
* Checks if the webhook sheet contains formula errors or is missing rows.
* Returns true if #ERROR! or other error values are detected, or if the
* webhook sheet has fewer rows than the main sheet (excluding the header row).
*/
async hasWebhookErrors(): Promise<boolean> {
try {
Expand All @@ -350,9 +351,19 @@ export class GoogleSheetsAdaptor implements DataSourceAdaptor {
const notificationDomain = new URL(notificationUrl).hostname;
const webhookSheetName = `Mapped Webhook: ${notificationDomain}/${this.dataSourceId}`;

// Read first few cells from webhook sheet to check for errors
const url = `https://sheets.googleapis.com/v4/spreadsheets/${this.spreadsheetId}/values/${encodeURIComponent(webhookSheetName)}!A1:B5`;
const response = await this.makeGoogleSheetsRequest(url);
// Get main sheet row count (column A, same as prepareWebhookSheet)
const mainSheetUrl = `https://sheets.googleapis.com/v4/spreadsheets/${this.spreadsheetId}/values/${encodeURIComponent(`${this.sheetName}!A:A`)}`;
const mainSheetResponse =
await this.makeGoogleSheetsRequest(mainSheetUrl);
const mainSheetData = mainSheetResponse.ok
? ((await mainSheetResponse.json()) as { values: string[][] })
: { values: [] };
Comment on lines +358 to +360
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mainSheetResponse.ok failures are silently treated as an empty sheet ({ values: [] }), which can mask permission/auth issues and make the “missing rows” check unreliable. Align this with prepareWebhookSheet by handling non-OK responses explicitly (e.g., read the body and throw/log + return) so refreshWebhooks can surface and act on real errors.

Suggested change
const mainSheetData = mainSheetResponse.ok
? ((await mainSheetResponse.json()) as { values: string[][] })
: { values: [] };
if (!mainSheetResponse.ok) {
const responseBody = await mainSheetResponse.text();
logger.warn(
`Could not read main sheet for webhook error check for ${this.dataSourceId}`,
{
status: mainSheetResponse.status,
responseBody,
},
);
throw new Error(
`Failed to read main sheet: ${mainSheetResponse.status}`,
);
}
const mainSheetData =
(await mainSheetResponse.json()) as { values: string[][] };

Copilot uses AI. Check for mistakes.
// Subtract 1 for the header row
const mainRowCount = Math.max(0, (mainSheetData.values?.length || 0) - 1);

// Read all rows from webhook sheet column A to check for errors and count rows
const webhookUrl = `https://sheets.googleapis.com/v4/spreadsheets/${this.spreadsheetId}/values/${encodeURIComponent(webhookSheetName)}!A:B`;
const response = await this.makeGoogleSheetsRequest(webhookUrl);
Comment thread
joaquimds marked this conversation as resolved.
Comment thread
joaquimds marked this conversation as resolved.

if (!response.ok) {
return false; // Sheet doesn't exist or can't be read
Expand All @@ -376,6 +387,17 @@ export class GoogleSheetsAdaptor implements DataSourceAdaptor {
}
}

// Check if webhook sheet is missing rows relative to the main sheet
// Row 1 in the webhook sheet contains the row-count formula, so exclude it
// to compare webhook data rows against main-sheet data rows.
const webhookDataRowCount = Math.max(0, rows.length - 1);
if (webhookDataRowCount < mainRowCount) {
logger.warn(
`Webhook sheet for ${this.dataSourceId} has ${webhookDataRowCount} data rows but main sheet has ${mainRowCount} data rows`,
);
return true;
}

return false;
} catch (error) {
logger.warn(`Could not check webhook errors for ${this.dataSourceId}`, {
Expand Down
45 changes: 44 additions & 1 deletion src/server/jobs/refreshWebhooks.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { DataSourceType, airtableConfigSchema } from "@/models/DataSource";
import {
DataSourceType,
airtableConfigSchema,
googleSheetsConfigSchema,
} from "@/models/DataSource";
import { AirtableAdaptor } from "@/server/adaptors/airtable";
import { GoogleSheetsAdaptor } from "@/server/adaptors/googlesheets";
import {
findDataSourceById,
findDataSourcesByType,
Expand Down Expand Up @@ -42,6 +47,44 @@ const refreshWebhooks = async (args: object | null): Promise<boolean> => {
continue;
}
}

const googleSheetsDataSources = await findDataSourcesByType(
DataSourceType.GoogleSheets,
);
for (const source of googleSheetsDataSources) {
const result = googleSheetsConfigSchema.safeParse(source.config);
if (!result.success) {
logger.warn(
`Failed to parse Google Sheets config for data source ${source.id}`,
{ error: result.error },
);
continue;
}
const config = result.data;
const adaptor = new GoogleSheetsAdaptor(
source.id,
config.spreadsheetId,
config.sheetName,
config.oAuthCredentials,
);
const enable = source.autoEnrich || source.autoImport;
try {
await adaptor.toggleWebhook(enable);
if (enable) {
const hasErrors = await adaptor.hasWebhookErrors();
if (hasErrors) {
await adaptor.repairWebhook();
}
}
} catch (error) {
logger.warn(
`Failed to refresh Google Sheets webhook for data source ${source.id}`,
{ error },
);
continue;
}
}

return true;
};

Expand Down
Loading