fix(smartsheet): read flat response shape for GET sheet, array result for POST row
Deploy to route.crispygoat.com / deploy (push) Successful in 4m11s
Deploy to route.crispygoat.com / deploy (push) Successful in 4m11s
The Smartsheet API uses TWO different response shapes:
- GET /2.0/sheets/{id} → flat: { id, name, columns, ... }
- POST /2.0/sheets/{id}/rows → wrapped: { result: [{id, rowNumber}], resultCode, ... }
Our code assumed both were wrapped in a 'result' object, which meant:
- getSheetMeta read data.result.id → undefined on success.
The defensive guard in 71e1792 caught it and returned 502 'missing
result.id' instead of crashing — but no sheet has ever tested
successfully, so the bug was invisible until a real token was
wired up.
- addRow read data.result.id → undefined, returning rowId='undefined'
and rowNumber=undefined silently. Sync has never worked; any row
insert would have created a row with bogus metadata in the queue.
Fixes:
- getSheetMeta now reads data.id / data.name / data.columns at the
top level. Defensive guard checks for data?.id == null instead
of data?.result?.id == null.
- addRow now reads data.result[0].id / .rowNumber (array shape) and
throws SmartsheetApiError(502) if the array is empty/missing.
Verified end-to-end with the user's token against sheet 782660409446276
(Water Logs, 8 columns: Entry ID, Logged At, Headgate, Measurement, Unit,
Irrigator, Notes). All column titles match our Water Log field names
exactly, so the column-mapping dropdowns will auto-pair cleanly.
This commit is contained in:
+34
-19
@@ -207,7 +207,10 @@ export async function getSheetMeta(
|
|||||||
if (!token) throw new Error("token is required");
|
if (!token) throw new Error("token is required");
|
||||||
|
|
||||||
type ApiResponse = {
|
type ApiResponse = {
|
||||||
result: {
|
// NOTE: GET /2.0/sheets/{id} returns a FLAT response (id, name,
|
||||||
|
// columns at the top level), NOT wrapped in a `result` envelope.
|
||||||
|
// This is different from POST endpoints (which use { result: [...] }).
|
||||||
|
// See https://smartsheet.redoc.ly/#operation/getSheet
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
columns: Array<{
|
columns: Array<{
|
||||||
@@ -217,7 +220,6 @@ export async function getSheetMeta(
|
|||||||
index: number;
|
index: number;
|
||||||
}>;
|
}>;
|
||||||
};
|
};
|
||||||
};
|
|
||||||
|
|
||||||
const data = await request<ApiResponse>(
|
const data = await request<ApiResponse>(
|
||||||
"GET",
|
"GET",
|
||||||
@@ -225,27 +227,26 @@ export async function getSheetMeta(
|
|||||||
token,
|
token,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Defensive: Smartsheet occasionally returns a 2xx with a body that
|
// Defensive: if Smartsheet returns a 2xx without `id`, something is
|
||||||
// lacks the `result` envelope (e.g. when a non-numeric / share-slug
|
// wrong (older API version, account-level issue, etc.). Without this
|
||||||
// sheet ID is passed — the API doesn't reject it cleanly). Without
|
// guard we'd crash with "Cannot read properties of undefined (reading
|
||||||
// this guard we'd crash with "Cannot read properties of undefined
|
// 'id')" deep in the column-mapping render. `extractSheetId` catches
|
||||||
// (reading 'id')" deep in the column-mapping render. `extractSheetId`
|
// share-link slugs up front; this guard is belt-and-suspenders for
|
||||||
// catches the common share-slug case up front; this guard is the
|
// anything else.
|
||||||
// belt-and-suspenders for anything else.
|
if (!data || data.id == null) {
|
||||||
if (!data?.result || data.result.id == null) {
|
|
||||||
throw new SmartsheetApiError(
|
throw new SmartsheetApiError(
|
||||||
502,
|
502,
|
||||||
`Smartsheet returned an unexpected response for sheet ${sheetId} ` +
|
`Smartsheet returned an unexpected response for sheet ${sheetId} ` +
|
||||||
`(missing result.id). If you pasted a Smartsheet share-link URL, ` +
|
`(missing id at top level). If you pasted a Smartsheet share-link ` +
|
||||||
`paste the numeric sheet ID instead — open the sheet in Smartsheet ` +
|
`URL, paste the numeric sheet ID instead — open the sheet in ` +
|
||||||
`while signed in and copy the number from the address bar.`,
|
`Smartsheet while signed in and copy the number from the address bar.`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: String(data.result.id),
|
id: String(data.id),
|
||||||
name: data.result.name ?? "(unnamed sheet)",
|
name: data.name ?? "(unnamed sheet)",
|
||||||
columns: (data.result.columns ?? []).map((c) => ({
|
columns: (data.columns ?? []).map((c) => ({
|
||||||
id: c.id != null ? String(c.id) : "",
|
id: c.id != null ? String(c.id) : "",
|
||||||
title: c.title ?? "",
|
title: c.title ?? "",
|
||||||
type: c.type ?? "TEXT_NUMBER",
|
type: c.type ?? "TEXT_NUMBER",
|
||||||
@@ -278,8 +279,14 @@ export async function addRow(
|
|||||||
return v;
|
return v;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// NOTE: POST /2.0/sheets/{id}/rows wraps the created row(s) in
|
||||||
|
// `{ result: [{id, rowNumber}, ...] }` — different shape from the
|
||||||
|
// flat GET /2.0/sheets/{id} response. See
|
||||||
|
// https://smartsheet.redoc.ly/#operation/addRows
|
||||||
type ApiResponse = {
|
type ApiResponse = {
|
||||||
result: { id: number; rowNumber: number };
|
result: Array<{ id: number; rowNumber: number }>;
|
||||||
|
resultCode?: number;
|
||||||
|
message?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const data = await request<ApiResponse>(
|
const data = await request<ApiResponse>(
|
||||||
@@ -298,9 +305,17 @@ export async function addRow(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const row = data?.result?.[0];
|
||||||
|
if (!row || row.id == null) {
|
||||||
|
throw new SmartsheetApiError(
|
||||||
|
502,
|
||||||
|
`Smartsheet POST /sheets/${sheetId}/rows returned no row. ` +
|
||||||
|
`Response: ${JSON.stringify(data).slice(0, 200)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
rowId: String(data.result.id),
|
rowId: String(row.id),
|
||||||
rowNumber: data.result.rowNumber,
|
rowNumber: row.rowNumber,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user