@dataql/firebase-adapter
Version:
Firebase adapter for DataQL with zero API changes
495 lines (428 loc) • 12.6 kB
text/typescript
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,
};