0

New Row Added

by
Published today

Emits the rows added at the bottom of a worksheet since the last poll, as `{ row, values }`. The first run records the current row count and emits nothing. It compares row counts, so a row inserted in the middle or a deletion paired with an addition is not seen. Polls because Microsoft Graph has no change notification for cell contents, only for the file as a whole. `workbook` is the .xlsx file's id, or its path from the drive root (e.g. `/Reports/budget.xlsx`). Requires Files.ReadWrite (a connection default); a workbook in a drive that is not your own needs Files.ReadWrite.All.

Scriptยท trigger ms_excel Verified

The script

Submitted by hugo989 Typescript (fetch-only)
Verified 5 hours ago
1
//native
2

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

5
const graph = "https://graph.microsoft.com/v1.0"
6

7
// A workbook is addressed by its drive item id, or by a path from the drive root starting with "/"
8
function workbookUrl(workbook: string, drive_id: string | undefined) {
9
  const drive =
10
    drive_id != null && drive_id !== ""
11
      ? `${graph}/drives/${encodeURIComponent(drive_id)}`
12
      : `${graph}/me/drive`
13
  const item = workbook.startsWith("/")
14
    ? `root:${workbook.split("/").map(encodeURIComponent).join("/")}:`
15
    : `items/${encodeURIComponent(workbook)}`
16
  return `${drive}/${item}/workbook`
17
}
18

19
// Requests without a session can read a copy that lags a minute behind the last write;
20
// a new session always opens the current workbook
21
async function workbookSession(auth: RT.MsExcel, wb: string) {
22
  const response = await fetch(`${wb}/createSession`, {
23
    method: "POST",
24
    headers: {
25
      Authorization: `Bearer ${auth.token}`,
26
      "Content-Type": "application/json",
27
      Accept: "application/json",
28
    },
29
    body: JSON.stringify({ persistChanges: true }),
30
  })
31
  if (!response.ok) {
32
    throw new Error(`${response.status} ${await response.text()}`)
33
  }
34
  return ((await response.json()) as { id: string }).id
35
}
36

37
export type DynSelect_workbook = string
38

39
export async function workbook(
40
  auth: RT.MsExcel,
41
  drive_id: string | undefined
42
) {
43
  const drive =
44
    drive_id != null && drive_id !== ""
45
      ? `${graph}/drives/${encodeURIComponent(drive_id)}`
46
      : `${graph}/me/drive`
47
  const response = await fetch(
48
    `${drive}/root/search(q='.xlsx')?$select=id,name&$top=200`,
49
    {
50
      headers: {
51
        Authorization: `Bearer ${auth.token}`,
52
        Accept: "application/json",
53
      },
54
    }
55
  )
56
  if (!response.ok) {
57
    throw new Error(`${response.status} ${await response.text()}`)
58
  }
59
  const { value } = (await response.json()) as {
60
    value: { id: string; name: string }[]
61
  }
62
  return value
63
    .filter((f) => f.name.toLowerCase().endsWith(".xlsx"))
64
    .map((f) => ({ value: f.id, label: f.name }))
65
}
66

67
export type DynSelect_worksheet = string
68

69
export async function worksheet(
70
  auth: RT.MsExcel,
71
  workbook: string,
72
  drive_id: string | undefined
73
) {
74
  const wb = workbookUrl(workbook, drive_id)
75
  const session = await workbookSession(auth, wb)
76
  const response = await fetch(`${wb}/worksheets?$select=name`, {
77
    headers: {
78
      Authorization: `Bearer ${auth.token}`,
79
      Accept: "application/json",
80
      "workbook-session-id": session,
81
    },
82
  })
83
  if (!response.ok) {
84
    throw new Error(`${response.status} ${await response.text()}`)
85
  }
86
  const { value } = (await response.json()) as { value: { name: string }[] }
87
  return value.map((w) => ({ value: w.name, label: w.name }))
88
}
89

90
/**
91
 * New Row Added
92
 * Emits the rows added at the bottom of a worksheet since the last poll, as `{ row, values }`. The first run records the current row count and emits nothing. It compares row counts, so a row inserted in the middle or a deletion paired with an addition is not seen. Polls because Microsoft Graph has no change notification for cell contents, only for the file as a whole. `workbook` is the .xlsx file's id, or its path from the drive root (e.g. `/Reports/budget.xlsx`). Requires Files.ReadWrite (a connection default); a workbook in a drive that is not your own needs Files.ReadWrite.All.
93
 */
94
export async function main(
95
  auth: RT.MsExcel,
96
  workbook: DynSelect_workbook,
97
  worksheet: DynSelect_worksheet,
98
  drive_id: string | undefined
99
) {
100
  const wb = workbookUrl(workbook, drive_id)
101
  const session = await workbookSession(auth, wb)
102
  const previous: number | undefined = await wmill.getState()
103

104
  const response = await fetch(
105
    `${wb}/worksheets/${encodeURIComponent(worksheet)}/usedRange(valuesOnly=true)?$select=address,values`,
106
    {
107
      headers: {
108
        Authorization: `Bearer ${auth.token}`,
109
        Accept: "application/json",
110
        "workbook-session-id": session,
111
      },
112
    }
113
  )
114

115
  if (!response.ok) {
116
    throw new Error(`${response.status} ${await response.text()}`)
117
  }
118

119
  // address is like Sheet1!A1:C5; the last number is the last used row
120
  const used = (await response.json()) as { address: string; values: any[][] }
121
  const isEmpty =
122
    used.values.length === 1 &&
123
    used.values[0].length === 1 &&
124
    used.values[0][0] === ""
125
  const lastRow = isEmpty ? 0 : Number(used.address.match(/(\d+)$/)![1])
126
  await wmill.setState(lastRow)
127

128
  // First run, or rows were removed: only record the count
129
  if (previous == null || lastRow <= previous) return []
130

131
  const firstRow = lastRow - used.values.length + 1
132
  return used.values
133
    .map((values, i) => ({ row: firstRow + i, values }))
134
    .filter((r) => r.row > previous)
135
}
136