0

New Row

by
Published today

Trigger on rows appended to a worksheet. Polls, because Google has no row-level push for Sheets.

Scriptยท trigger gsheets Verified

The script

Submitted by hugo989 Typescript (fetch-only)
Verified 47 minutes ago
1
//native
2

3
import * as wmill from "windmill-client"
4

5
export type DynSelect_sheet_name = string
6

7
// Dropdown of the spreadsheet's worksheet (tab) names.
8
export async function sheet_name(auth: RT.Gsheets, spreadsheet_id: string) {
9
  const response = await fetch(
10
    `https://sheets.googleapis.com/v4/spreadsheets/${spreadsheet_id}?fields=sheets.properties.title`,
11
    {
12
      headers: {
13
        Authorization: `Bearer ${auth.token}`,
14
        Accept: "application/json",
15
      },
16
    }
17
  )
18
  if (!response.ok) {
19
    throw new Error(`${response.status} ${await response.text()}`)
20
  }
21
  const { sheets } = (await response.json()) as {
22
    sheets: { properties: { title: string } }[]
23
  }
24
  return sheets.map((s) => ({
25
    value: s.properties.title,
26
    label: s.properties.title,
27
  }))
28
}
29

30
/**
31
 * New Row
32
 * Polls for rows appended below the last row seen on the previous poll (polling because Google has no row-level push: Drive's push notification for a spreadsheet only says the file changed). Emits each new row with its 1-based `row_number` and values keyed by the header row (row 1). The first run records the current row count and emits nothing.
33
 */
34
export async function main(
35
  auth: RT.Gsheets,
36
  spreadsheet_id: string,
37
  sheet_name: DynSelect_sheet_name
38
) {
39
  // ponytail: reads the whole sheet each poll; fine up to tens of thousands of rows.
40
  const range = encodeURIComponent(`'${sheet_name.replaceAll("'", "''")}'`)
41
  const response = await fetch(
42
    `https://sheets.googleapis.com/v4/spreadsheets/${spreadsheet_id}/values/${range}`,
43
    {
44
      headers: {
45
        Authorization: `Bearer ${auth.token}`,
46
        Accept: "application/json",
47
      },
48
    }
49
  )
50

51
  if (!response.ok) {
52
    throw new Error(`${response.status} ${await response.text()}`)
53
  }
54

55
  // `values` stops at the last non-empty row, so its length is the row count.
56
  const { values = [] } = (await response.json()) as { values?: string[][] }
57
  const seen: number | undefined = await wmill.getState()
58
  await wmill.setState(values.length)
59

60
  // First run, or rows were deleted: re-baseline without emitting.
61
  if (seen === undefined || seen === null || values.length <= seen) return []
62

63
  const headers = values[0] ?? []
64
  return values.slice(Math.max(seen, 1)).map((row, i) => ({
65
    row_number: Math.max(seen, 1) + i + 1,
66
    values: Object.fromEntries(headers.map((h, j) => [h, row[j] ?? ""])),
67
  }))
68
}
69