swadhinbiswas feat: add src/db/adapter/firestore.ts · 2/18/2026
91332f2
177 lines · 5.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
import {
  BaseDatabaseAdapter,
  ColumnType,
  DatabaseConfig,
  Migration,
  MigrationStatus,
  QueryOptions,
  QueryResult,
  TableAlterCommand,
  TableSchema,
  WhereClause,
} from "./types";

const dynamicImport = new Function("m", "return import(m)") as (m: string) => Promise<any>;

function applyQuery(ref: any, where?: WhereClause[]) {
  let query = ref;
  for (const clause of where || []) {
    const opMap: Record<string, string> = {
      "=": "==",
      eq: "==",
      "!=": "!=",
      ne: "!=",
      ">": ">",
      gt: ">",
      ">=": ">=",
      gte: ">=",
      "<": "<",
      lt: "<",
      "<=": "<=",
      lte: "<=",
      in: "in",
      "not in": "not-in",
    };
    const op = opMap[clause.operator];
    if (!op) continue;
    query = query.where(clause.field, op, clause.value);
  }
  return query;
}

export class FirestoreAdapter extends BaseDatabaseAdapter {
  private db: any;

  async connect(): Promise<void> {
    if (this.connected) return;
    const mod = await dynamicImport("@google-cloud/firestore");
    const Firestore = mod.Firestore;
    this.db = new Firestore({
      projectId: this.config.database || process.env.GOOGLE_CLOUD_PROJECT,
      keyFilename: this.config.options?.keyFilename as string | undefined,
    });
    this.connected = true;
  }

  async disconnect(): Promise<void> {
    this.connected = false;
  }

  async findOne<T>(table: string, options?: QueryOptions): Promise<T | null> {
    const rows = await this.findMany<T>(table, { ...options, limit: 1 });
    return rows[0] || null;
  }

  async findMany<T>(table: string, options?: QueryOptions): Promise<T[]> {
    let query = applyQuery(this.db.collection(table), options?.where);
    if (options?.orderBy?.length) {
      for (const o of options.orderBy) query = query.orderBy(o.field, o.direction);
    }
    if (options?.limit) query = query.limit(options.limit);
    if (options?.offset) query = query.offset(options.offset);
    const snap = await query.get();
    return snap.docs.map((d: any) => ({ id: d.id, ...d.data() })) as T[];
  }

  async create<T>(table: string, data: Partial<T>): Promise<T> {
    const id = (data as any).id as string | undefined;
    const docData = { ...(data as any) };
    delete docData.id;
    if (id) {
      await this.db.collection(table).doc(id).set(docData);
      return { id, ...docData } as T;
    }
    const ref = await this.db.collection(table).add(docData);
    return { id: ref.id, ...docData } as T;
  }

  async createMany<T>(table: string, data: Partial<T>[]): Promise<T[]> {
    const out: T[] = [];
    for (const item of data) out.push(await this.create<T>(table, item));
    return out;
  }

  async update<T>(table: string, id: string | number, data: Partial<T>): Promise<T | null> {
    const ref = this.db.collection(table).doc(String(id));
    const doc = await ref.get();
    if (!doc.exists) return null;
    await ref.set(data as any, { merge: true });
    const next = await ref.get();
    return { id: next.id, ...next.data() } as T;
  }

  async updateMany<T>(table: string, options: QueryOptions, data: Partial<T>): Promise<number> {
    const rows = await this.findMany<any>(table, options);
    for (const row of rows) {
      await this.db.collection(table).doc(String(row.id)).set(data as any, { merge: true });
    }
    return rows.length;
  }

  async delete(table: string, id: string | number): Promise<boolean> {
    const ref = this.db.collection(table).doc(String(id));
    const snap = await ref.get();
    if (!snap.exists) return false;
    await ref.delete();
    return true;
  }

  async deleteMany(table: string, options?: QueryOptions): Promise<number> {
    const rows = await this.findMany<any>(table, options);
    for (const row of rows) {
      await this.db.collection(table).doc(String(row.id)).delete();
    }
    return rows.length;
  }

  async count(table: string, options?: QueryOptions): Promise<number> {
    const rows = await this.findMany<any>(table, options);
    return rows.length;
  }

  async rawQuery<T>(query: string, params?: unknown[]): Promise<QueryResult<T>> {
    throw new Error("Firestore adapter does not support SQL rawQuery");
  }

  async beginTransaction(): Promise<void> {}
  async commitTransaction(): Promise<void> {}
  async rollbackTransaction(): Promise<void> {}

  async tableExists(table: string): Promise<boolean> {
    const snap = await this.db.collection(table).limit(1).get();
    return !snap.empty;
  }

  async createTable(table: string, schema: TableSchema): Promise<void> {}
  async dropTable(table: string): Promise<void> {
    const rows = await this.findMany<any>(table);
    for (const row of rows) {
      await this.db.collection(table).doc(String(row.id)).delete();
    }
  }
  async alterTable(table: string, changes: TableAlterCommand[]): Promise<void> {}

  async runMigration(migration: Migration): Promise<void> {
    await migration.up(this);
    await this.db.collection("_migrations").doc(migration.id).set({
      id: migration.id,
      name: migration.name,
      executedAt: new Date().toISOString(),
    });
  }

  async getMigrationStatus(): Promise<MigrationStatus[]> {
    const snap = await this.db.collection("_migrations").get();
    return snap.docs.map((d: any) => ({
      id: d.id,
      name: d.data().name || d.id,
      executedAt: d.data().executedAt ? new Date(d.data().executedAt) : null,
      status: d.data().executedAt ? "executed" : "pending",
    }));
  }

  protected mapColumnType(type: ColumnType): string {
    return type;
  }
}