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
| @@ -0,0 +1,262 @@ | ||
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 | |