11import "server-only" ;
22
3- import { db } from "@/lib/firebase" ;
4- import {
5- collection ,
6- getDocs ,
7- limit ,
8- orderBy ,
9- query ,
10- where ,
11- type DocumentData ,
12- type QueryDocumentSnapshot ,
13- } from "firebase/firestore" ;
143import {
154 DEFAULT_OG_IMAGE_PATH ,
165 SITE_NAME ,
@@ -58,6 +47,37 @@ const VIDEO_URL_FIELDS = [
5847
5948type SocialMap = Record < string , string > ;
6049
50+ type FirestoreValue = {
51+ stringValue ?: string ;
52+ booleanValue ?: boolean ;
53+ integerValue ?: string ;
54+ doubleValue ?: number ;
55+ timestampValue ?: string ;
56+ mapValue ?: {
57+ fields ?: Record < string , FirestoreValue > ;
58+ } ;
59+ arrayValue ?: {
60+ values ?: FirestoreValue [ ] ;
61+ } ;
62+ referenceValue ?: string ;
63+ nullValue ?: null ;
64+ } ;
65+
66+ type FirestoreDocument = {
67+ name : string ;
68+ fields ?: Record < string , FirestoreValue > ;
69+ } ;
70+
71+ type FirestoreListResponse = {
72+ documents ?: FirestoreDocument [ ] ;
73+ nextPageToken ?: string ;
74+ } ;
75+
76+ type ContentDocument = {
77+ id : string ;
78+ data : Record < string , unknown > ;
79+ } ;
80+
6181export type AuthorRecord = {
6282 docId : string ;
6383 uid : string ;
@@ -256,10 +276,97 @@ function buildVideoRecord(
256276 } ;
257277}
258278
259- function normalizeAuthorDoc (
260- doc : QueryDocumentSnapshot < DocumentData > ,
261- ) : AuthorRecord {
262- const data = doc . data ( ) as Record < string , unknown > ;
279+ function getRequiredFirebaseConfig ( ) {
280+ const apiKey = process . env . NEXT_PUBLIC_FIREBASE_API_KEY ;
281+ const projectId = process . env . NEXT_PUBLIC_FIREBASE_PROJECT_ID ;
282+
283+ if ( ! apiKey || ! projectId ) {
284+ throw new Error ( "Missing Firebase public API key or project ID." ) ;
285+ }
286+
287+ return { apiKey, projectId } ;
288+ }
289+
290+ function decodeFirestoreValue ( value : FirestoreValue ) : unknown {
291+ if ( "stringValue" in value ) return value . stringValue || "" ;
292+ if ( "booleanValue" in value ) return Boolean ( value . booleanValue ) ;
293+ if ( "integerValue" in value ) return Number ( value . integerValue || 0 ) ;
294+ if ( "doubleValue" in value ) return Number ( value . doubleValue || 0 ) ;
295+ if ( "timestampValue" in value ) return value . timestampValue || "" ;
296+ if ( "referenceValue" in value ) return value . referenceValue || "" ;
297+ if ( "arrayValue" in value ) {
298+ return ( value . arrayValue ?. values || [ ] ) . map ( ( entry ) =>
299+ decodeFirestoreValue ( entry ) ,
300+ ) ;
301+ }
302+ if ( "mapValue" in value ) {
303+ return decodeFirestoreFields ( value . mapValue ?. fields || { } ) ;
304+ }
305+
306+ return null ;
307+ }
308+
309+ function decodeFirestoreFields ( fields : Record < string , FirestoreValue > ) {
310+ return Object . entries ( fields ) . reduce < Record < string , unknown > > (
311+ ( acc , [ key , value ] ) => {
312+ acc [ key ] = decodeFirestoreValue ( value ) ;
313+ return acc ;
314+ } ,
315+ { } ,
316+ ) ;
317+ }
318+
319+ function getDocumentId ( documentName : string ) {
320+ return documentName . split ( "/" ) . pop ( ) || documentName ;
321+ }
322+
323+ async function getCollectionDocuments (
324+ collectionId : "articles" | "authors" ,
325+ ) : Promise < ContentDocument [ ] > {
326+ const { apiKey, projectId } = getRequiredFirebaseConfig ( ) ;
327+ const documents : ContentDocument [ ] = [ ] ;
328+ let pageToken : string | undefined ;
329+
330+ do {
331+ const params = new URLSearchParams ( {
332+ key : apiKey ,
333+ pageSize : "100" ,
334+ } ) ;
335+
336+ if ( pageToken ) {
337+ params . set ( "pageToken" , pageToken ) ;
338+ }
339+
340+ const response = await fetch (
341+ `https://firestore.googleapis.com/v1/projects/${ encodeURIComponent (
342+ projectId ,
343+ ) } /databases/(default)/documents/${ collectionId } ?${ params . toString ( ) } `,
344+ {
345+ next : { revalidate : 300 } ,
346+ } ,
347+ ) ;
348+
349+ if ( ! response . ok ) {
350+ throw new Error (
351+ `Firestore REST request failed for ${ collectionId } : ${ response . status } ${ response . statusText } ` ,
352+ ) ;
353+ }
354+
355+ const payload = ( await response . json ( ) ) as FirestoreListResponse ;
356+ documents . push (
357+ ...( payload . documents || [ ] ) . map ( ( document ) => ( {
358+ id : getDocumentId ( document . name ) ,
359+ data : decodeFirestoreFields ( document . fields || { } ) ,
360+ } ) ) ,
361+ ) ;
362+ pageToken = payload . nextPageToken ;
363+ } while ( pageToken ) ;
364+
365+ return documents ;
366+ }
367+
368+ function normalizeAuthorDoc ( doc : ContentDocument ) : AuthorRecord {
369+ const data = doc . data ;
263370 const name = pickString ( data , [ "name" ] ) || "Unknown Author" ;
264371
265372 return {
@@ -289,10 +396,10 @@ function createAuthorLookup(authors: AuthorRecord[]) {
289396}
290397
291398function normalizeArticleDoc (
292- doc : QueryDocumentSnapshot < DocumentData > ,
399+ doc : ContentDocument ,
293400 authorLookup : Map < string , AuthorRecord > ,
294401) : ArticleRecord {
295- const data = doc . data ( ) as Record < string , unknown > ;
402+ const data = doc . data ;
296403 const authorUID =
297404 pickString ( data , [ "authorUID" , "authorUid" , "authorId" ] ) || "" ;
298405 const author = authorLookup . get ( authorUID ) ;
@@ -364,19 +471,19 @@ export function buildTopicSummaries(articles: ArticleRecord[]): TopicSummary[] {
364471}
365472
366473export async function getAllAuthors ( ) {
367- const snapshot = await getDocs ( collection ( db , "authors" ) ) ;
368- return snapshot . docs
369- . map ( ( doc ) => normalizeAuthorDoc ( doc ) )
474+ const documents = await getCollectionDocuments ( "authors" ) ;
475+ return documents
476+ . map ( ( document ) => normalizeAuthorDoc ( document ) )
370477 . sort ( ( a , b ) => a . name . localeCompare ( b . name ) ) ;
371478}
372479
373480export async function getPublishedArticles ( limitCount ?: number ) {
374481 const authors = await getAllAuthors ( ) ;
375482 const authorLookup = createAuthorLookup ( authors ) ;
376- const snapshot = await getDocs ( query ( collection ( db , "articles" ) , orderBy ( "date" , "desc" ) ) ) ;
483+ const documents = await getCollectionDocuments ( "articles" ) ;
377484
378- const articles = snapshot . docs
379- . map ( ( doc ) => normalizeArticleDoc ( doc , authorLookup ) )
485+ const articles = documents
486+ . map ( ( document ) => normalizeArticleDoc ( document , authorLookup ) )
380487 . filter ( ( article ) => article . publish ) ;
381488
382489 const sorted = sortArticlesDescending ( articles ) ;
@@ -386,15 +493,12 @@ export async function getPublishedArticles(limitCount?: number) {
386493export async function getPublishedArticleBySlug ( slug : string ) {
387494 const authors = await getAllAuthors ( ) ;
388495 const authorLookup = createAuthorLookup ( authors ) ;
389- const snapshot = await getDocs (
390- query ( collection ( db , "articles" ) , where ( "slug" , "==" , slug ) , limit ( 1 ) ) ,
391- ) ;
496+ const documents = await getCollectionDocuments ( "articles" ) ;
497+ const document = documents . find ( ( entry ) => entry . data . slug === slug ) ;
392498
393- if ( snapshot . empty ) {
394- return null ;
395- }
499+ if ( ! document ) return null ;
396500
397- const article = normalizeArticleDoc ( snapshot . docs [ 0 ] , authorLookup ) ;
501+ const article = normalizeArticleDoc ( document , authorLookup ) ;
398502 return article . publish ? article : null ;
399503}
400504
@@ -404,15 +508,10 @@ export async function getPublishedArticlesForAuthor(authorUID: string) {
404508}
405509
406510export async function getAuthorBySlug ( slug : string ) {
407- const snapshot = await getDocs (
408- query ( collection ( db , "authors" ) , where ( "slug" , "==" , slug ) , limit ( 1 ) ) ,
409- ) ;
410-
411- if ( snapshot . empty ) {
412- return null ;
413- }
511+ const documents = await getCollectionDocuments ( "authors" ) ;
512+ const document = documents . find ( ( entry ) => entry . data . slug === slug ) ;
414513
415- return normalizeAuthorDoc ( snapshot . docs [ 0 ] ) ;
514+ return document ? normalizeAuthorDoc ( document ) : null ;
416515}
417516
418517export async function getPublishedTopicBySlug ( topicSlug : string ) {
0 commit comments