feat: add src/db/adapter/firestore.ts

swadhinbiswas committed 8 months ago
91332f2
1 parent 35fcda6
commit 91332f2
1 file changed
+176 -0
Showing 1 changed file+176-0
▼src/db/adapter/firestore.ts
+176-0
1
+import {
2
+ BaseDatabaseAdapter,
3
+ ColumnType,
4
+ DatabaseConfig,
5
+ Migration,
6
+ MigrationStatus,
7
+ QueryOptions,
8
+ QueryResult,
9
+ TableAlterCommand,
10
+ TableSchema,
11
+ WhereClause,
12
+} from "./types";
13
+
14
+const dynamicImport = new Function("m", "return import(m)") as (m: string) => Promise<any>;
15
+
16
+function applyQuery(ref: any, where?: WhereClause[]) {
17
+ let query = ref;
18
+ for (const clause of where || []) {
19
+ const opMap: Record<string, string> = {
20
+ "=": "==",
21
+ eq: "==",
22
+ "!=": "!=",
23
+ ne: "!=",
24
+ ">": ">",
25
+ gt: ">",
26
+ ">=": ">=",
27
+ gte: ">=",
28
+ "<": "<",
29
+ lt: "<",
30
+ "<=": "<=",
31
+ lte: "<=",
32
+ in: "in",
33
+ "not in": "not-in",
34
+ };
35
+ const op = opMap[clause.operator];
36
+ if (!op) continue;
37
+ query = query.where(clause.field, op, clause.value);
38
+ }
39
+ return query;
40
+}
41
+
42
+export class FirestoreAdapter extends BaseDatabaseAdapter {
43
+ private db: any;
44
+
45
+ async connect(): Promise<void> {
46
+ if (this.connected) return;
47
+ const mod = await dynamicImport("@google-cloud/firestore");
48
+ const Firestore = mod.Firestore;
49
+ this.db = new Firestore({
50
+ projectId: this.config.database || process.env.GOOGLE_CLOUD_PROJECT,
51
+ keyFilename: this.config.options?.keyFilename as string | undefined,
52
+ });
53
+ this.connected = true;
54
+ }
55
+
56
+ async disconnect(): Promise<void> {
57
+ this.connected = false;
58
+ }
59
+
60
+ async findOne<T>(table: string, options?: QueryOptions): Promise<T | null> {
61
+ const rows = await this.findMany<T>(table, { ...options, limit: 1 });
62
+ return rows[0] || null;
63
+ }
64
+
65
+ async findMany<T>(table: string, options?: QueryOptions): Promise<T[]> {
66
+ let query = applyQuery(this.db.collection(table), options?.where);
67
+ if (options?.orderBy?.length) {
68
+ for (const o of options.orderBy) query = query.orderBy(o.field, o.direction);
69
+ }
70
+ if (options?.limit) query = query.limit(options.limit);
71
+ if (options?.offset) query = query.offset(options.offset);
72
+ const snap = await query.get();
73
+ return snap.docs.map((d: any) => ({ id: d.id, ...d.data() })) as T[];
74
+ }
75
+
76
+ async create<T>(table: string, data: Partial<T>): Promise<T> {
77
+ const id = (data as any).id as string | undefined;
78
+ const docData = { ...(data as any) };
79
+ delete docData.id;
80
+ if (id) {
81
+ await this.db.collection(table).doc(id).set(docData);
82
+ return { id, ...docData } as T;
83
+ }
84
+ const ref = await this.db.collection(table).add(docData);
85
+ return { id: ref.id, ...docData } as T;
86
+ }
87
+
88
+ async createMany<T>(table: string, data: Partial<T>[]): Promise<T[]> {
89
+ const out: T[] = [];
90
+ for (const item of data) out.push(await this.create<T>(table, item));
91
+ return out;
92
+ }
93
+
94
+ async update<T>(table: string, id: string | number, data: Partial<T>): Promise<T | null> {
95
+ const ref = this.db.collection(table).doc(String(id));
96
+ const doc = await ref.get();
97
+ if (!doc.exists) return null;
98
+ await ref.set(data as any, { merge: true });
99
+ const next = await ref.get();
100
+ return { id: next.id, ...next.data() } as T;
101
+ }
102
+
103
+ async updateMany<T>(table: string, options: QueryOptions, data: Partial<T>): Promise<number> {
104
+ const rows = await this.findMany<any>(table, options);
105
+ for (const row of rows) {
106
+ await this.db.collection(table).doc(String(row.id)).set(data as any, { merge: true });
107
+ }
108
+ return rows.length;
109
+ }
110
+
111
+ async delete(table: string, id: string | number): Promise<boolean> {
112
+ const ref = this.db.collection(table).doc(String(id));
113
+ const snap = await ref.get();
114
+ if (!snap.exists) return false;
115
+ await ref.delete();
116
+ return true;
117
+ }
118
+
119
+ async deleteMany(table: string, options?: QueryOptions): Promise<number> {
120
+ const rows = await this.findMany<any>(table, options);
121
+ for (const row of rows) {
122
+ await this.db.collection(table).doc(String(row.id)).delete();
123
+ }
124
+ return rows.length;
125
+ }
126
+
127
+ async count(table: string, options?: QueryOptions): Promise<number> {
128
+ const rows = await this.findMany<any>(table, options);
129
+ return rows.length;
130
+ }
131
+
132
+ async rawQuery<T>(query: string, params?: unknown[]): Promise<QueryResult<T>> {
133
+ throw new Error("Firestore adapter does not support SQL rawQuery");
134
+ }
135
+
136
+ async beginTransaction(): Promise<void> {}
137
+ async commitTransaction(): Promise<void> {}
138
+ async rollbackTransaction(): Promise<void> {}
139
+
140
+ async tableExists(table: string): Promise<boolean> {
141
+ const snap = await this.db.collection(table).limit(1).get();
142
+ return !snap.empty;
143
+ }
144
+
145
+ async createTable(table: string, schema: TableSchema): Promise<void> {}
146
+ async dropTable(table: string): Promise<void> {
147
+ const rows = await this.findMany<any>(table);
148
+ for (const row of rows) {
149
+ await this.db.collection(table).doc(String(row.id)).delete();
150
+ }
151
+ }
152
+ async alterTable(table: string, changes: TableAlterCommand[]): Promise<void> {}
153
+
154
+ async runMigration(migration: Migration): Promise<void> {
155
+ await migration.up(this);
156
+ await this.db.collection("_migrations").doc(migration.id).set({
157
+ id: migration.id,
158
+ name: migration.name,
159
+ executedAt: new Date().toISOString(),
160
+ });
161
+ }
162
+
163
+ async getMigrationStatus(): Promise<MigrationStatus[]> {
164
+ const snap = await this.db.collection("_migrations").get();
165
+ return snap.docs.map((d: any) => ({
166
+ id: d.id,
167
+ name: d.data().name || d.id,
168
+ executedAt: d.data().executedAt ? new Date(d.data().executedAt) : null,
169
+ status: d.data().executedAt ? "executed" : "pending",
170
+ }));
171
+ }
172
+
173
+ protected mapColumnType(type: ColumnType): string {
174
+ return type;
175
+ }
176
+}
0
177