{"id":13779122,"url":"https://github.com/mrbandler/FsFirestore","last_synced_at":"2025-05-11T12:32:47.911Z","repository":{"id":52973371,"uuid":"153169064","full_name":"mrbandler/FsFirestore","owner":"mrbandler","description":"Functional F# library to access Firestore database hosted on Google Cloud Platform (GCP) or Firebase.","archived":false,"fork":false,"pushed_at":"2021-04-11T20:08:10.000Z","size":221,"stargazers_count":26,"open_issues_count":2,"forks_count":4,"subscribers_count":3,"default_branch":"master","last_synced_at":"2024-09-30T07:14:51.898Z","etag":null,"topics":["base","cloud","core","dotnet","fire","firebase","firebase-database","firestore","fs","fsfirestore","fsharp","gcp","google","platform","store"],"latest_commit_sha":null,"homepage":"","language":"F#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/mrbandler.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2018-10-15T19:20:09.000Z","updated_at":"2024-05-05T15:16:44.000Z","dependencies_parsed_at":"2022-09-04T09:22:48.563Z","dependency_job_id":null,"html_url":"https://github.com/mrbandler/FsFirestore","commit_stats":null,"previous_names":[],"tags_count":12,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mrbandler%2FFsFirestore","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mrbandler%2FFsFirestore/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mrbandler%2FFsFirestore/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mrbandler%2FFsFirestore/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mrbandler","download_url":"https://codeload.github.com/mrbandler/FsFirestore/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":225049026,"owners_count":17412912,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["base","cloud","core","dotnet","fire","firebase","firebase-database","firestore","fs","fsfirestore","fsharp","gcp","google","platform","store"],"created_at":"2024-08-03T18:01:01.372Z","updated_at":"2024-11-17T14:31:12.572Z","avatar_url":"https://github.com/mrbandler.png","language":"F#","funding_links":["https://www.paypal.me/mrbandler/"],"categories":["Cloud"],"sub_categories":[],"readme":"\u003cimg src=\"https://raw.githubusercontent.com/mrbandler/FsFirestore/master/Icons/FsFirestoreTransparentBanner.png\" alt=\"FsFirestore Icon Banner\" width=\"150\" height=\"110\"/\u003e\n\n# FsFirestore\n\n[![Action status](https://github.com/mrbandler/FsFirestore/workflows/build/badge.svg)](https://github.com/mrbandler/FsFirestore/actions)\n[![NuGet Badge](https://buildstats.info/nuget/FsFirestore?includePreReleases=true)](https://www.nuget.org/packages/FsFirestore)\n\n[![Donate with Bitcoin](https://en.cryptobadges.io/badge/micro/3LTBGYAHQCDE4ZbEiTreJjzgnsDhY6X2D2)](https://en.cryptobadges.io/donate/3LTBGYAHQCDE4ZbEiTreJjzgnsDhY6X2D2)\n[![Donate with Litecoin](https://en.cryptobadges.io/badge/micro/LcHsJH13A8PmHJQwpbWevGUebZwhWNMXgS)](https://en.cryptobadges.io/donate/LcHsJH13A8PmHJQwpbWevGUebZwhWNMXgS)\n[![Donate with Ethereum](https://en.cryptobadges.io/badge/micro/0x54499ee409687E9C43589693093D004a0cbfEE72)](https://en.cryptobadges.io/donate/0x54499ee409687E9C43589693093D004a0cbfEE72)\n[![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.me/mrbandler/)\n\n**Functional F# library to access Firestore database hosted on Google Cloud Platform (GCP) or Firebase.**\n\n## Table Of Content\n1. [Usage](#1-usage) ⌨️\n2. [Bugs and Features](#2-bugs-and-features) 🐞💡\n3. [Buy me a coffee](#3-buy-me-a-coffee) ☕\n4. [License](#4-license) 📃\n\n---\n\n## 1. Usage\n\n### Connect to Firestore\n\nTo use any of the Firestore features you have to initialize the connection via a Service Account JSON (either for [Firebase](https://console.firebase.google.com/project/_/settings/serviceaccounts/adminsdk) or [GCP](https://cloud.google.com/docs/authentication/getting-started)).\n\nThere are four ways to connect to your Firestore instance.\n\n#### 1. Service Account JSON Path\n\n```fsharp\nopen FsFirestore.Firestore\n\nlet connected = connectToFirestore \"./path/to/your/service_account.json\"\n```\n\nThe `connectToFirestore` function returns a boolean, to indicate whether the connection could be established.\n\n#### 2. Project ID\n\nSet the path to your service account JSON to the environment variable `GOOGLE_APPLICATION_CREDENTIALS` and use your project ID to connect.\n\n```fsharp\nopen FsFirestore.Firestore\n\nconnectToFirestoreProject \"your_project_id\"\n```\n\n#### 3. Service Account JSON Path \u0026 DB Builder\n\nUse a DB builder to connect, this lets you specify additional settings such as Custom Converters (example below).\n\n```fsharp\nopen FsFirestore.Firestore\nopen Google.Cloud.Firestore\n\n// Player ID.\ntype PlayerId (id: string) =\n    let id = id\n    member this.Id with get () = id\n\n// Custom player ID converter.\ntype PlayerIdConverter () =\n    interface IFirestoreConverter\u003cPlayerId\u003e with\n\n    member this.ToFirestore value = value.Id :\u003e obj\n\n    member this.FromFirestore value =\n        match value with\n        | :? string as id -\u003e PlayerId id\n        | _ -\u003e PlayerId \"\"\n\n// Use custom type with custom converter.\n[\u003cFirestoreData\u003e]\ntype Game () =\n    inherit FirestoreDocument()\n\n    [\u003cFirestoreProperty\u003e]\n    member val PlayerA = PlayerId \"0\"\n\n    [\u003cFirestoreProperty\u003e]\n    member val PlayerB = PlayerId \"1\"\n\n// connect :: Unit -\u003e Boolean\nlet connect () =\n    let converter = PlayerIdConverter ()\n    let registry = ConverterRegistry ()\n    registry.Add converter\n\n    let builder = FirestoreDbBuilder ()\n    builder.ConverterRegistry \u003c- registry\n\n    builder |\u003e connectToFirestoreWithBuilder \"./path/to/your/service_account.json\"\n```\n\n#### 4. DB Builder Only\n\nSet the path to your service account JSON to the environment variable `GOOGLE_APPLICATION_CREDENTIALS` and use your project ID to connect.\n\n```fsharp\nopen FsFirestore.Firestore\nopen Google.Cloud.Firestore\n\nlet builder = FirestoreDbBuilder ()\nbuilder.ProjectId \u003c- \"your_project_id\"\n\nbuilder |\u003e connectToFirestoreWithBuilderOnly\n```\n\n### Create, Read, Update and Delete (CRUD)\n\nAfter your successfully connected to your Firestore you can start manipulating data.\n\n#### Model Classes\n\nFor better handling within the API we added generic retrieval functions, to simply retrieve your wanted model. Sadly the Google .NET API only let's us use classes, to compensate we created a base class that can be used to make your life easier. For a detailed description on data models for Firebase read [this](http://googleapis.github.io/google-cloud-dotnet/docs/Google.Cloud.Firestore/datamodel.html).\n\n```fsharp\nopen Google.Cloud.Firestore\nopen FsFirestore.Types\n\n[\u003cFirestoreData\u003e]\ntype Address() =\n    inherit FirestoreDocument() // Base class that comes with FsFirestore\n\n    [\u003cFirestoreProperty\u003e]\n    member val Street = \"Pennsylvania Avenue\" with get, set\n\n    [\u003cFirestoreProperty\u003e]\n    member val HouseNo = 1600 with get, set\n\n    [\u003cFirestoreProperty\u003e]\n    member val City = \"Washington\" with get, set\n\n    [\u003cFirestoreProperty\u003e]\n    member val State = \"DC\" with get, set\n\n// Because of the inheritance we now have some niffty features in this straight forward model class.\nlet address = new Address()\n\n// We can retrieve all fields in a object list,\n// which can later be used to query Firestore.\nlet fields = address.AllFields // =\u003e [\"Pennsylvania Avenue\"; 1600; \"Washington\"; \"DC\"]\nlet specificField = address.Fields(\"HouseNo\", \"City\") // =\u003e [1600; \"Washington\"]\n\n// We can ask the model which ID and collection it belongs to.\nlet docId = address.Id\nlet collectionId = address.CollectionId\n```\n\n#### Reading Documents\n\n```fsharp\nopen Google.Cloud.Firestore\nopen FsFirestore.Firestore\n\n// Let's read an address from Firestore and\n// automatically convert it to our model.\nlet address = document\u003cAddress\u003e \"addresses\" \"POTUS-address\"\n\n// Again if your model inherits from \"FirestoreDocument\"\n// you can use these features.\nlet docId = address.Id // =\u003e \"POTUS-address\"\"\nlet collectionId = address.CollectionId // =\u003e \"addresses\"\"\n\n// --- or ---\n\n// Let's just retrieve the document reference...\nlet addressRef = documentRef \"addresses\" \"POTUS-address\"\n// And convert it to our model class manually.\nlet address = convertTo\u003cAddress\u003e addressRef\n\n// Again if your model inherits from \"FirestoreDocument\"\n// you can use these features.\nlet docId = address.Id // =\u003e \"POTUS-address\"\"\nlet collectionId = address.CollectionId // =\u003e \"addresses\"\"\n\n// --- or to check if a document exists ---\n\n// Let's retrieve a document snapshot.\nlet addressSnap = documentSnapshot \"addresses\" \"POTUS-address\"\n\n// Then you can check if a snapshot exists.\nlet exists = addressSnap.Exists // =\u003e true, false\n\n```\n\n#### Querying Documents\n\n```fsharp\nopen Google.Cloud.Firestore\nopen FsFirestore.Firestore\nopen FsFirestore.Query\n\n// To query a collection we first need to retrieve it from Firestore\nlet queryCollection = collection \"addresses\"\n\n// Now we can chain conditions.\n// Let's query all addresses in Pennsylvania Avenue, DC up to POTUS's one.\nlet addresses =\n    queryCollection\n    |\u003e orderBy \"HouseNo\"\n    |\u003e whereEqualTo \"State\" \"DC\"\n    |\u003e whereEqualTo \"Street\" \"Pennsylvania Avenue\"\n    |\u003e whereGreaterThenOrEqualTo \"HouseNo\" 1\n    |\u003e whereLessThenOrEqualTo \"HouseNo\" 1600\n    |\u003e execQuery\u003cAddress\u003e\n```\n\n#### Writing Documents\n\n```fsharp\nopen Google.Cloud.Firestore\nopen FsFirestore.Firestore\n\n// Let's create the model that we want to add to Firestore\nlet address = new Address()\n\n// Now we can add the address to Firestore with a given\n// collection name and ID.\nlet docRef = addDocument \"addresses\" (Some \"POTUS-address\") address\n\n// -- or --\n\n// We can also add the address and let the ID be generated automatically.\nlet docRef = addDocument \"addresses\" None address\n```\n\n#### Updating Documents\n\n```fsharp\nopen FsFirestore.Firestore\n\n// We can first the read the document we want to update\n// from Firestore.\nlet address = document\u003cAddress\u003e \"addresses\" \"POTUS-address\"\n\n// Now let's move the presidents house number along one number.\naddress.HouseNo \u003c- 1601\n\n// And update the document within Firestore.\nlet docRef = updateDocument \"addresses\" \"POTUS-address\" address\n```\n\n#### Deleting  Documents\n\n```fsharp\nopen FsFirestore.Firestore\n\n// To delete a document we simply need the collection ID\n// and the document ID.\ndeleteDocument None \"addresses\" \"POTUS-address\"\n\n// -- or --\n\n// Additionally we can specify a precondition for the deletion process.\nlet timeStamp = Timestamp.FromDateTime(DateTime.Today)\nlet precondition = Precondition.LastUpdated(timeStamp)\ndeleteDocument precondition \"addresses\" \"POTUS-address\"\n```\n\n### Transactions\n\nTransactions are functions which take in a `Transaction` object to create, read, update, delete and query documents within the transaction scope.\n\n##### Reading Documents in Transaction\n\n```fsharp\nopen Google.Cloud.Firestore\nopen FsFirestore.Transaction\n\n// Reading a document works just as expected only with the minor difference\n// to use the transaction specific function from the Transaction module.\nlet transactionFunc (trans: Transaction) =\n\tdocumentInTrans\u003cAddress\u003e trans \"addresses\" \"POTUS-address\"\n\n// Now let's run the transaction.\n// Notice that the transaction will return the return value from the actual transaction\n// function.\nlet address = runTransaction transactionFunc\n\n// -- or --\n\n// You can specify the return values type, but F# will detected the type automatically\n// most of the time.\nlet address = runTransaction\u003cAddress\u003e transactionFunc\n```\n\n##### Writing Documents in Transaction\n\n```fsharp\nopen Google.Cloud.Firestore\nopen FsFirestore.Transaction\n\n// Let's create the model that we want to add to Firestore\nlet address = new Address()\n\n// Now let's write a transaction to add an address to Firestore with a given\n// collection name and ID.\nlet transactionFunc (trans: Transaction) =\n\taddDocumentInTrans trans \"addresses\" (Some \"POTUS-address\") address\n\n// Let's run the transaction.\nlet docRef = runTransaction transactionFunc\n\n// -- or --\n\n// Of course, we can also add the address and let the ID be generated automatically.\nlet transactionFunc (trans: Transaction) =\n\taddDocumentInTrans trans \"addresses\" None address\n```\n\n##### Updating Documents in Transaction\n\n```fsharp\nopen Google.Cloud.Firestore\nopen FsFirestore.Firestore\n\n// Let's create our update transaction.\nlet transactionFunc (trans: Transaction) =\n    // We can first read the document we want to update\n    // from Firestore.\n\tlet address = documentInTrans\u003cAddress\u003e trans \"addresses\" \"POTUS-address\"\n\n    // Now let's move the presidents house number along one number.\n    address.HouseNo \u003c- 1601\n\n    // And update the document.\n    updateDocumentInTrans trans \"addresses\" \"POTUS-address\" address\n\n// Let's run the transaction.\nlet docRef = runTransaction transactionFunc\n```\n\n##### Deleting Documents in Transaction\n\n```fsharp\nopen Google.Cloud.Firestore\nopen FsFirestore.Firestore\n\n// Let's create our deletion transaction.\nlet transactionFunc (trans: Transaction) =\n    // To delete a document we simply need the collection ID\n    // and the document ID.\n\tdeleteDocumentInTrans trans None \"addresses\" \"POTUS-address\"\n\n// Let's run the transaction.\nrunTransaction transactionFunc\n\n// -- or --\n\n// As mentioned in the CRUD section we can also specify a precondition\n// for the deletion process.\nlet transactionFunc (trans: Transaction) =\n    let timeStamp = Timestamp.FromDateTime(DateTime.Today)\n    let precondition = Precondition.LastUpdated(timeStamp)\n    deleteDocumentInTrans trans precondition \"addresses\" \"POTUS-address\"\n\nrunTransaction transactionFunc\n```\n\n### Listening for Changes\n\nFirestore provides the ability to use a streaming API that let's you listen to changes made do specific documents or a complete set of documents managed by a query.\n\nThe listener functions will be called on document creation, deletion and update. You can even specify a listener functions for none existing documents, this requires a named document ID before hand.\n\n#### Data Definition\n\nFor the listening examples we will use different model classes.\n\n```fsharp\nopen Google.Cloud.Firestore\nopen FsFirestore.Types\n\n// Stores scores per user, the username will be the document ID.\n[\u003cFirestoreData\u003e]\ntype Score() =\n\tinherit FirestoreDocument() // Base class that comes with FsFirestore\n\n\t[\u003cFirestoreProperty\u003e]\n\tmember val BestScore = 0 with get, set\n\n\t[\u003cFirestoreProperty\u003e]\n\tmember val LastScore = 0 with get, set\n\n// Stores a list of usernames that have high scores.\n// A highscore is any score above 1000.\n[\u003cFirestoreData\u003e]\ntype HighScores() =\n\tinherit FirestoreDocument() // Base class that comes with FsFirestore\n\n\t[\u003cFirestoreProperty\u003e]\n\tmember val Usernames = [] with get, set\n```\n\n#### Mounting Listeners\n\n```fsharp\nopen FsFirestore.Firestore\nopen FsFirestore.Listening\n\n// To creater a listener it's as easy as writing a simple function,\n// as it practically is just a function.\nlet callback (snap: DocumentSnapshot) =\n\tif snap.Exists = true then\n\t\t// The callback takes in a document snapshot, we can convert the snap\n\t\t// to our model.\n\t\tlet score = convertSnapshotTo\u003cScore\u003e snap\n\n\t\t// Now we can use the listener to set the best score to the last score\n\t\t// if it was better then the current best score.\n\t\tif score.LastScore \u003e score.BestScore then\n\t\t\tscore.BestScore \u003c- score.LastScore\n\t\t\tupdateDocument score.CollectionId score.Ids score\n\n// Now we can simple mount our created listener callback and in\n// turn receive a listener object from Firestore\nlet listener = listenOnDocument \"scores\" \"mrbandler\" callback\n\n// If we want to stop listening we just stop listening.\nstopListening listener\n```\n\n#### Listening For Query Changes\n\n```fsharp\nopen FsFirestore.Firestore\nopen FsFirestore.Listening\n\n// Now we can use a listener to update a different document with the\n// best high scores.\nlet callback (querySnap: QuerySnapshot) =\n\t// If the query changes the callback is called and we can retrieve the\n\t// updated query results.\n\tlet scores = convertSnapshotsTo\u003cScore\u003e querySnap.Documents |\u003e List.ofSeq\n\n\t// Now we can extract the usernames from the scores into an array.\n\tlet usernames =\n\t\tscores\n\t\t|\u003e List.map (fun score -\u003e score.Id)\n\t\t|\u003e Array.ofList\n\n\t// Let's update our highscores document with the new usernames.\n\tlet highScores = document\u003cHighScores\u003e \"highscores\" \"users\"\n\thighScores.Usernames \u003c- usernames\n\n\tupdateDocument highScores.CollectionId highScores.Id highScores\n\n// Now let's mount our created listener callback to a query.\n// We only want highscores that are above 1000, to be neat we also order them.\nlet listener =\n\tcollection \"scores\"\n\t|\u003e whereGreaterThen \"BestScore\" 1000\n\t|\u003e orderBy \"BestScore\"\n\t|\u003e listenOnQuery callback\n\n// If we want to stop listening we just stop listening.\nstopListening listener\n```\n\nIn the above example we a simply retrieving all documents from the query which can be a lot of data. There is a way to only work with the changes from the query.\n\n```fsharp\nopen FsFirestore.Firestore\nopen FsFirestore.Listening\n\n// Now we can use a listener to update a different document with the\n// best high scores.\nlet callback (querySnap: QuerySnapshot) =\n\t// If the query changes the callback is called and we can retrieve the\n\t// updated query results.\n\tlet scoreChanges = convertQueryChanges\u003cScore\u003e querySnap.Changes |\u003e List.ofSeq\n\n\t// A document change contains a bit more data then a usual document\n\tlet scoreChange = List.item 0\n\tlet doc = scoreChange.document           // =\u003e Actuall converted document data\n \tlet changeType = scoreChange.changeType  // =\u003e Added (there also is Updated and Removed)\n \tlet newIndex = scoreChange.newIndex      // =\u003e New index (option) if moved in the query\n\tlet oldIndex = scoreChange.oldIndex      // =\u003e Old index (option) if moved in the query\n\n\t// Now we can extract the usernames from the scores into an array.\n\tlet usernames =\n\t\tscoreChanges\n\t\t|\u003e List.filter (fun scoreChange -\u003e scoreChange.changeType = DocumentChange.Type.Added)\n\t\t|\u003e List.map (fun scoreChange -\u003e scoreChange.document.Id)\n\t\t|\u003e Array.ofList\n\n\t// Let's update our highscores document with the new usernames.\n\tlet highScores = document\u003cHighScores\u003e \"highscores\" \"users\"\n\tArray.append highScores.Usernames usernames // Now instead of overwriting the all usernames we add to the array.\n\tupdateDocument highScores.CollectionId highScores.Id highScores\n\n// Now let's mount our created listener callback to a query.\n// We only want highscores that are above 1000, to be neat we also order them.\nlet listener =\n\tcollection \"scores\"\n\t|\u003e whereGreaterThen \"BestScore\" 1000\n\t|\u003e orderBy \"BestScore\"\n\t|\u003e listenOnQuery callback\n\n// If we want to stop listening we just stop listening.\nstopListening listener\n```\n\n## 2. Bugs and Features\n\nPlease open a issue when you encounter any bugs 🐞 or have an idea for a new feature 💡.\n\n## 3. Buy me a coffee\n\nIf you like you can buy me a coffee:\n\n[![Support via PayPal](https://cdn.rawgit.com/twolfson/paypal-github-button/1.0.0/dist/button.svg)](https://www.paypal.me/mrbandler/)\n\n[![Donate with Bitcoin](https://en.cryptobadges.io/badge/big/3LTBGYAHQCDE4ZbEiTreJjzgnsDhY6X2D2)](https://en.cryptobadges.io/donate/3LTBGYAHQCDE4ZbEiTreJjzgnsDhY6X2D2)\n\n[![Donate with Litecoin](https://en.cryptobadges.io/badge/big/LcHsJH13A8PmHJQwpbWevGUebZwhWNMXgS)](https://en.cryptobadges.io/donate/LcHsJH13A8PmHJQwpbWevGUebZwhWNMXgS)\n\n[![Donate with Ethereum](https://en.cryptobadges.io/badge/big/0x54499ee409687E9C43589693093D004a0cbfEE72)](https://en.cryptobadges.io/donate/0x54499ee409687E9C43589693093D004a0cbfEE72)\n\n---\n\n## 4. License\n\nMIT License\n\nCopyright (c) 2018 fivefingergames\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmrbandler%2FFsFirestore","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmrbandler%2FFsFirestore","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmrbandler%2FFsFirestore/lists"}