1 | |
2 |
|
3 | import * as wmill from "windmill-client" |
4 |
|
5 | const graph = "https://graph.microsoft.com/v1.0" |
6 |
|
7 | export type DynSelect_site_id = string |
8 |
|
9 | export async function site_id(auth: RT.MsSharepoint) { |
10 | const response = await fetch( |
11 | `${graph}/sites?search=*&$select=id,displayName,webUrl&$top=200`, |
12 | { |
13 | headers: { |
14 | Authorization: `Bearer ${auth.token}`, |
15 | Accept: "application/json", |
16 | }, |
17 | } |
18 | ) |
19 | if (!response.ok) { |
20 | throw new Error(`${response.status} ${await response.text()}`) |
21 | } |
22 | const { value } = (await response.json()) as { |
23 | value: { id: string; displayName: string; webUrl: string }[] |
24 | } |
25 | return value.map((s) => ({ |
26 | value: s.id, |
27 | label: `${s.displayName} (${s.webUrl})`, |
28 | })) |
29 | } |
30 |
|
31 | export type DynSelect_drive_id = string |
32 |
|
33 | export async function drive_id(auth: RT.MsSharepoint, site_id: string) { |
34 | const response = await fetch( |
35 | `${graph}/sites/${site_id}/drives?$select=id,name`, |
36 | { |
37 | headers: { |
38 | Authorization: `Bearer ${auth.token}`, |
39 | Accept: "application/json", |
40 | }, |
41 | } |
42 | ) |
43 | if (!response.ok) { |
44 | throw new Error(`${response.status} ${await response.text()}`) |
45 | } |
46 | const { value } = (await response.json()) as { |
47 | value: { id: string; name: string }[] |
48 | } |
49 | return value.map((d) => ({ value: d.id, label: d.name })) |
50 | } |
51 |
|
52 | |
53 | function driveUrl(site_id: string, drive_id: string | undefined) { |
54 | return drive_id != null && drive_id !== "" |
55 | ? `${graph}/drives/${encodeURIComponent(drive_id)}` |
56 | : `${graph}/sites/${site_id}/drive` |
57 | } |
58 |
|
59 | |
60 | function ref(item: string | undefined) { |
61 | if (item == null || item === "" || item === "/") return "root" |
62 | if (!item.startsWith("/")) return `items/${encodeURIComponent(item)}` |
63 | const path = item |
64 | .replace(/\/+$/, "") |
65 | .split("/") |
66 | .map(encodeURIComponent) |
67 | .join("/") |
68 | return `root:${path}:` |
69 | } |
70 |
|
71 | |
72 | * New or Updated File |
73 | * Emits the files or folders created (or, with `events`, also modified; a folder counts as modified when something inside it changes) in a site's default document library, or in another of its libraries when `drive_id` is set since the last poll, optionally only under one `folder` path (e.g. `/Reports`). The first run records the drive's current state and emits nothing. Polls the drive's delta feed because Microsoft Graph push needs a change-notification subscription renewed every few weeks against a public endpoint, which belongs in a Windmill native trigger; run this as a Scheduled Poll until then. Requires Sites.Read.All (covered by the default Sites.ReadWrite.All). |
74 | */ |
75 | export async function main( |
76 | auth: RT.MsSharepoint, |
77 | site_id: DynSelect_site_id, |
78 | events: "created" | "created_or_updated" = "created", |
79 | kind: "files" | "folders" | "all" = "files", |
80 | folder: string | undefined, |
81 | drive_id: DynSelect_drive_id | undefined |
82 | ) { |
83 | const drive = driveUrl(site_id, drive_id) |
84 | const state: { delta_link: string; since: string } | undefined = |
85 | await wmill.getState() |
86 | |
87 | const now = new Date().toISOString() |
88 | const latest = `${drive}/root/delta?token=latest` |
89 |
|
90 | |
91 | const items = new Map<string, { [key: string]: any }>() |
92 | let deltaLink: string | undefined |
93 | let expired = false |
94 | let next: string | undefined = state?.delta_link ?? latest |
95 | while (next) { |
96 | const response = await fetch(next, { |
97 | headers: { |
98 | Authorization: `Bearer ${auth.token}`, |
99 | Accept: "application/json", |
100 | }, |
101 | }) |
102 | |
103 | if (response.status === 410 && !expired) { |
104 | expired = true |
105 | items.clear() |
106 | next = latest |
107 | continue |
108 | } |
109 | if (!response.ok) { |
110 | throw new Error(`${response.status} ${await response.text()}`) |
111 | } |
112 | const page = (await response.json()) as { |
113 | value: { [key: string]: any }[] |
114 | "@odata.nextLink"?: string |
115 | "@odata.deltaLink"?: string |
116 | } |
117 | for (const item of page.value) items.set(item.id, item) |
118 | next = page["@odata.nextLink"] |
119 | deltaLink = page["@odata.deltaLink"] ?? deltaLink |
120 | } |
121 | await wmill.setState({ delta_link: deltaLink, since: now }) |
122 |
|
123 | |
124 | if (!state?.delta_link || expired) return [] |
125 |
|
126 | let under = "" |
127 | let underId: string | undefined |
128 | if (folder != null && folder !== "" && folder !== "/") { |
129 | under = folder.replace(/\/+$/, "") |
130 | const response = await fetch(`${drive}/${ref(under)}?$select=id`, { |
131 | headers: { |
132 | Authorization: `Bearer ${auth.token}`, |
133 | Accept: "application/json", |
134 | }, |
135 | }) |
136 | if (!response.ok) { |
137 | throw new Error(`${response.status} ${await response.text()}`) |
138 | } |
139 | underId = ((await response.json()) as { id: string }).id |
140 | } |
141 |
|
142 | return [...items.values()].filter((item) => { |
143 | if (item.deleted || item.root) return false |
144 | if (kind === "files" && !item.file) return false |
145 | if (kind === "folders" && !item.folder) return false |
146 | if ( |
147 | events === "created" && |
148 | Date.parse(item.createdDateTime) < Date.parse(state.since) |
149 | ) { |
150 | return false |
151 | } |
152 | if (under === "") return true |
153 | |
154 | |
155 | const parent = decodeURIComponent(item.parentReference?.path ?? "").split( |
156 | "root:" |
157 | )[1] |
158 | return ( |
159 | item.parentReference?.id === underId || |
160 | parent === under || |
161 | (parent ?? "").startsWith(under + "/") |
162 | ) |
163 | }) |
164 | } |
165 |
|