aboutsummaryrefslogtreecommitdiff
path: root/packages/db/src/runtime/db-client.ts
blob: e45a2d717286038ec38dbfdda3fa6a2ea87fb40b (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
import { type Config as LibSQLConfig, createClient } from '@libsql/client';
import type { LibSQLDatabase } from 'drizzle-orm/libsql';
import { drizzle as drizzleLibsql } from 'drizzle-orm/libsql';
import type { SqliteRemoteDatabase } from 'drizzle-orm/sqlite-proxy';

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

function applyTransactionNotSupported(db: SqliteRemoteDatabase) {
	Object.assign(db, {
		transaction() {
			throw new Error(
				'`db.transaction()` is not currently supported. We recommend `db.batch()` for automatic error rollbacks across multiple queries.',
			);
		},
	});
}

type LocalDbClientOptions = {
	dbUrl: string;
	enableTransactions: boolean;
};

export function createLocalDatabaseClient(options: LocalDbClientOptions): LibSQLDatabase {
	const url = isWebContainer ? 'file:content.db' : options.dbUrl;
	const client = createClient({ url });
	const db = drizzleLibsql(client);

	if (!options.enableTransactions) {
		applyTransactionNotSupported(db);
	}
	return db;
}

type RemoteDbClientOptions = {
	dbType: 'libsql';
	appToken: string;
	remoteUrl: string | URL;
};

export function createRemoteDatabaseClient(options: RemoteDbClientOptions) {
	const remoteUrl = new URL(options.remoteUrl);

	return createRemoteLibSQLClient(options.appToken, remoteUrl, options.remoteUrl.toString());
}

// this function parses the options from a `Record<string, string>`
// provided from the object conversion of the searchParams and properly
// verifies that the Config object is providing the correct types.
// without this, there is runtime errors due to incorrect values
export function parseOpts(config: Record<string, string>): Partial<LibSQLConfig> {
	return {
		...config,
		...(config.syncInterval ? { syncInterval: parseInt(config.syncInterval) } : {}),
		...('readYourWrites' in config ? { readYourWrites: config.readYourWrites !== 'false' } : {}),
		...('offline' in config ? { offline: config.offline !== 'false' } : {}),
		...('tls' in config ? { tls: config.tls !== 'false' } : {}),
		...(config.concurrency ? { concurrency: parseInt(config.concurrency) } : {}),
	};
}

function createRemoteLibSQLClient(appToken: string, remoteDbURL: URL, rawUrl: string) {
	const options: Record<string, string> = Object.fromEntries(remoteDbURL.searchParams.entries());
	remoteDbURL.search = '';

	let url = remoteDbURL.toString();
	if (remoteDbURL.protocol === 'memory:') {
		// libSQL expects a special string in place of a URL
		// for in-memory DBs.
		url = ':memory:';
	} else if (
		remoteDbURL.protocol === 'file:' &&
		remoteDbURL.pathname.startsWith('/') &&
		!rawUrl.startsWith('file:/')
	) {
		// libSQL accepts relative and absolute file URLs
		// for local DBs. This doesn't match the URL specification.
		// Parsing `file:some.db` and `file:/some.db` should yield
		// the same result, but libSQL interprets the former as
		// a relative path, and the latter as an absolute path.
		// This detects when such a conversion happened during parsing
		// and undoes it so that the URL given to libSQL is the
		// same as given by the user.
		url = 'file:' + remoteDbURL.pathname.substring(1);
	}

	const client = createClient({ ...parseOpts(options), url, authToken: appToken });
	return drizzleLibsql(client);
}