From 6d5c9a132a3f3ae6c92d45a3497c60c965678a35 Mon Sep 17 00:00:00 2001 From: Sam DeMarrais Date: Tue, 23 Jun 2026 23:39:13 -0400 Subject: [PATCH] Cross-compatibility --- functions/src/events/types.ts | 10 ++ functions/src/hearings/search.ts | 10 +- .../cleanupHearingVideoFormat.ts | 17 ++++ .../updateHearingVideoFormat.ts | 98 +++++++++++++++++++ 4 files changed, 133 insertions(+), 2 deletions(-) create mode 100644 scripts/firebase-admin/cleanupHearingVideoFormat.ts create mode 100644 scripts/firebase-admin/updateHearingVideoFormat.ts diff --git a/functions/src/events/types.ts b/functions/src/events/types.ts index 4101b41d1..952d6cfe5 100644 --- a/functions/src/events/types.ts +++ b/functions/src/events/types.ts @@ -97,6 +97,13 @@ export const HearingContent = BaseEventContent.extend({ export type HearingListItem = Static export const HearingListItem = Record({ EventId: Number }) +export type Video = Static +export const Video = Record({ + url: String, + title: String, + transcriptionId: String +}) + export type Hearing = Static export const Hearing = BaseEvent.extend({ type: L("hearing"), @@ -104,6 +111,9 @@ export const Hearing = BaseEvent.extend({ videoURL: Optional(String), videoTranscriptionId: Optional(String), videoFetchedAt: Optional(InstanceOf(Timestamp)), + transcriptionIds: Optional(Array(String)), + videos: Optional(Array(Video)), + videosFetchedAt: Optional(InstanceOf(Timestamp)), committeeChairs: Optional(Array(String)) }) diff --git a/functions/src/hearings/search.ts b/functions/src/hearings/search.ts index fe26d0385..b36fe5e2f 100644 --- a/functions/src/hearings/search.ts +++ b/functions/src/hearings/search.ts @@ -57,7 +57,13 @@ export const { }, convert: data => { const hearing = Hearing.check(data) - const { content, startsAt: startsAtTimestamp, id, videoURL } = hearing + const { + content, + startsAt: startsAtTimestamp, + id, + videoURL, + videos + } = hearing const startsAt = startsAtTimestamp.toMillis() const schedule = DateTime.fromMillis(startsAt, { zone: timeZone }) @@ -115,7 +121,7 @@ export const { bill => bill.slug || `${courtNumber}/${bill.number}` ), court: courtNumber, - hasVideo: Boolean(videoURL) + hasVideo: Boolean(videoURL || videos?.length) } } }) diff --git a/scripts/firebase-admin/cleanupHearingVideoFormat.ts b/scripts/firebase-admin/cleanupHearingVideoFormat.ts new file mode 100644 index 000000000..e9a0de5cc --- /dev/null +++ b/scripts/firebase-admin/cleanupHearingVideoFormat.ts @@ -0,0 +1,17 @@ +import { FieldValue } from "../../functions/src/firebase" +import { reformatFactory } from "./updateHearingVideoFormat" +import { Script } from "./types" + +function getVideoFormatCleanup(data: FirebaseFirestore.DocumentData): any { + if (!("videoURL" in data)) { + return null + } + + return { + videoTranscriptionId: FieldValue.delete(), + videoFetchedAt: FieldValue.delete(), + videoURL: FieldValue.delete() + } +} + +export const script: Script = reformatFactory(getVideoFormatCleanup) diff --git a/scripts/firebase-admin/updateHearingVideoFormat.ts b/scripts/firebase-admin/updateHearingVideoFormat.ts new file mode 100644 index 000000000..710ef62c4 --- /dev/null +++ b/scripts/firebase-admin/updateHearingVideoFormat.ts @@ -0,0 +1,98 @@ +import { Record, String } from "runtypes" +import { Script } from "./types" + +const Args = Record({ + hearingId: String.optional() +}) + +export function reformatFactory( + fn: (data: FirebaseFirestore.DocumentData) => any +) { + return async ({ + db, + args + }: { + db: FirebaseFirestore.Firestore + args: any + }) => { + const { hearingId } = Args.check(args) + + // Process a single event by eventId + if (hearingId) { + const snapshot = await db + .collection("events") + .where("type", "==", "hearing") + .where("id", "==", hearingId) + .get() + + if (snapshot.empty || snapshot.docs.length !== 1) { + throw new Error( + `The number of documents matching the event id ${hearingId} must be exactly one` + ) + } + + const doc = snapshot.docs[0] + const modify = fn(doc.data()) + if (modify) { + await doc.ref.update(modify) + } + } else { + const snapshot = await db + .collection("events") + .where("type", "==", "hearing") + .get() + + if (snapshot.empty) { + throw new Error("Hearing backfill failed; no documents were found") + } + + let bulkWriter = db.bulkWriter() + + for (const doc of snapshot.docs) { + console.log(doc.data().id) + const modify = fn(doc.data()) + if (modify) { + bulkWriter.update(doc.ref, modify) + } + } + await bulkWriter.close() + } + + console.log("Video backfill complete") + } +} + +function getVideoFormatUpdate(data: FirebaseFirestore.DocumentData): any { + if ("videos" in data || !("videoURL" in data)) { + return null + } + + const url = data.videoURL + const fetchedAt = data.videoFetchedAt + const transcriptionId = data.videoTranscriptionId + + if (!url || !fetchedAt || !transcriptionId) { + throw new Error( + `In the data for ${data.id}, it is expected that if videoURL exists videoFetchedAt and videoTranscriptionId also exist` + ) + } + + const transcriptionIds = [transcriptionId] + + const videos = [ + { + // Default; not shown + title: data.id, + url, + transcriptionId + } + ] + + return { + videos, + transcriptionIds, + videosFetchedAt: fetchedAt + } +} + +export const script: Script = reformatFactory(getVideoFormatUpdate)