@dill-pixel/storage-adapter-firebase
Version:
Firebase Storage Adapter
362 lines (307 loc) • 11.7 kB
text/typescript
import { Application, IApplication, isDev, IStorageAdapter, Logger, StorageAdapter } from 'dill-pixel';
import type { FirebaseApp, FirebaseOptions } from 'firebase/app';
import { initializeApp } from 'firebase/app';
import type { DocumentData, Firestore, QueryConstraint } from 'firebase/firestore';
import {
addDoc,
collection,
deleteDoc,
doc,
getDoc,
getDocs,
getFirestore,
query,
setDoc,
where,
} from 'firebase/firestore';
import { firebaseVersion, version } from './version';
interface DocumentResult extends DocumentData {
id: string;
[key: string]: unknown;
}
export interface IFirebaseAdapter extends IStorageAdapter<Application, FirebaseOptions> {
db: Firestore;
firebaseApp: FirebaseApp;
initialize(options: Partial<FirebaseOptions>, app: IApplication): void;
save<DocumentResult>(collectionName: string, data: DocumentData, id?: string): Promise<DocumentResult>;
getDocumentById(collectionName: string, id: string): Promise<DocumentResult | null>;
getDocumentWhere(collectionName: string, field: string, value: unknown): Promise<DocumentResult | null>;
getCollection(collectionName: string): Promise<DocumentResult[]>;
deleteDocumentById(collectionName: string, id: string): Promise<DocumentResult | null>;
deleteDocumentWhere(collectionName: string, field: string, value: unknown): Promise<DocumentResult | null>;
deleteCollection(collectionName: string): Promise<void>;
queryCollection(collectionName: string, ...queries: QueryConstraint[]): Promise<DocumentResult[]>;
}
interface IFirebaseAdapterOptions extends FirebaseOptions {
debug?: boolean;
}
/**
* A class representing a storage adapter that uses Firebase.
* @extends StorageAdapter
*/
export class FirebaseAdapter extends StorageAdapter implements IFirebaseAdapter {
private _options: IFirebaseAdapterOptions;
private _firebaseApp: FirebaseApp;
/**
* Returns the Firebase app.
* @returns {FirebaseApp} The Firebase app.
*/
get firebaseApp(): FirebaseApp {
return this._firebaseApp;
}
private _db: Firestore;
/**
* Returns the Firestore database.
* @returns {Firestore} The Firestore database.
*/
get db() {
return this._db;
}
private hello() {
const hello = `%c Dill Pixel Firebase Storage Adapter v${version} | %cFirebase v${firebaseVersion}`;
console.log(
hello,
'background: rgba(31, 41, 55, 1);color: #74b64c',
'background: rgba(31, 41, 55, 1);color: #e91e63',
'background: rgba(31, 41, 55, 1);color: #74b64c',
);
if (this._options.debug) {
Logger.log(this._options);
}
}
/**
* Initializes the adapter.
* @param {IApplication} _app The application that the adapter belongs to.
* @param {FirebaseOptions} options The options to initialize the adapter with.
* @returns {void}
*/
public initialize(options: Partial<IFirebaseAdapterOptions>, _app: IApplication): void {
const defaultConfig: IFirebaseAdapterOptions = {
debug: isDev,
apiKey: _app.env.VITE_FIREBASE_API_KEY || _app.env.FIREBASE_API_KEY,
authDomain: _app.env.VITE_FIREBASE_AUTH_DOMAIN || _app.env.FIREBASE_AUTH_DOMAIN,
projectId: _app.env.VITE_FIREBASE_PROJECT_ID || _app.env.FIREBASE_PROJECT_ID,
storageBucket: _app.env.VITE_FIREBASE_STORAGE_BUCKET || _app.env.FIREBASE_STORAGE_BUCKET,
messagingSenderId: _app.env.VITE_FIREBASE_MESSAGING_SENDER_ID || _app.env.FIREBASE_MESSAGING_SENDER_ID,
appId: _app.env.VITE_FIREBASE_APP_ID || _app.env.FIREBASE_APP_ID,
};
this._options = { ...defaultConfig, ...options };
this._firebaseApp = initializeApp(this._options);
this._db = getFirestore(this._firebaseApp); // initialize Firestore and get a reference to the database
this.hello();
}
/**
* Save or update a document in a collection.
* @param {string} collectionName The name of the collection.
* @param {DocumentData} data The data to save or update.
* @param {string} id The ID of the document to save or update, if applicable.
* @returns {Promise<DocumentResult>} The saved or updated document.
*
* @example
* await this.app.firebase.save('users', { username: 'relish', score: 50 }, 'custom-id');
*/
async save<DocumentResult>(collectionName: string, data: DocumentData, id?: string): Promise<DocumentResult> {
if (!this.db) {
throw new Error('Firestore has not been initialized. Call initialize() first.');
}
let docRef;
try {
if (id) {
docRef = doc(this.db, collectionName, id);
} else {
docRef = await addDoc(collection(this.db, collectionName), data);
}
// If the document does not exist, it will be created.
// If the document does exist, the data will be merged into the existing document.
await setDoc(docRef, data, { merge: true });
const docSnap = await getDoc(docRef);
return {
id: docSnap.id,
...docSnap.data(),
} as DocumentResult;
} catch (error) {
throw new Error(`Error saving document: ${error}`);
}
}
/**
* Get a single document by its ID.
* @param {string} collectionName The name of the collection.
* @param {string} id The ID of the document to get.
* @returns {Promise<DocumentResult | null>} The document, or null if not found.
*
* @example
* await this.app.firebase.getDocumentById('users', 'custom-id');
*/
async getDocumentById(collectionName: string, id: string): Promise<DocumentResult | null> {
if (!this.db) {
throw new Error('Firestore has not been initialized. Call initialize() first.');
}
const docRef = doc(this.db, collectionName, id);
try {
const docSnap = await getDoc(docRef);
if (!docSnap.exists()) return null;
const data = {
id: docSnap.id,
...docSnap.data(),
};
if (!data) return null;
return data;
} catch (error) {
throw new Error(`Error getting document: ${error}`);
}
}
/**
* Get a single document by a field value.
* @param {string} collectionName The name of the collection.
* @param {string} field The field to query.
* @param {unknown} value The value to query.
* @returns {Promise<DocumentResult | null>} The document, or null if not found.
*
* @example
* await this.app.firebase.getDocumentByField('users', 'username', 'relish');
*/
async getDocumentWhere(collectionName: string, field: string, value: unknown): Promise<DocumentResult | null> {
if (!this.db) {
throw new Error('Firestore has not been initialized. Call initialize() first.');
}
try {
const collectionRef = collection(this.db, collectionName);
const q = query(collectionRef, where(field, '==', value));
const querySnapshot = await getDocs(q);
if (!querySnapshot.empty) {
const doc = querySnapshot.docs[0];
return { id: doc.id, ...doc.data() };
} else {
return null;
}
} catch (error) {
throw new Error(`Error getting document: ${error}`);
}
}
/**
* Get all documents in a collection.
* @param {string} collectionName The name of the collection.
* @returns {Promise<DocumentResult[]>} An array of documents.
*
* @example
* await this.app.firebase.getCollection('users');
*/
async getCollection(collectionName: string): Promise<DocumentResult[]> {
if (!this.db) {
throw new Error('Firestore has not been initialized. Call initialize() first.');
}
try {
const collectionRef = collection(this.db, collectionName);
const querySnapshot = await getDocs(collectionRef);
const documents: DocumentResult[] = [];
querySnapshot.forEach((doc) => {
documents.push({ id: doc.id, ...doc.data() });
});
return documents;
} catch (error) {
throw new Error(`Error getting collection: ${error}`);
}
}
/**
* Delete a document by its ID.
* @param {string} collectionName The name of the collection.
* @param {string} id The ID of the document to delete.
* @returns {Promise<DocumentResult | null>} The deleted document, or null if not found.
*
* @example
* await this.app.firebase.deleteDocumentById('users', 'custom-id');
*/
async deleteDocumentById(collectionName: string, id: string): Promise<DocumentResult | null> {
if (!this.db) {
throw new Error('Firestore has not been initialized. Call initialize() first.');
}
try {
const docRef = doc(this.db, collectionName, id);
const docToDelete = await getDoc(docRef);
if (!docToDelete.exists()) {
return null;
}
await deleteDoc(docRef);
return {
id: docToDelete.id,
...docToDelete.data(),
};
} catch (error) {
throw new Error(`Error deleting document: ${error}`);
}
}
/**
* Delete a document by a field value.
* @param {string} collectionName The name of the collection.
* @param {string} field The field to query.
* @param {unknown} value The value to query.
* @returns {Promise<DocumentResult | null>} The deleted document, or null if not found.
*
* @example
* await this.app.firebase.deleteDocumentByField('users', 'username', 'relish');
*/
async deleteDocumentWhere(collectionName: string, field: string, value: unknown): Promise<DocumentResult | null> {
if (!this.db) {
throw new Error('Firestore has not been initialized. Call initialize() first.');
}
try {
const docToDelete = await this.getDocumentWhere(collectionName, field, value);
if (!docToDelete) return null;
const docRef = doc(this.db, collectionName, docToDelete.id);
await deleteDoc(docRef);
return docToDelete;
} catch (error) {
throw new Error(`Error deleting document: ${error}`);
}
}
/**
* Delete all documents in a collection.
* @param {string} collectionName The name of the collection.
* @returns {Promise<void>} A promise that resolves when the operation is complete.
*
* @example
* await this.app.firebase.deleteCollection('users');
*/
async deleteCollection(collectionName: string): Promise<void> {
if (!this.db) {
throw new Error('Firestore has not been initialized. Call initialize() first.');
}
try {
const collectionRef = collection(this.db, collectionName);
const querySnapshot = await getDocs(collectionRef);
const docsToDelete: Promise<void>[] = [];
querySnapshot.forEach((doc) => {
docsToDelete.push(deleteDoc(doc.ref));
});
await Promise.all(docsToDelete);
} catch (error) {
throw new Error(`Error deleting collection: ${error}`);
}
}
/**
* Query a collection.
* @param {string} collectionName The name of the collection.
* @param {QueryConstraint[]} queries The query constraints to apply.
* @returns {Promise<DocumentResult[]>} An array of documents.
*
* @example
* await this.app.firebase.queryCollection('users', where('score', '>', 0), limit(10));
*/
async queryCollection(collectionName: string, ...queries: QueryConstraint[]): Promise<DocumentResult[]> {
if (!this.db) {
throw new Error('Firestore has not been initialized. Call initialize() first.');
}
const documents: DocumentResult[] = [];
try {
const collectionRef = collection(this.db, collectionName);
const q = query(collectionRef, ...queries);
const querySnapshot = await getDocs(q);
querySnapshot.forEach((doc) => {
documents.push({ id: doc.id, ...doc.data() });
});
return documents;
} catch (error) {
throw new Error(`Error querying collection: ${error}`);
}
}
}