Skip to content

Commit ce1d0c3

Browse files
committed
Improve the script
1 parent 9b54cb5 commit ce1d0c3

File tree

6 files changed

+209
-85
lines changed

6 files changed

+209
-85
lines changed

pnpm-lock.yaml

Lines changed: 34 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pnpm-workspace.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
packages:
22
- "."
33
- "scripts/sync-landing-schema"
4+
- "scripts/sync-working-groups"

scripts/sync-working-groups/index.tsx

Lines changed: 0 additions & 85 deletions
This file was deleted.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"name": "@graphql-website/sync-working-groups",
3+
"private": true,
4+
"type": "module",
5+
"main": "./sync-working-groups.ts",
6+
"scripts": {
7+
"start": "node ./sync-working-groups.ts"
8+
},
9+
"dependencies": {
10+
"arktype": "^2.1.27"
11+
}
12+
}
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
#!/usr/bin/env node
2+
3+
import { writeFile } from "node:fs/promises"
4+
import { type } from "arktype"
5+
6+
const CALENDAR_ID =
7+
"linuxfoundation.org_ik79t9uuj2p32i3r203dgv5mo8@group.calendar.google.com"
8+
const API_KEY = process.env.GOOGLE_CALENDAR_API_KEY
9+
const OUTPUT_FILE = new URL("./upcoming-events.ndjson", import.meta.url)
10+
const MAX_RESULTS = 25
11+
const DATETIME_REGEX =
12+
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2})?(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})?$/
13+
14+
const Instant = type({
15+
"dateTime?": "string",
16+
"date?": "string",
17+
"timeZone?": "string",
18+
})
19+
20+
const Event = type({
21+
id: "string",
22+
"status?": "string",
23+
"summary?": "string",
24+
"location?": "string",
25+
"description?": "string",
26+
start: Instant,
27+
end: Instant,
28+
htmlLink: "string",
29+
updated: "string",
30+
})
31+
32+
const responseSchema = type({
33+
items: Event.array(),
34+
})
35+
36+
const WorkingGroupMeetingSchema = type({
37+
id: "string",
38+
title: "string",
39+
"description?": "string",
40+
"location?": "string",
41+
start: Instant,
42+
end: Instant,
43+
htmlLink: "string",
44+
updated: "string",
45+
calendarId: "string",
46+
})
47+
48+
type CalendarEvent = typeof Event.infer
49+
export type WorkingGroupMeeting = typeof WorkingGroupMeetingSchema.infer
50+
51+
async function main() {
52+
if (!API_KEY) {
53+
console.error("GOOGLE_CALENDAR_API_KEY is not set")
54+
process.exit(1)
55+
}
56+
57+
const timeMin = new Date().toISOString()
58+
const searchParams = new URLSearchParams({
59+
key: API_KEY,
60+
timeMin,
61+
singleEvents: "true",
62+
orderBy: "startTime",
63+
maxResults: String(MAX_RESULTS),
64+
})
65+
66+
const endpoint = new URL(
67+
`${encodeURIComponent(CALENDAR_ID)}/events?${searchParams}`,
68+
"https://www.googleapis.com/calendar/v3/calendars/",
69+
)
70+
71+
console.log(`\nFetching events for calendar: ${CALENDAR_ID}`)
72+
console.log(`Filtering from: ${timeMin}`)
73+
74+
const response = await fetch(endpoint)
75+
const body = await response.json()
76+
if (!response.ok) {
77+
const errorDetails = body.error?.message || response.statusText
78+
throw new Error(
79+
`Calendar API request failed: ${response.status} ${errorDetails}`,
80+
)
81+
}
82+
83+
const payload = responseSchema.assert(body)
84+
const meetings = payload.items
85+
.filter(event => event.status !== "cancelled")
86+
.map(toWorkingGroupMeeting)
87+
.sort((a, b) => {
88+
const aStart = a.start.dateTime ?? a.start.date ?? ""
89+
const bStart = b.start.dateTime ?? b.start.date ?? ""
90+
return aStart.localeCompare(bStart)
91+
})
92+
93+
const ndjson = meetings.map(event => JSON.stringify(event)).join("\n")
94+
const content = meetings.length > 0 ? `${ndjson}\n` : ""
95+
await writeFile(OUTPUT_FILE, content, "utf8")
96+
97+
console.log(`Saved ${meetings.length} event(s) to ${OUTPUT_FILE.pathname}`)
98+
}
99+
100+
function toWorkingGroupMeeting(event: CalendarEvent): WorkingGroupMeeting {
101+
return WorkingGroupMeetingSchema.assert({
102+
id: event.id,
103+
title: event.summary || "Untitled working group meeting",
104+
...(event.description && { description: event.description }),
105+
...(event.location && { location: event.location }),
106+
start: event.start,
107+
end: event.end,
108+
htmlLink: assertUrl(event.htmlLink, "event.htmlLink"),
109+
updated: assertDateTime(event.updated, "event.updated"),
110+
calendarId: CALENDAR_ID,
111+
})
112+
}
113+
114+
function assertDateTime(value: string, label: string) {
115+
if (!DATETIME_REGEX.test(value)) {
116+
throw new Error(
117+
`Invalid ${label}: expected YYYY-MM-DDThh:mm:ssZ or offset, received ${value}`,
118+
)
119+
}
120+
return value
121+
}
122+
123+
function assertUrl(value: string, label: string) {
124+
try {
125+
new URL(value)
126+
return value
127+
} catch {
128+
throw new Error(`Invalid ${label}: received ${value}`)
129+
}
130+
}
131+
132+
try {
133+
await main()
134+
} catch (error: unknown) {
135+
console.error(error)
136+
process.exit(1)
137+
}

0 commit comments

Comments
 (0)