UNPKG

@dataql/firebase-adapter

Version:

Firebase adapter for DataQL with zero API changes

495 lines (428 loc) 12.6 kB
import { Data, DataOptions } from "@dataql/core"; // Firebase configuration and types export interface FirebaseConfig { apiKey: string; authDomain: string; projectId: string; storageBucket?: string; messagingSenderId?: string; appId: string; measurementId?: string; } // DataQL configuration for Firebase adapter export interface DataQLFirebaseOptions { // Required for DataQL infrastructure routing appToken?: string; // Database name for client isolation dbName?: string; // Environment routing env?: "dev" | "prod"; // Development prefix devPrefix?: string; // Optional custom connection customConnection?: any; // Firebase-specific options firebase?: FirebaseConfig; } // Firestore-like types export interface DocumentSnapshot<T = any> { id: string; exists: boolean; data(): T | undefined; get(field: string): any; } export interface QuerySnapshot<T = any> { empty: boolean; size: number; docs: QueryDocumentSnapshot<T>[]; forEach(callback: (doc: QueryDocumentSnapshot<T>) => void): void; } export interface QueryDocumentSnapshot<T = any> { id: string; data(): T; get(field: string): any; } export interface WriteResult { writeTime: string; } export interface DocumentReference<T = any> { id: string; path: string; get(): Promise<DocumentSnapshot<T>>; set(data: T, options?: SetOptions): Promise<WriteResult>; update(data: Partial<T>): Promise<WriteResult>; delete(): Promise<WriteResult>; onSnapshot( onNext: (snapshot: DocumentSnapshot<T>) => void, onError?: (error: Error) => void ): () => void; } export interface SetOptions { merge?: boolean; } export type WhereFilterOp = | "==" | "!=" | "<" | "<=" | ">" | ">=" | "array-contains" | "in" | "array-contains-any"; export type OrderByDirection = "asc" | "desc"; export interface Query<T = any> { where(fieldPath: string, opStr: WhereFilterOp, value: any): Query<T>; orderBy(fieldPath: string, directionStr?: OrderByDirection): Query<T>; limit(limit: number): Query<T>; get(): Promise<QuerySnapshot<T>>; onSnapshot( onNext: (snapshot: QuerySnapshot<T>) => void, onError?: (error: Error) => void ): () => void; } export interface CollectionReference<T = any> extends Query<T> { id: string; path: string; doc(documentPath?: string): DocumentReference<T>; add(data: T): Promise<DocumentReference<T>>; } export interface Firestore { collection(collectionPath: string): CollectionReference; doc(documentPath: string): DocumentReference; } export interface FirebaseApp { name: string; options: FirebaseConfig; } // Implementation classes class DataQLDocumentSnapshot<T = any> implements DocumentSnapshot<T> { constructor( public id: string, private _data: T | null, public exists: boolean ) {} data(): T | undefined { return this._data || undefined; } get(field: string): any { return this._data && typeof this._data === "object" ? (this._data as any)[field] : undefined; } } class DataQLQueryDocumentSnapshot<T = any> implements QueryDocumentSnapshot<T> { constructor( public id: string, private _data: T ) {} data(): T { return this._data; } get(field: string): any { return typeof this._data === "object" ? (this._data as any)[field] : undefined; } } class DataQLQuerySnapshot<T = any> implements QuerySnapshot<T> { public docs: QueryDocumentSnapshot<T>[]; public empty: boolean; public size: number; constructor(documents: Array<{ id: string; data: T }>) { this.docs = documents.map( (doc) => new DataQLQueryDocumentSnapshot(doc.id, doc.data) ); this.empty = documents.length === 0; this.size = documents.length; } forEach(callback: (doc: QueryDocumentSnapshot<T>) => void): void { this.docs.forEach(callback); } } class DataQLDocumentReference<T = any> implements DocumentReference<T> { constructor( public id: string, public path: string, private _data: Data, private _collectionName: string ) {} private get _collection() { return this._data.collection( this._collectionName, this._getDefaultSchema() ); } private _getDefaultSchema() { return { id: { type: "ID", required: true }, createdAt: { type: "Date", default: "now" }, updatedAt: { type: "Date", default: "now" }, }; } async get(): Promise<DocumentSnapshot<T>> { try { const results = await this._collection.find({ id: this.id }); const result = results[0] || null; return new DataQLDocumentSnapshot(this.id, result as T, !!result); } catch (error) { return new DataQLDocumentSnapshot<T>(this.id, null as T, false); } } async set(data: T, options?: SetOptions): Promise<WriteResult> { try { if (options?.merge) { await this._collection.upsert({ id: this.id, ...data }); } else { await this._collection.create({ id: this.id, ...data }); } return { writeTime: new Date().toISOString() }; } catch (error) { throw new Error( `Failed to set document: ${error instanceof Error ? error.message : "Unknown error"}` ); } } async update(data: Partial<T>): Promise<WriteResult> { try { await this._collection.update({ id: this.id }, data); return { writeTime: new Date().toISOString() }; } catch (error) { throw new Error( `Failed to update document: ${error instanceof Error ? error.message : "Unknown error"}` ); } } async delete(): Promise<WriteResult> { try { await this._collection.delete({ id: this.id }); return { writeTime: new Date().toISOString() }; } catch (error) { throw new Error( `Failed to delete document: ${error instanceof Error ? error.message : "Unknown error"}` ); } } onSnapshot( onNext: (snapshot: DocumentSnapshot<T>) => void, onError?: (error: Error) => void ): () => void { console.log(`Subscribed to document ${this.path}`); return () => { console.log(`Unsubscribed from document ${this.path}`); }; } } class DataQLQuery<T = any> implements Query<T> { protected _filters: Array<{ field: string; operator: WhereFilterOp; value: any; }> = []; protected _orderBy?: { field: string; direction: OrderByDirection }; protected _limit?: number; constructor( protected _data: Data, protected _collectionName: string ) {} private get _collection() { return this._data.collection( this._collectionName, this._getDefaultSchema() ); } private _getDefaultSchema() { return { id: { type: "ID", required: true }, createdAt: { type: "Date", default: "now" }, updatedAt: { type: "Date", default: "now" }, }; } where(fieldPath: string, opStr: WhereFilterOp, value: any): Query<T> { const newQuery = new DataQLQuery<T>(this._data, this._collectionName); newQuery._filters = [ ...this._filters, { field: fieldPath, operator: opStr, value }, ]; newQuery._orderBy = this._orderBy; newQuery._limit = this._limit; return newQuery; } orderBy(fieldPath: string, directionStr?: OrderByDirection): Query<T> { const newQuery = new DataQLQuery<T>(this._data, this._collectionName); newQuery._filters = [...this._filters]; newQuery._orderBy = { field: fieldPath, direction: directionStr || "asc" }; newQuery._limit = this._limit; return newQuery; } limit(limit: number): Query<T> { const newQuery = new DataQLQuery<T>(this._data, this._collectionName); newQuery._filters = [...this._filters]; newQuery._orderBy = this._orderBy; newQuery._limit = limit; return newQuery; } private _buildDataQLFilter(): any { const filter: any = {}; for (const filterItem of this._filters) { const { field, operator, value } = filterItem; switch (operator) { case "==": filter[field] = value; break; case "!=": filter[field] = { $ne: value }; break; case "<": filter[field] = { $lt: value }; break; case "<=": filter[field] = { $lte: value }; break; case ">": filter[field] = { $gt: value }; break; case ">=": filter[field] = { $gte: value }; break; case "array-contains": filter[field] = { $in: [value] }; break; case "in": filter[field] = { $in: value }; break; case "array-contains-any": filter[field] = { $in: value }; break; default: filter[field] = value; } } return filter; } private _applySort(results: any[]): any[] { if (!this._orderBy) return results; const { field, direction } = this._orderBy; return results.sort((a, b) => { const aVal = a[field]; const bVal = b[field]; let comparison = 0; if (aVal < bVal) comparison = -1; else if (aVal > bVal) comparison = 1; return direction === "desc" ? -comparison : comparison; }); } async get(): Promise<QuerySnapshot<T>> { try { const filter = this._buildDataQLFilter(); let results = await this._collection.find(filter); results = this._applySort(results); if (this._limit) { results = results.slice(0, this._limit); } const documents = results.map((item: any) => ({ id: item.id || Math.random().toString(36).substr(2, 9), data: item, })); return new DataQLQuerySnapshot<T>(documents); } catch (error) { throw new Error( `Query failed: ${error instanceof Error ? error.message : "Unknown error"}` ); } } onSnapshot( onNext: (snapshot: QuerySnapshot<T>) => void, onError?: (error: Error) => void ): () => void { console.log(`Subscribed to collection ${this._collectionName} query`); return () => { console.log(`Unsubscribed from collection ${this._collectionName} query`); }; } } class DataQLCollectionReference<T = any> extends DataQLQuery<T> implements CollectionReference<T> { constructor( public id: string, public path: string, data: Data ) { super(data, id); } doc(documentPath?: string): DocumentReference<T> { const docId = documentPath || Math.random().toString(36).substr(2, 9); return new DataQLDocumentReference<T>( docId, `${this.path}/${docId}`, this._data, this.id ); } async add(data: T): Promise<DocumentReference<T>> { const docId = Math.random().toString(36).substr(2, 9); const docRef = this.doc(docId); await docRef.set(data); return docRef; } } class DataQLFirestore implements Firestore { constructor(private _data: Data) {} collection(collectionPath: string): CollectionReference { return new DataQLCollectionReference( collectionPath, collectionPath, this._data ); } doc(documentPath: string): DocumentReference { const pathParts = documentPath.split("/"); const collectionName = pathParts[0]; const docId = pathParts[1]; return new DataQLDocumentReference( docId, documentPath, this._data, collectionName ); } } class DataQLFirebaseApp implements FirebaseApp { public firestore: Firestore; constructor( public name: string, public options: FirebaseConfig, private _data: Data ) { this.firestore = new DataQLFirestore(_data); } } // Main Firebase functions export function initializeApp( config: FirebaseConfig, options?: DataQLFirebaseOptions ): FirebaseApp { // Ensure proper DataQL infrastructure routing (Client → Worker → Lambda → Database) const dataqlOptions = { appToken: options?.appToken || "default", // Required for DataQL routing env: options?.env || "prod", // Routes to production infrastructure devPrefix: options?.devPrefix || "dev_", // Optional prefix for dev environments dbName: options?.dbName, // Database isolation per client customConnection: options?.customConnection, // Optional custom connection }; const data = new Data(dataqlOptions); return new DataQLFirebaseApp("default", config, data); } export function getFirestore(app?: FirebaseApp): Firestore { if (!app) { throw new Error("Firebase app not initialized"); } return (app as DataQLFirebaseApp).firestore; } // Default export export default { initializeApp, getFirestore, };