@@ -10,47 +10,38 @@ const OUTPUT_FILE = new URL("./working-group-events.ndjson", import.meta.url)
1010const DAYS_BACK = 30
1111const DAYS_TO_KEEP = 90
1212const DAYS_AHEAD = 30
13- const DATETIME_REGEX =
14- / ^ \d { 4 } - \d { 2 } - \d { 2 } T \d { 2 } : \d { 2 } (?: : \d { 2 } ) ? (?: \. \d + ) ? (?: Z | [ + - ] \d { 2 } : \d { 2 } ) ? $ /
1513
1614const Instant = type ( {
1715 "dateTime?" : "string" ,
18- "date?" : "string" ,
19- "timeZone?" : "string" ,
16+ "date?" : "string" , // full day events have just date
2017} )
18+ . pipe ( ( value ) : string => {
19+ if ( value . dateTime ) return value . dateTime
20+ if ( value . date ) return `${ value . date } T00:00:00Z`
21+ throw new Error ( "Instant is missing date/dateTime" )
22+ } )
23+ . to ( "string.date" )
2124
22- const Event = type ( {
25+ const calendarEventSchema = type ( {
2326 id : "string" ,
27+ "title?" : "string" ,
2428 "status?" : "string" ,
25- "summary?" : "string" ,
26- "location?" : "string" ,
2729 "description?" : "string" ,
30+ "location?" : "string" ,
2831 start : Instant ,
2932 end : Instant ,
3033 htmlLink : "string" ,
3134 updated : "string" ,
3235} )
3336
3437const responseSchema = type ( {
35- items : Event . array ( ) ,
38+ items : calendarEventSchema . array ( ) ,
3639 "nextSyncToken?" : "string" ,
3740 "nextPageToken?" : "string" ,
3841} )
3942
40- const WorkingGroupMeetingSchema = type ( {
41- id : "string" ,
42- title : "string" ,
43- "description?" : "string" ,
44- "location?" : "string" ,
45- start : Instant ,
46- end : Instant ,
47- htmlLink : "string" ,
48- updated : "string" ,
49- calendarId : "string" ,
50- } )
51-
52- type CalendarEvent = typeof Event . infer
53- export type WorkingGroupMeeting = typeof WorkingGroupMeetingSchema . infer
43+ export type WorkingGroupMeeting =
44+ typeof calendarEventSchema . inferIntrospectableOut
5445
5546async function main ( ) {
5647 if ( ! API_KEY ) {
@@ -62,10 +53,7 @@ async function main() {
6253 const existingMeetings = await readExistingMeetings ( )
6354 console . log ( `Found ${ existingMeetings . length } existing event(s) in file` )
6455
65- const lastMeeting = existingMeetings . at ( - 1 )
66- const lastMeetingStart =
67- lastMeeting ?. start . dateTime ??
68- ( lastMeeting ?. start . date ? `${ lastMeeting . start . date } T00:00:00Z` : null )
56+ const lastMeetingStart = existingMeetings . at ( - 1 ) ?. start ?? null
6957 const cutoffDate = new Date (
7058 now . getTime ( ) - DAYS_TO_KEEP * 24 * 60 * 60 * 1000 ,
7159 )
@@ -77,7 +65,7 @@ async function main() {
7765
7866 const timeMin =
7967 lastMeetingStart !== null && lastMeetingStart !== undefined
80- ? new Date ( Math . min ( Date . parse ( lastMeetingStart ) + 1 , now . getTime ( ) ) )
68+ ? new Date ( Math . min ( Date . parse ( lastMeetingStart ) , now . getTime ( ) ) )
8169 : new Date ( now . getTime ( ) - DAYS_BACK * 24 * 60 * 60 * 1000 )
8270
8371 const timeMax = new Date ( now . getTime ( ) + DAYS_AHEAD * 24 * 60 * 60 * 1000 )
@@ -110,9 +98,9 @@ async function main() {
11098
11199 const payload = responseSchema . assert ( body )
112100
113- let allNewMeetings = payload . items
101+ let newMeetings = payload . items
114102 . filter ( event => event . status !== "cancelled" )
115- . map ( toWorkingGroupMeeting )
103+ . map ( event => calendarEventSchema . out . assert ( event ) )
116104
117105 if ( payload . nextPageToken ) {
118106 let pageToken : string | undefined = payload . nextPageToken
@@ -129,17 +117,16 @@ async function main() {
129117 throw new Error ( `Page fetch failed: ${ pageResponse . status } ` )
130118 }
131119 const pagePayload = responseSchema . assert ( pageBody )
132- allNewMeetings = [
133- ...allNewMeetings ,
120+ newMeetings = [
121+ ...newMeetings ,
134122 ...pagePayload . items
135123 . filter ( event => event . status !== "cancelled" )
136- . map ( toWorkingGroupMeeting ) ,
124+ . map ( event => calendarEventSchema . out . assert ( event ) ) ,
137125 ]
138126 pageToken = pagePayload . nextPageToken
139127 }
140128 }
141129
142- const newMeetings = allNewMeetings
143130 const newIds = new Set ( newMeetings . map ( meeting => meeting . id ) )
144131 const existingIds = new Set ( existingMeetings . map ( meeting => meeting . id ) )
145132 const newCount = Array . from ( newIds ) . filter ( id => ! existingIds . has ( id ) ) . length
@@ -159,8 +146,7 @@ async function main() {
159146 const futureLimit = new Date ( now . getTime ( ) + DAYS_AHEAD * 24 * 60 * 60 * 1000 )
160147 const futureLimitStr = futureLimit . toISOString ( ) . split ( "T" ) [ 0 ]
161148 const filteredMeetings = allMeetings . filter ( meeting => {
162- const start = meeting . start . dateTime ?? meeting . start . date ?? ""
163- const startDate = start . split ( "T" ) [ 0 ]
149+ const startDate = meeting . start . split ( "T" ) [ 0 ]
164150 return startDate >= cutoffDateStr && startDate <= futureLimitStr
165151 } )
166152
@@ -171,18 +157,16 @@ async function main() {
171157 `Filtered to ${ filteredMeetings . length } event(s) after removing old entries` ,
172158 )
173159
174- const sortedMeetings = filteredMeetings . sort ( ( a , b ) => {
175- const aStart = a . start . dateTime ?? a . start . date ?? ""
176- const bStart = b . start . dateTime ?? b . start . date ?? ""
177- return aStart . localeCompare ( bStart )
178- } )
160+ const sortedMeetings = filteredMeetings . sort ( ( a , b ) =>
161+ a . start . localeCompare ( b . start ) ,
162+ )
179163
180164 const ndjson = sortedMeetings . map ( event => JSON . stringify ( event ) ) . join ( "\n" )
181165 const content = sortedMeetings . length > 0 ? `${ ndjson } \n` : ""
182166 await writeFile ( OUTPUT_FILE , content , "utf8" )
183167
184168 console . log (
185- `Saved ${ sortedMeetings . length } event(s) to ${ OUTPUT_FILE . pathname } ` ,
169+ `Saved ${ sortedMeetings . length } event(s) ( ${ newCount } new) to ${ OUTPUT_FILE . pathname } ` ,
186170 )
187171}
188172
@@ -193,7 +177,7 @@ async function readExistingMeetings(): Promise<WorkingGroupMeeting[]> {
193177 . trim ( )
194178 . split ( "\n" )
195179 . filter ( line => line . trim ( ) )
196- . map ( line => WorkingGroupMeetingSchema . assert ( JSON . parse ( line ) ) )
180+ . map ( line => calendarEventSchema . out . assert ( JSON . parse ( line ) ) )
197181 } catch ( error : any ) {
198182 if ( error . code === "ENOENT" ) {
199183 return [ ]
@@ -222,38 +206,6 @@ function mergeMeetings(
222206 return Array . from ( byId . values ( ) )
223207}
224208
225- function toWorkingGroupMeeting ( event : CalendarEvent ) : WorkingGroupMeeting {
226- return WorkingGroupMeetingSchema . assert ( {
227- id : event . id ,
228- title : event . summary || "Untitled working group meeting" ,
229- ...( event . description && { description : event . description } ) ,
230- ...( event . location && { location : event . location } ) ,
231- start : event . start ,
232- end : event . end ,
233- htmlLink : assertUrl ( event . htmlLink , "event.htmlLink" ) ,
234- updated : assertDateTime ( event . updated , "event.updated" ) ,
235- calendarId : CALENDAR_ID ,
236- } )
237- }
238-
239- function assertDateTime ( value : string , label : string ) {
240- if ( ! DATETIME_REGEX . test ( value ) ) {
241- throw new Error (
242- `Invalid ${ label } : expected YYYY-MM-DDThh:mm:ssZ or offset, received ${ value } ` ,
243- )
244- }
245- return value
246- }
247-
248- function assertUrl ( value : string , label : string ) {
249- try {
250- new URL ( value )
251- return value
252- } catch {
253- throw new Error ( `Invalid ${ label } : received ${ value } ` )
254- }
255- }
256-
257209try {
258210 await main ( )
259211} catch ( error : unknown ) {
0 commit comments