1 | |
2 |
|
3 | export type DynSelect_sheet_name = string |
4 |
|
5 | |
6 | export async function sheet_name(auth: RT.Gsheets, spreadsheet_id: string) { |
7 | const response = await fetch( |
8 | `https://sheets.googleapis.com/v4/spreadsheets/${spreadsheet_id}?fields=sheets.properties.title`, |
9 | { |
10 | headers: { |
11 | Authorization: `Bearer ${auth.token}`, |
12 | Accept: "application/json", |
13 | }, |
14 | } |
15 | ) |
16 | if (!response.ok) { |
17 | throw new Error(`${response.status} ${await response.text()}`) |
18 | } |
19 | const { sheets } = (await response.json()) as { |
20 | sheets: { properties: { title: string } }[] |
21 | } |
22 | return sheets.map((s) => ({ |
23 | value: s.properties.title, |
24 | label: s.properties.title, |
25 | })) |
26 | } |
27 |
|
28 | |
29 | * Upsert Row |
30 | * Update the first row whose `key_column` (a header name or column letter) equals the key, or append `values` as a new row when none does. The key defaults to the value of `values` in the key column; matching is exact on the displayed value. Values are parsed as if typed in the UI. Returns `action` (updated/appended) and `row_number`. |
31 | */ |
32 | export async function main( |
33 | auth: RT.Gsheets, |
34 | spreadsheet_id: string, |
35 | sheet_name: DynSelect_sheet_name, |
36 | key_column: string, |
37 | values: any[], |
38 | key_value: string | undefined |
39 | ) { |
40 | const headers = { |
41 | Authorization: `Bearer ${auth.token}`, |
42 | Accept: "application/json", |
43 | } |
44 | const base = `https://sheets.googleapis.com/v4/spreadsheets/${spreadsheet_id}/values` |
45 | const tab = `'${sheet_name.replaceAll("'", "''")}'` |
46 |
|
47 | |
48 | const read = await fetch(`${base}/${encodeURIComponent(tab)}`, { headers }) |
49 | if (!read.ok) { |
50 | throw new Error(`${read.status} ${await read.text()}`) |
51 | } |
52 | const { values: rows = [] } = (await read.json()) as { values?: string[][] } |
53 | const header = rows[0] ?? [] |
54 |
|
55 | |
56 | let index = header.indexOf(key_column) |
57 | if (index === -1 && /^[A-Za-z]{1,3}$/.test(key_column)) { |
58 | index = |
59 | [...key_column.toUpperCase()].reduce( |
60 | (n, c) => n * 26 + c.charCodeAt(0) - 64, |
61 | 0 |
62 | ) - 1 |
63 | } |
64 | if (index === -1) { |
65 | throw new Error( |
66 | `Column "${key_column}" not found in header row: ${header.join(", ")}` |
67 | ) |
68 | } |
69 | const key = key_value ?? String(values[index] ?? "") |
70 | const match = rows.findIndex((row, i) => i > 0 && (row[index] ?? "") === key) |
71 |
|
72 | if (match !== -1) { |
73 | const url = new URL(`${base}/${encodeURIComponent(`${tab}!A${match + 1}`)}`) |
74 | url.searchParams.append("valueInputOption", "USER_ENTERED") |
75 | const response = await fetch(url, { |
76 | method: "PUT", |
77 | headers: { ...headers, "Content-Type": "application/json" }, |
78 | body: JSON.stringify({ values: [values] }), |
79 | }) |
80 | if (!response.ok) { |
81 | throw new Error(`${response.status} ${await response.text()}`) |
82 | } |
83 | return { |
84 | action: "updated", |
85 | row_number: match + 1, |
86 | ...(await response.json()), |
87 | } |
88 | } |
89 |
|
90 | const url = new URL(`${base}/${encodeURIComponent(tab)}:append`) |
91 | url.searchParams.append("valueInputOption", "USER_ENTERED") |
92 | url.searchParams.append("insertDataOption", "INSERT_ROWS") |
93 | const response = await fetch(url, { |
94 | method: "POST", |
95 | headers: { ...headers, "Content-Type": "application/json" }, |
96 | body: JSON.stringify({ values: [values] }), |
97 | }) |
98 | if (!response.ok) { |
99 | throw new Error(`${response.status} ${await response.text()}`) |
100 | } |
101 | const result = await response.json() |
102 | return { |
103 | action: "appended", |
104 | row_number: Number(result.updates?.updatedRange?.match(/!\D+(\d+)/)?.[1]), |
105 | ...result, |
106 | } |
107 | } |
108 |
|