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

swadhinbiswas committed 8 months ago
6bea0e1
1 parent d2c6c0d
commit 6bea0e1
1 file changed
+262 -0
Showing 1 changed file+262-0
▼src/db/adapter/turso.ts
+262-0
1
+import {
2
+ BaseDatabaseAdapter,
3
+ ColumnType,
4
+ DatabaseConfig,
5
+ Migration,
6
+ MigrationStatus,
7
+ QueryOptions,
8
+ QueryResult,
9
+ TableAlterCommand,
10
+ TableSchema,
11
+} from "./types";
12
+
13
+export class TursoAdapter extends BaseDatabaseAdapter {
14
+ private client: any;
15
+ private inTransaction = false;
16
+
17
+ async connect(): Promise<void> {
18
+ if (this.connected) return;
19
+ const libsql = await import("@libsql/client");
20
+ this.client = libsql.createClient({
21
+ url: this.config.url,
22
+ authToken: this.config.authToken,
23
+ });
24
+ await this.client.execute("SELECT 1");
25
+ this.connected = true;
26
+ }
27
+
28
+ async disconnect(): Promise<void> {
29
+ this.connected = false;
30
+ }
31
+
32
+ async findOne<T>(table: string, options?: QueryOptions): Promise<T | null> {
33
+ const rows = await this.findMany<T>(table, { ...options, limit: 1 });
34
+ return rows[0] || null;
35
+ }
36
+
37
+ async findMany<T>(table: string, options?: QueryOptions): Promise<T[]> {
38
+ const selectClause = options?.select?.length ? options.select.join(", ") : "*";
39
+ let sql = `SELECT ${selectClause} FROM ${table}`;
40
+ const args: unknown[] = [];
41
+
42
+ if (options?.where?.length) {
43
+ const where = this.buildWhereConditions(options.where);
44
+ sql += ` ${where.sql}`;
45
+ args.push(...where.params);
46
+ }
47
+ if (options?.orderBy?.length) {
48
+ sql += ` ORDER BY ${options.orderBy.map((o) => `${o.field} ${o.direction.toUpperCase()}`).join(", ")}`;
49
+ }
50
+ if (options?.limit) {
51
+ sql += " LIMIT ?";
52
+ args.push(options.limit);
53
+ }
54
+ if (options?.offset) {
55
+ sql += " OFFSET ?";
56
+ args.push(options.offset);
57
+ }
58
+
59
+ const result = await this.client.execute({ sql, args });
60
+ return (result.rows || []) as T[];
61
+ }
62
+
63
+ async create<T>(table: string, data: Partial<T>): Promise<T> {
64
+ const keys = Object.keys(data);
65
+ const values = Object.values(data);
66
+ const placeholders = keys.map(() => "?").join(", ");
67
+ const sql = `INSERT INTO ${table} (${keys.join(", ")}) VALUES (${placeholders})`;
68
+ const result = await this.client.execute({ sql, args: values });
69
+ const insertId = result.lastInsertRowid;
70
+ if (insertId !== undefined && insertId !== null) {
71
+ const found = await this.findById<T>(table, Number(insertId));
72
+ if (found) return found;
73
+ }
74
+ return { ...(data as T) };
75
+ }
76
+
77
+ async createMany<T>(table: string, data: Partial<T>[]): Promise<T[]> {
78
+ const results: T[] = [];
79
+ for (const item of data) results.push(await this.create<T>(table, item));
80
+ return results;
81
+ }
82
+
83
+ async update<T>(table: string, id: string | number, data: Partial<T>): Promise<T | null> {
84
+ const keys = Object.keys(data);
85
+ const values = Object.values(data);
86
+ const setClause = keys.map((k) => `${k} = ?`).join(", ");
87
+ const sql = `UPDATE ${table} SET ${setClause} WHERE id = ?`;
88
+ await this.client.execute({ sql, args: [...values, id] });
89
+ return this.findById<T>(table, id);
90
+ }
91
+
92
+ async updateMany<T>(table: string, options: QueryOptions, data: Partial<T>): Promise<number> {
93
+ const keys = Object.keys(data);
94
+ const values = Object.values(data);
95
+ let sql = `UPDATE ${table} SET ${keys.map((k) => `${k} = ?`).join(", ")}`;
96
+ const args: unknown[] = [...values];
97
+ if (options?.where?.length) {
98
+ const where = this.buildWhereConditions(options.where);
99
+ sql += ` ${where.sql}`;
100
+ args.push(...where.params);
101
+ }
102
+ const result = await this.client.execute({ sql, args });
103
+ return result.rowsAffected || 0;
104
+ }
105
+
106
+ async delete(table: string, id: string | number): Promise<boolean> {
107
+ const result = await this.client.execute({
108
+ sql: `DELETE FROM ${table} WHERE id = ?`,
109
+ args: [id],
110
+ });
111
+ return (result.rowsAffected || 0) > 0;
112
+ }
113
+
114
+ async deleteMany(table: string, options?: QueryOptions): Promise<number> {
115
+ let sql = `DELETE FROM ${table}`;
116
+ const args: unknown[] = [];
117
+ if (options?.where?.length) {
118
+ const where = this.buildWhereConditions(options.where);
119
+ sql += ` ${where.sql}`;
120
+ args.push(...where.params);
121
+ }
122
+ const result = await this.client.execute({ sql, args });
123
+ return result.rowsAffected || 0;
124
+ }
125
+
126
+ async count(table: string, options?: QueryOptions): Promise<number> {
127
+ let sql = `SELECT COUNT(*) as count FROM ${table}`;
128
+ const args: unknown[] = [];
129
+ if (options?.where?.length) {
130
+ const where = this.buildWhereConditions(options.where);
131
+ sql += ` ${where.sql}`;
132
+ args.push(...where.params);
133
+ }
134
+ const result = await this.client.execute({ sql, args });
135
+ return Number(result.rows?.[0]?.count || 0);
136
+ }
137
+
138
+ async rawQuery<T>(query: string, params?: unknown[]): Promise<QueryResult<T>> {
139
+ const result = await this.client.execute({ sql: query, args: params || [] });
140
+ return {
141
+ rows: (result.rows || []) as T[],
142
+ rowCount: result.rows?.length || result.rowsAffected || 0,
143
+ lastInsertId: result.lastInsertRowid,
144
+ affectedRows: result.rowsAffected || 0,
145
+ };
146
+ }
147
+
148
+ async beginTransaction(): Promise<void> {
149
+ if (this.inTransaction) return;
150
+ await this.client.execute("BEGIN");
151
+ this.inTransaction = true;
152
+ }
153
+
154
+ async commitTransaction(): Promise<void> {
155
+ if (!this.inTransaction) return;
156
+ await this.client.execute("COMMIT");
157
+ this.inTransaction = false;
158
+ }
159
+
160
+ async rollbackTransaction(): Promise<void> {
161
+ if (!this.inTransaction) return;
162
+ await this.client.execute("ROLLBACK");
163
+ this.inTransaction = false;
164
+ }
165
+
166
+ async tableExists(table: string): Promise<boolean> {
167
+ const result = await this.client.execute({
168
+ sql: `SELECT name FROM sqlite_master WHERE type='table' AND name=?`,
169
+ args: [table],
170
+ });
171
+ return (result.rows?.length || 0) > 0;
172
+ }
173
+
174
+ async createTable(table: string, schema: TableSchema): Promise<void> {
175
+ const columnDefs = schema.columns.map((col) => {
176
+ let def = `${col.name} ${this.mapColumnType(col.type)}`;
177
+ if (col.autoIncrement && schema.primaryKey === col.name) {
178
+ def = `${col.name} INTEGER PRIMARY KEY AUTOINCREMENT`;
179
+ } else {
180
+ if (!col.nullable) def += " NOT NULL";
181
+ if (col.unique) def += " UNIQUE";
182
+ }
183
+ return def;
184
+ });
185
+ if (schema.primaryKey && !schema.columns.some((c) => c.autoIncrement)) {
186
+ const pk = Array.isArray(schema.primaryKey) ? schema.primaryKey.join(", ") : schema.primaryKey;
187
+ columnDefs.push(`PRIMARY KEY (${pk})`);
188
+ }
189
+ await this.client.execute(`CREATE TABLE IF NOT EXISTS ${table} (${columnDefs.join(", ")})`);
190
+ }
191
+
192
+ async dropTable(table: string): Promise<void> {
193
+ await this.client.execute(`DROP TABLE IF EXISTS ${table}`);
194
+ }
195
+
196
+ async alterTable(table: string, changes: TableAlterCommand[]): Promise<void> {
197
+ for (const change of changes) {
198
+ if (change.type === "add_column" && change.column) {
199
+ await this.client.execute(
200
+ `ALTER TABLE ${table} ADD COLUMN ${change.column.name} ${this.mapColumnType(change.column.type)}`
201
+ );
202
+ }
203
+ }
204
+ }
205
+
206
+ async runMigration(migration: Migration): Promise<void> {
207
+ await this.beginTransaction();
208
+ try {
209
+ await migration.up(this);
210
+ await this.rawQuery(
211
+ `INSERT INTO _migrations (id, name, executed_at) VALUES (?, ?, ?)`,
212
+ [migration.id, migration.name, new Date().toISOString()]
213
+ );
214
+ await this.commitTransaction();
215
+ } catch (error) {
216
+ await this.rollbackTransaction();
217
+ throw error;
218
+ }
219
+ }
220
+
221
+ async getMigrationStatus(): Promise<MigrationStatus[]> {
222
+ const exists = await this.tableExists("_migrations");
223
+ if (!exists) {
224
+ await this.createTable("_migrations", {
225
+ columns: [
226
+ { name: "id", type: "string", nullable: false },
227
+ { name: "name", type: "string", nullable: false },
228
+ { name: "executed_at", type: "datetime", nullable: true },
229
+ ],
230
+ primaryKey: "id",
231
+ });
232
+ }
233
+ const result = await this.rawQuery<{ id: string; name: string; executed_at: string | null }>(
234
+ "SELECT * FROM _migrations ORDER BY executed_at"
235
+ );
236
+ return result.rows.map((row) => ({
237
+ id: row.id,
238
+ name: row.name,
239
+ executedAt: row.executed_at ? new Date(row.executed_at) : null,
240
+ status: row.executed_at ? "executed" : "pending",
241
+ }));
242
+ }
243
+
244
+ protected mapColumnType(type: ColumnType): string {
245
+ const typeMap: Record<ColumnType, string> = {
246
+ string: "TEXT",
247
+ text: "TEXT",
248
+ integer: "INTEGER",
249
+ bigint: "INTEGER",
250
+ float: "REAL",
251
+ decimal: "REAL",
252
+ boolean: "INTEGER",
253
+ date: "TEXT",
254
+ datetime: "TEXT",
255
+ timestamp: "TEXT",
256
+ json: "TEXT",
257
+ blob: "BLOB",
258
+ uuid: "TEXT",
259
+ };
260
+ return typeMap[type] || "TEXT";
261
+ }
262
+}
0
263