1 | |
2 |
|
3 | const graph = "https://graph.microsoft.com/v1.0" |
4 |
|
5 | |
6 | function workbookUrl(workbook: string, drive_id: string | undefined) { |
7 | const drive = |
8 | drive_id != null && drive_id !== "" |
9 | ? `${graph}/drives/${encodeURIComponent(drive_id)}` |
10 | : `${graph}/me/drive` |
11 | const item = workbook.startsWith("/") |
12 | ? `root:${workbook.split("/").map(encodeURIComponent).join("/")}:` |
13 | : `items/${encodeURIComponent(workbook)}` |
14 | return `${drive}/${item}/workbook` |
15 | } |
16 |
|
17 | |
18 | |
19 | async function workbookSession(auth: RT.MsExcel, wb: string) { |
20 | const response = await fetch(`${wb}/createSession`, { |
21 | method: "POST", |
22 | headers: { |
23 | Authorization: `Bearer ${auth.token}`, |
24 | "Content-Type": "application/json", |
25 | Accept: "application/json", |
26 | }, |
27 | body: JSON.stringify({ persistChanges: true }), |
28 | }) |
29 | if (!response.ok) { |
30 | throw new Error(`${response.status} ${await response.text()}`) |
31 | } |
32 | return ((await response.json()) as { id: string }).id |
33 | } |
34 |
|
35 | export type DynSelect_workbook = string |
36 |
|
37 | export async function workbook( |
38 | auth: RT.MsExcel, |
39 | drive_id: string | undefined |
40 | ) { |
41 | const drive = |
42 | drive_id != null && drive_id !== "" |
43 | ? `${graph}/drives/${encodeURIComponent(drive_id)}` |
44 | : `${graph}/me/drive` |
45 | const response = await fetch( |
46 | `${drive}/root/search(q='.xlsx')?$select=id,name&$top=200`, |
47 | { |
48 | headers: { |
49 | Authorization: `Bearer ${auth.token}`, |
50 | Accept: "application/json", |
51 | }, |
52 | } |
53 | ) |
54 | if (!response.ok) { |
55 | throw new Error(`${response.status} ${await response.text()}`) |
56 | } |
57 | const { value } = (await response.json()) as { |
58 | value: { id: string; name: string }[] |
59 | } |
60 | return value |
61 | .filter((f) => f.name.toLowerCase().endsWith(".xlsx")) |
62 | .map((f) => ({ value: f.id, label: f.name })) |
63 | } |
64 |
|
65 | export type DynSelect_worksheet = string |
66 |
|
67 | export async function worksheet( |
68 | auth: RT.MsExcel, |
69 | workbook: string, |
70 | drive_id: string | undefined |
71 | ) { |
72 | const wb = workbookUrl(workbook, drive_id) |
73 | const session = await workbookSession(auth, wb) |
74 | const response = await fetch(`${wb}/worksheets?$select=name`, { |
75 | headers: { |
76 | Authorization: `Bearer ${auth.token}`, |
77 | Accept: "application/json", |
78 | "workbook-session-id": session, |
79 | }, |
80 | }) |
81 | if (!response.ok) { |
82 | throw new Error(`${response.status} ${await response.text()}`) |
83 | } |
84 | const { value } = (await response.json()) as { value: { name: string }[] } |
85 | return value.map((w) => ({ value: w.name, label: w.name })) |
86 | } |
87 |
|
88 | |
89 | function columnLetter(n: number): string { |
90 | return n <= 0 |
91 | ? "" |
92 | : columnLetter(Math.floor((n - 1) / 26)) + |
93 | String.fromCharCode(65 + ((n - 1) % 26)) |
94 | } |
95 |
|
96 | |
97 | * Add Row |
98 | * Append one row below the last used row of a worksheet, starting in column A. `values` is the row's cells in order, e.g. `["Ada", 36, "=B2*2"]`. For a worksheet formatted as a table, Add Table Rows is the safer choice. `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. |
99 | */ |
100 | export async function main( |
101 | auth: RT.MsExcel, |
102 | workbook: DynSelect_workbook, |
103 | worksheet: DynSelect_worksheet, |
104 | values: any[], |
105 | drive_id: string | undefined |
106 | ) { |
107 | const wb = workbookUrl(workbook, drive_id) |
108 | const session = await workbookSession(auth, wb) |
109 | if (!values?.length) throw new Error("values needs at least one cell") |
110 | const usedResponse = await fetch( |
111 | `${wb}/worksheets/${encodeURIComponent(worksheet)}/usedRange(valuesOnly=true)?$select=address,values`, |
112 | { |
113 | headers: { |
114 | Authorization: `Bearer ${auth.token}`, |
115 | Accept: "application/json", |
116 | "workbook-session-id": session, |
117 | }, |
118 | } |
119 | ) |
120 | if (!usedResponse.ok) { |
121 | throw new Error(`${usedResponse.status} ${await usedResponse.text()}`) |
122 | } |
123 | |
124 | const used = (await usedResponse.json()) as { |
125 | address: string |
126 | values: any[][] |
127 | } |
128 | const isEmpty = |
129 | used.values.length === 1 && |
130 | used.values[0].length === 1 && |
131 | used.values[0][0] === "" |
132 | const row = isEmpty ? 1 : Number(used.address.match(/(\d+)$/)![1]) + 1 |
133 | const address = `A${row}:${columnLetter(values.length)}${row}` |
134 |
|
135 | const response = await fetch( |
136 | `${wb}/worksheets/${encodeURIComponent(worksheet)}/range(address='${address}')`, |
137 | { |
138 | method: "PATCH", |
139 | headers: { |
140 | Authorization: `Bearer ${auth.token}`, |
141 | "Content-Type": "application/json", |
142 | Accept: "application/json", |
143 | "workbook-session-id": session, |
144 | }, |
145 | body: JSON.stringify({ values: [values] }), |
146 | } |
147 | ) |
148 |
|
149 | if (!response.ok) { |
150 | throw new Error(`${response.status} ${await response.text()}`) |
151 | } |
152 |
|
153 | return await response.json() |
154 | } |
155 |
|