Skip to content

Commit

Permalink
feat: use firestore db and admin singletons stored as private fields …
Browse files Browse the repository at this point in the history
…in a class, #28
  • Loading branch information
ciatph committed Feb 10, 2025
1 parent c89f604 commit 209357d
Show file tree
Hide file tree
Showing 6 changed files with 200 additions and 51 deletions.
2 changes: 1 addition & 1 deletion examples/example-parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const main = async () => {
const handler = new CsvToFireStore(path.resolve(__dirname, 'example.csv'))

// Directly override CsvToFireStore's ParserCSV read() method
// and csv_rows[] {Object[]} array to include only the "name" column
// and csv_rows[] {Object[]} array to include only the "name" column
// during Firestore upload
handler.read = (row) => {
handler.csv_rows.push({
Expand Down
4 changes: 3 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ require('dotenv').config()
const ParserCSV = require('./src/lib/classes/parser')
const FirestoreData = require('./src/lib/classes/firestore-data/firestore-data')
const CsvToFireStore = require('./src/lib/classes/csvtofirestore')
const FirebaseDB = require('./src/lib/classes/firebasedb')

module.exports = {
CsvToFireStore,
ParserCSV,
FirestoreData
FirestoreData,
FirebaseDB
}
28 changes: 21 additions & 7 deletions src/lib/classes/csvtofirestore/index.js
Original file line number Diff line number Diff line change
@@ -1,23 +1,37 @@
const ParserCSV = require('../parser')
const { uploadToCollection } = require('../firestore-data')
const FirestoreData = require('../firestore-data/firestore-data')

/**
* Read the contents of a CSV file and
* upload the CSV contents to a Firestore collection.
* Class that reads the contents of a CSV file and
* uploads the CSV contents to a Firestore collection.
*/
class CsvToFireStore extends ParserCSV {
/**
* Upload the internal CSV file contents to a Firestore collection.
* The Firestore collection will be created if it does not yet exist.
* `FirestoreData` class instance containing methods for managing Firestore data.
* It also contains internal Firebase `admin` and `firestore` Singletons.
* @type {object}
*/
#firestore

constructor (csvFilePath) {
super(csvFilePath)
this.#firestore = new FirestoreData()
}

/**
* Uploads the internal CSV file contents, or external data that resembles `ParserCSV`'s
* JSON Object[] array structure to a Firestore collection, creating the Firestore
* collection if it does not yet exist.
* @param {String} collectionName - Firestore collection name
* @param {Boolean} overwrite - delete all documents in the collection before uploading data
* @param {Boolean} overwrite - Flag to delete all documents in the collection before uploading data
* @param {Object[]} data - Array of objects (1 level only) to upload
*/
async firestoreUpload (collectionName, overwrite = true, data = []) {
const dataToUpload = (data && data.length > 0)
? data
: this.data()
await uploadToCollection(collectionName, dataToUpload, overwrite)

await this.#firestore.uploadToCollection(collectionName, dataToUpload, overwrite)
}
}

Expand Down
160 changes: 160 additions & 0 deletions src/lib/classes/firebasedb/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
require('dotenv').config()
const { initializeApp, getApps, getApp } = require('firebase-admin/app')
const { getFirestore } = require('firebase-admin/firestore')
const admin = require('firebase-admin')

/**
* @class FirebaseDB
* @description Class that provides a Singleton instance of the Firebase `admin` and `firestore` database objects
* initialized using the service account credentials in the `.env` file. It also displays relevant firebase-admin
* and Firestore database information.
*/
class FirebaseDB {
/**
* csv-firestore internal Firebase admin namespace
* @type {object}
*/
#admin

/**
* Firestore database object namespace, containing methods for
* interfacing with the Firestore database.
* @type {object}
*/
#db

/**
* Initialized Fireabase app name
* @type {string}
*/
#appName

/**
* Minimal Firestore database details (also available in `#db`)
* @type {object}
*/
#dbSettings = {
/**
* Firestore database ID (name)
* @type {string}
*/
databaseId: null,
/**
* Firestore project ID
* @type {string}
*/
projectId: null
}

/**
* Version of the firebase-admin package that created the `#admin` and firestore `#db` Singletons
* @type {string}
*/
#sdkVersion

/**
* Creates an instance of the `FirebaseDB` class and intializes or uses and existing
* Firebase `admin` and `firestore` database Singleton using the firebase-admin
* `getApp()` and `getApps()` functions.
* @constructor
*/
constructor () {
this.initialize()
}

/**
* Initializes the Firebase `admin` and Firestore `database` using the Firebase credentials in the `.env` file.
* Ensures Singleton Firebase app instances by checking initialized Firebase apps using the `getApps()` and `getApp()`
* firebase-admin functions.
* @param {string} [appName] (Optional) Firebase app name to tag a Firebase app instance.
* @returns {void}
*/
initialize () {
const appsLength = getApps().length

if (!appsLength) {
if (process.env.FIREBASE_SERVICE_ACC === undefined || process.env.FIREBASE_PRIVATE_KEY === undefined) {
console.log('FIREBASE_SERVICE_ACC or FIREBASE_PRIVATE_KEY is missing.')
process.exit(1)
}

// Add double-quotes around the "private_key" JSON
const serviceAccount = JSON.parse(process.env.FIREBASE_SERVICE_ACC)
serviceAccount.private_key = process.env.FIREBASE_PRIVATE_KEY.replace(/\\n/g, '\n')

// Initialize a "[DEFAULT]" Firebase app
// Developers expect only one (1) app instance per firebase-admin version
initializeApp({
credential: admin.credential.cert(serviceAccount)
})
}

// Use existing app instance
const app = getApp()

this.#db = getFirestore(app)
this.#admin = admin
this.#appName = app.name
this.#sdkVersion = admin.SDK_VERSION

this.#dbSettings = {
databaseId: this.#db.databaseId,
projectId: this.#db.projectId
}
}

/**
* Prints relevant firebase-admin (firestore, app) information to screen.
* @returns {void}
*/
log () {
let log = `Firebase app name: ${this.#appName}\n`
log += `Firestore database ID: ${this.#dbSettings.databaseId}\n`
log += `Firestore project ID: ${this.#dbSettings.projectId}\n`
log += `SDK Version: ${this.sdkVersion}`

console.log(log)
}

/**
* Firestore admin getter
* @returns {object} Inialized Firebase admin object namespace
*/
get admin () {
return this.#admin
}

/**
* Firestore database getter
* @returns {object} Inialized Firestore database object
*/
get db () {
return this.#db
}

/**
* Firebase app name getter
* @returns {string} Initialized Firebase app name
*/
get appName () {
return this.#appName
}

/**
* firebase-admin SDK version getter
* @returns {string} firebase-admin SDK version
*/
get sdkVersion () {
return this.#sdkVersion
}

/**
* Firestore database settings getter
* @returns {object} Minimal Firestore database settings object
*/
get dbSettings () {
return this.#dbSettings
}
}

module.exports = FirebaseDB
49 changes: 11 additions & 38 deletions src/lib/classes/firestore-data/firestore-data.js
Original file line number Diff line number Diff line change
@@ -1,32 +1,19 @@
const { db, admin } = require('./db')
const FirebaseDB = require('../firebasedb')

/**
* A wrapper around firebase-admin for bulk delete and write data operations to specified Firestore collections.
* Uses firebase-admin v10.0.2.
* Requires a privileged environment to run while using the project's
* service account JSON credentials (see db.js)
* Initializes and keeps a `firestore` and `admin` Singleton reference from firebase-admin for data access.
* Uses firebase-admin v13.1.0.
* Requires a privileged environment to run using the project's
* service account JSON credentials (see .env file)
*/
class FirestoreData {
/** Firestore DB */
#db

/** Firebase admin */
#admin

/**
* Initialize FirestoreData with Firestore DB and Firebase admin
*/
constructor () {
this.#db = db
this.#admin = admin
}

class FirestoreData extends FirebaseDB {
/**
* Delete a firestore collection including all its documents
* @param {String} collectionName - firestore collection name
*/
async deleteCollection (collectionName) {
const collectionRef = db.collection(collectionName)
const collectionRef = this.db.collection(collectionName)
const query = collectionRef.offset(0)
// const query = collectionRef.orderBy(fieldName)

Expand All @@ -51,7 +38,7 @@ class FirestoreData {
}

// Delete documents in a batch
const batch = db.batch()
const batch = this.db.batch()
snapshot.docs.forEach((doc) => {
batch.delete(doc.ref)
})
Expand All @@ -72,14 +59,14 @@ class FirestoreData {
* @param {Boolean} overwrite - delete all documents in the collection before uploading data
*/
async uploadToCollection (collectionName, data, overwrite = true) {
const batch = db.batch()
const batch = this.db.batch()

if (overwrite) {
await this.deleteCollection(collectionName)
}

data.forEach((item, index) => {
const docRef = db.collection(collectionName).doc()
const docRef = this.db.collection(collectionName).doc()
batch.set(docRef, item)
})

Expand All @@ -93,7 +80,7 @@ class FirestoreData {
* @returns {Boolean} true|false
*/
async fieldNameExists (collectionName, fieldName) {
const collectionRef = db.collection(collectionName)
const collectionRef = this.db.collection(collectionName)
const query = collectionRef.orderBy(fieldName).limit(1)

try {
Expand All @@ -103,20 +90,6 @@ class FirestoreData {
throw new Error(err.message)
}
}

/**
* Return the private Firestore DB
*/
get db () {
return this.#db
}

/**
* Return the private Firbase admin
*/
get admin () {
return this.#admin
}
}

module.exports = FirestoreData
8 changes: 4 additions & 4 deletions src/lib/classes/firestore-data/db.js → src/lib/utils/db.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ const { initializeApp, getApps, getApp } = require('firebase-admin/app')
const { getFirestore } = require('firebase-admin/firestore')
const admin = require('firebase-admin')

// Modular Singleton Firebase app initialization
if (!getApps().length) {
if (process.env.FIREBASE_SERVICE_ACC === undefined || process.env.FIREBASE_PRIVATE_KEY === undefined) {
console.log('FIREBASE_SERVICE_ACC or FIREBASE_PRIVATE_KEY is missing.')
Expand All @@ -13,10 +14,9 @@ if (!getApps().length) {
const serviceAccount = JSON.parse(process.env.FIREBASE_SERVICE_ACC)
serviceAccount.private_key = process.env.FIREBASE_PRIVATE_KEY.replace(/\\n/g, '\n')

initializeApp(
{ credential: admin.credential.cert(serviceAccount) },
'csv-firestore-app'
)
initializeApp({
credential: admin.credential.cert(serviceAccount)
})
}

// Use existing app instance
Expand Down

0 comments on commit 209357d

Please sign in to comment.