summaryrefslogtreecommitdiff
path: root/packages/db/src/runtime/db-client.ts
blob: 28db1138fb5b276a1957577a33c7613339bd4ecc (plain) (blame)
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
import type { InStatement } from '@libsql/client';
import { createClient } from '@libsql/client';
import { type DBTables } from '../core/types.js';
import type { LibSQLDatabase } from 'drizzle-orm/libsql';
import { drizzle as drizzleLibsql } from 'drizzle-orm/libsql';
import { drizzle as drizzleProxy } from 'drizzle-orm/sqlite-proxy';
import { type SQLiteTable } from 'drizzle-orm/sqlite-core';
import { z } from 'zod';
import { getTableName } from 'drizzle-orm';

const isWebContainer = !!process.versions?.webcontainer;

interface LocalDatabaseClient extends LibSQLDatabase, Disposable {}

export async function createLocalDatabaseClient({
	tables,
	dbUrl,
	seeding,
}: {
	dbUrl: string;
	tables: DBTables;
	seeding: boolean;
}): Promise<LocalDatabaseClient> {
	const url = isWebContainer ? 'file:content.db' : dbUrl;
	const client = createClient({ url });
	const db = Object.assign(drizzleLibsql(client), {
		[Symbol.dispose || Symbol.for('Symbol.dispose')]() {
			client.close();
		},
	});

	if (seeding) return db;

	const { insert: drizzleInsert, update: drizzleUpdate, delete: drizzleDelete } = db;
	return Object.assign(db, {
		insert(Table: SQLiteTable) {
			checkIfModificationIsAllowed(tables, Table);
			return drizzleInsert.call(this, Table);
		},
		update(Table: SQLiteTable) {
			checkIfModificationIsAllowed(tables, Table);
			return drizzleUpdate.call(this, Table);
		},
		delete(Table: SQLiteTable) {
			checkIfModificationIsAllowed(tables, Table);
			return drizzleDelete.call(this, Table);
		},
	});
}

function checkIfModificationIsAllowed(tables: DBTables, Table: SQLiteTable) {
	const tableName = getTableName(Table);
	const collection = tables[tableName];
	if (!collection.writable) {
		throw new Error(`The [${tableName}] collection is read-only.`);
	}
}

export function createRemoteDatabaseClient(appToken: string, remoteDbURL: string) {
	const url = new URL('/db/query', remoteDbURL);

	const db = drizzleProxy(async (sql, parameters, method) => {
		const requestBody: InStatement = { sql, args: parameters };
		// eslint-disable-next-line no-console
		console.info(JSON.stringify(requestBody));
		const res = await fetch(url, {
			method: 'POST',
			headers: {
				Authorization: `Bearer ${appToken}`,
				'Content-Type': 'application/json',
			},
			body: JSON.stringify(requestBody),
		});
		if (!res.ok) {
			throw new Error(
				`Failed to execute query.\nQuery: ${sql}\nFull error: ${res.status} ${await res.text()}}`
			);
		}

		const queryResultSchema = z.object({
			rows: z.array(z.unknown()),
		});
		let rows: unknown[];
		try {
			const json = await res.json();
			rows = queryResultSchema.parse(json).rows;
		} catch (e) {
			throw new Error(
				`Failed to execute query.\nQuery: ${sql}\nFull error: Unexpected JSON response. ${
					e instanceof Error ? e.message : String(e)
				}`
			);
		}

		// Drizzle expects each row as an array of its values
		const rowValues: unknown[][] = [];

		for (const row of rows) {
			if (row != null && typeof row === 'object') {
				rowValues.push(Object.values(row));
			}
		}

		if (method === 'get') {
			return { rows: rowValues[0] };
		}

		return { rows: rowValues };
	});
	return db;
}