fix(smartsheet): detect share-link slugs and guard malformed API responses
Deploy to route.crispygoat.com / deploy (push) Successful in 3m55s
Deploy to route.crispygoat.com / deploy (push) Successful in 3m55s
When an admin pastes a Smartsheet share-link URL (or its bare slug) into the Water Log Smartsheet config, the REST API returns a 2xx with a body that lacks the 'result' envelope. getSheetMeta then crashed with: Cannot read properties of undefined (reading 'id') The error bubbled up through testSmartsheetConnection and surfaced in the Test Connection UI as a generic TypeError. Two-part fix in src/lib/smartsheet.ts: 1. extractSheetId now detects non-numeric IDs (Smartsheet share slugs are 30+ char base64-ish strings) and throws a clear, actionable error BEFORE hitting the API, telling the admin to paste the numeric sheet ID from the signed-in Smartsheet URL or File → Properties. 2. getSheetMeta has a defensive guard (if (!data?.result || data.result.id == null)) that throws a SmartsheetApiError(502, ...) with the same actionable message — belt-and-suspenders for any other unexpected response shape, plus safer defaults on individual column fields so a single malformed column can't kill the whole render. User's failing input was: https://app.smartsheet.com/sheets/V4XgwvVJqFjcxQmQMrgwm77WjCP7hpjqqW82RRR1 Their real numeric sheet ID is 782660409446276 (now accepted).
This commit is contained in:
+59
-23
@@ -137,39 +137,58 @@ async function request<T>(
|
|||||||
* - A bare ID: "123456789012345"
|
* - A bare ID: "123456789012345"
|
||||||
* - A URL: "https://app.smartsheet.com/sheets/abc123?..."
|
* - A URL: "https://app.smartsheet.com/sheets/abc123?..."
|
||||||
*
|
*
|
||||||
* Smartsheet share URLs include an opaque slug (e.g. "abc123"). We
|
* Smartsheet has TWO kinds of sheet URLs:
|
||||||
* can't resolve that slug without an API call (the sheet meta call
|
* - `https://app.smartsheet.com/sheets/<numeric-id>` — the real ID.
|
||||||
* will fail with 404 anyway), so we just take the last path segment
|
* Works against the REST API.
|
||||||
* and pass it as the sheet ID. If the user pasted a slug URL, the
|
* - `https://app.smartsheet.com/sheets/<share-slug>` — a 30+ char
|
||||||
* "Test Connection" UI will surface the 404 and prompt for a numeric
|
* opaque slug for sharing with non-users. The REST API does NOT
|
||||||
* ID instead. (This matches what most Smartsheet integrations do.)
|
* accept these; calling `/2.0/sheets/<slug>` returns a 2xx with
|
||||||
|
* an empty/malformed body, which used to crash our parser with
|
||||||
|
* "Cannot read properties of undefined (reading 'id')". We detect
|
||||||
|
* that case here and surface a clear, actionable error so the
|
||||||
|
* admin pastes the numeric ID instead.
|
||||||
|
*
|
||||||
|
* The numeric ID can be found in the sheet's URL once the user is
|
||||||
|
* signed in to Smartsheet, or via File → Properties in the Smartsheet
|
||||||
|
* UI.
|
||||||
*/
|
*/
|
||||||
export function extractSheetId(input: string): string {
|
export function extractSheetId(input: string): string {
|
||||||
const trimmed = input.trim();
|
const trimmed = input.trim();
|
||||||
if (!trimmed) {
|
if (!trimmed) {
|
||||||
throw new Error("Sheet ID or URL is required");
|
throw new Error("Sheet ID or URL is required");
|
||||||
}
|
}
|
||||||
// Plain numeric ID
|
// Plain numeric ID — short-circuit.
|
||||||
if (/^\d+$/.test(trimmed)) return trimmed;
|
if (/^\d+$/.test(trimmed)) return trimmed;
|
||||||
|
|
||||||
// URL form
|
// URL form — pull the segment after "sheets", or fall back to the
|
||||||
|
// last path segment.
|
||||||
|
let candidate = trimmed;
|
||||||
try {
|
try {
|
||||||
const url = new URL(trimmed);
|
const url = new URL(trimmed);
|
||||||
const segments = url.pathname.split("/").filter(Boolean);
|
const segments = url.pathname.split("/").filter(Boolean);
|
||||||
// Find the segment after "sheets"
|
|
||||||
const sheetsIdx = segments.findIndex((s) => s === "sheets");
|
const sheetsIdx = segments.findIndex((s) => s === "sheets");
|
||||||
if (sheetsIdx >= 0 && segments[sheetsIdx + 1]) {
|
if (sheetsIdx >= 0 && segments[sheetsIdx + 1]) {
|
||||||
return segments[sheetsIdx + 1];
|
candidate = segments[sheetsIdx + 1];
|
||||||
}
|
} else if (segments.length > 0) {
|
||||||
// Last segment as a fallback
|
candidate = segments[segments.length - 1] ?? trimmed;
|
||||||
if (segments.length > 0) {
|
|
||||||
return segments[segments.length - 1];
|
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// Not a URL — fall through
|
// Not a URL — leave `candidate` as the original input.
|
||||||
}
|
}
|
||||||
// Treat as opaque ID; API will surface 404 if it's not numeric.
|
|
||||||
return trimmed;
|
// The Smartsheet REST API only accepts numeric IDs. Share-link slugs
|
||||||
|
// look like "V4XgwvVJqFjcxQmQMrgwm77WjCP7hpjqqW82RRR1" (30+ chars,
|
||||||
|
// base64-ish). Detect and reject them with a clear message BEFORE
|
||||||
|
// hitting the API.
|
||||||
|
if (!/^\d+$/.test(candidate)) {
|
||||||
|
throw new Error(
|
||||||
|
"That looks like a Smartsheet share-link slug, not a numeric sheet ID. " +
|
||||||
|
"Open the sheet in Smartsheet (signed in) and copy the numeric ID from the URL — " +
|
||||||
|
"or use File → Properties to find it. " +
|
||||||
|
`Got: "${candidate.slice(0, 12)}${candidate.length > 12 ? "…" : ""}"`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return candidate;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -206,14 +225,31 @@ export async function getSheetMeta(
|
|||||||
token,
|
token,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Defensive: Smartsheet occasionally returns a 2xx with a body that
|
||||||
|
// lacks the `result` envelope (e.g. when a non-numeric / share-slug
|
||||||
|
// sheet ID is passed — the API doesn't reject it cleanly). Without
|
||||||
|
// this guard we'd crash with "Cannot read properties of undefined
|
||||||
|
// (reading 'id')" deep in the column-mapping render. `extractSheetId`
|
||||||
|
// catches the common share-slug case up front; this guard is the
|
||||||
|
// belt-and-suspenders for anything else.
|
||||||
|
if (!data?.result || data.result.id == null) {
|
||||||
|
throw new SmartsheetApiError(
|
||||||
|
502,
|
||||||
|
`Smartsheet returned an unexpected response for sheet ${sheetId} ` +
|
||||||
|
`(missing result.id). If you pasted a Smartsheet share-link URL, ` +
|
||||||
|
`paste the numeric sheet ID instead — open the sheet in Smartsheet ` +
|
||||||
|
`while signed in and copy the number from the address bar.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: String(data.result.id),
|
id: String(data.result.id),
|
||||||
name: data.result.name,
|
name: data.result.name ?? "(unnamed sheet)",
|
||||||
columns: data.result.columns.map((c) => ({
|
columns: (data.result.columns ?? []).map((c) => ({
|
||||||
id: String(c.id),
|
id: c.id != null ? String(c.id) : "",
|
||||||
title: c.title,
|
title: c.title ?? "",
|
||||||
type: c.type,
|
type: c.type ?? "TEXT_NUMBER",
|
||||||
index: c.index,
|
index: c.index ?? 0,
|
||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user