1 | |
2 |
|
3 | import * as wmill from "windmill-client" |
4 |
|
5 | const graph = "https://graph.microsoft.com/v1.0" |
6 |
|
7 | |
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 | |
20 | |
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 | |
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 | |
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 |
|