Skip to content

Instantly share code, notes, and snippets.

@Gordin
Last active February 8, 2023 17:40
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Gordin/5120c89115522ca1670559b97643ab9a to your computer and use it in GitHub Desktop.
Save Gordin/5120c89115522ca1670559b97643ab9a to your computer and use it in GitHub Desktop.
Uses the REST API instead of the native firestore library to be able to get whole collections in batches, instead of the builtin listDocuments that always loads all documents. This assumes that you are inside a firebase project with an initialized app, otherwise generating the AccesToken won't work
import fetch from 'node-fetch'
import * as app from 'firebase-admin/app'
type ListDocumentResult = ExistingListDocumentResult | EmptyListDocumentResult
type json = any
interface ExistingListDocumentResult extends EmptyListDocumentResult{
fields: Record<string,any>
createTime: string,
updateTime: string
}
interface EmptyListDocumentResult {
name: string
}
function isEmpty(result: ExistingListDocumentResult | EmptyListDocumentResult): result is EmptyListDocumentResult {
if ((result as ExistingListDocumentResult).fields) { return false }
return true
}
function isListDocumentResult(result: any): result is ExistingListDocumentResult | EmptyListDocumentResult {
if (Object.keys(result).includes('name')) { return true }
return false
}
export class FirestoreRestApi {
readonly parent: string
private _token?: app.GoogleOAuthAccessToken
constructor(readonly project = process.env.GCLOUD_PROJECT,) {
this.parent = `projects/${project}/databases/(default)/documents`
}
async * listDocuments(collectionPath: string, showMissing=`true`): AsyncGenerator<ListDocumentResult> {
let nextPageToken = ''
const pageSize = 500
while (true) {
const url = new URL(`https://firestore.googleapis.com/v1/${this.parent}/${collectionPath}`)
url.searchParams.append('pageSize', `${pageSize}`)
url.searchParams.append('pageToken', nextPageToken)
url.searchParams.append('showMissing', showMissing)
const data = await this.getData(url.toString())
for await (const document of data.documents) {
if (!isListDocumentResult(document)) {
throw new Error(`We got some weird data: ${JSON.stringify(document)}`)
}
yield document
}
if (!data.nextPageToken) {
break
}
nextPageToken = data.nextPageToken
}
}
async * listEmptyDocumentNames(collectionPath: string): AsyncGenerator<string> {
for await (const document of this.listDocuments(collectionPath, `true`)) {
if (isEmpty(document)) {
yield (document as EmptyListDocumentResult).name
}
}
}
private async accessToken(): Promise<string> {
// TODO if this is long-running, make sure to get a new one every hour
if (this._token) { return this._token.access_token }
this._token = await app.applicationDefault().getAccessToken()
return this._token.access_token
}
private async getData(url: string): Promise<json> {
const response = await fetch(url, {
headers: {
Authorization: `Bearer ${await this.accessToken()}`,
'Content-Type': 'application/json'
}
})
if (!response.ok) {
throw new Error(`Failed to fetch data: ${response.statusText}`)
}
return await response.json();
}
}
import { FirestoreRestApi } from '../utilities/rest_api_helper';
const api = new FirestoreRestApi()
const bla = async () => {
for await (const documentName of api.listEmptyDocumentNames('users')) {
// do stuff with the documentName
}
}
const blub = async () => {
for await (const documentName of api.listDocuments('users', `false`)) {
// do stuff with the documents
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment