summaryrefslogtreecommitdiff
path: root/packages/db/src/runtime/index.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/db/src/runtime/index.ts')
-rw-r--r--packages/db/src/runtime/index.ts157
1 files changed, 157 insertions, 0 deletions
diff --git a/packages/db/src/runtime/index.ts b/packages/db/src/runtime/index.ts
new file mode 100644
index 000000000..fb8579459
--- /dev/null
+++ b/packages/db/src/runtime/index.ts
@@ -0,0 +1,157 @@
+import { type ColumnBuilderBaseConfig, type ColumnDataType, sql } from 'drizzle-orm';
+import type { LibSQLDatabase } from 'drizzle-orm/libsql';
+import {
+ type IndexBuilder,
+ type SQLiteColumnBuilderBase,
+ customType,
+ index,
+ integer,
+ sqliteTable,
+ text,
+} from 'drizzle-orm/sqlite-core';
+import type { DBColumn, DBTable } from '../core/types.js';
+import { type SerializedSQL, isSerializedSQL } from './types.js';
+import { pathToFileURL } from './utils.js';
+export type Database = Omit<LibSQLDatabase, 'transaction'>;
+export type { Table } from './types.js';
+export { createRemoteDatabaseClient, createLocalDatabaseClient } from './db-client.js';
+
+export function hasPrimaryKey(column: DBColumn) {
+ return 'primaryKey' in column.schema && !!column.schema.primaryKey;
+}
+
+// Taken from:
+// https://stackoverflow.com/questions/52869695/check-if-a-date-string-is-in-iso-and-utc-format
+const isISODateString = (str: string) => /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/.test(str);
+
+const dateType = customType<{ data: Date; driverData: string }>({
+ dataType() {
+ return 'text';
+ },
+ toDriver(value) {
+ return value.toISOString();
+ },
+ fromDriver(value) {
+ if (!isISODateString(value)) {
+ // values saved using CURRENT_TIMESTAMP are not valid ISO strings
+ // but *are* in UTC, so append the UTC zone.
+ value += 'Z';
+ }
+ return new Date(value);
+ },
+});
+
+const jsonType = customType<{ data: unknown; driverData: string }>({
+ dataType() {
+ return 'text';
+ },
+ toDriver(value) {
+ return JSON.stringify(value);
+ },
+ fromDriver(value) {
+ return JSON.parse(value);
+ },
+});
+
+type D1ColumnBuilder = SQLiteColumnBuilderBase<
+ ColumnBuilderBaseConfig<ColumnDataType, string> & { data: unknown }
+>;
+
+export function asDrizzleTable(name: string, table: DBTable) {
+ const columns: Record<string, D1ColumnBuilder> = {};
+ if (!Object.entries(table.columns).some(([, column]) => hasPrimaryKey(column))) {
+ columns['_id'] = integer('_id').primaryKey();
+ }
+ for (const [columnName, column] of Object.entries(table.columns)) {
+ columns[columnName] = columnMapper(columnName, column);
+ }
+ const drizzleTable = sqliteTable(name, columns, (ormTable) => {
+ const indexes: Record<string, IndexBuilder> = {};
+ for (const [indexName, indexProps] of Object.entries(table.indexes ?? {})) {
+ const onColNames = Array.isArray(indexProps.on) ? indexProps.on : [indexProps.on];
+ const onCols = onColNames.map((colName) => ormTable[colName]);
+ if (!atLeastOne(onCols)) continue;
+
+ indexes[indexName] = index(indexName).on(...onCols);
+ }
+ return indexes;
+ });
+ return drizzleTable;
+}
+
+function atLeastOne<T>(arr: T[]): arr is [T, ...T[]] {
+ return arr.length > 0;
+}
+
+function columnMapper(columnName: string, column: DBColumn) {
+ let c: ReturnType<
+ | typeof text
+ | typeof integer
+ | typeof jsonType
+ | typeof dateType
+ | typeof integer<string, 'boolean'>
+ >;
+
+ switch (column.type) {
+ case 'text': {
+ c = text(columnName);
+ // Duplicate default logic across cases to preserve type inference.
+ // No clean generic for every column builder.
+ if (column.schema.default !== undefined)
+ c = c.default(handleSerializedSQL(column.schema.default));
+ if (column.schema.primaryKey === true) c = c.primaryKey();
+ break;
+ }
+ case 'number': {
+ c = integer(columnName);
+ if (column.schema.default !== undefined)
+ c = c.default(handleSerializedSQL(column.schema.default));
+ if (column.schema.primaryKey === true) c = c.primaryKey();
+ break;
+ }
+ case 'boolean': {
+ c = integer(columnName, { mode: 'boolean' });
+ if (column.schema.default !== undefined)
+ c = c.default(handleSerializedSQL(column.schema.default));
+ break;
+ }
+ case 'json':
+ c = jsonType(columnName);
+ if (column.schema.default !== undefined) c = c.default(column.schema.default);
+ break;
+ case 'date': {
+ c = dateType(columnName);
+ if (column.schema.default !== undefined) {
+ const def = handleSerializedSQL(column.schema.default);
+ c = c.default(typeof def === 'string' ? new Date(def) : def);
+ }
+ break;
+ }
+ }
+
+ if (!column.schema.optional) c = c.notNull();
+ if (column.schema.unique) c = c.unique();
+ return c;
+}
+
+function handleSerializedSQL<T>(def: T | SerializedSQL) {
+ if (isSerializedSQL(def)) {
+ return sql.raw(def.sql);
+ }
+ return def;
+}
+
+export function normalizeDatabaseUrl(envDbUrl: string | undefined, defaultDbUrl: string): string {
+ if (envDbUrl) {
+ // This could be a file URL, or more likely a root-relative file path.
+ // Convert it to a file URL.
+ if (envDbUrl.startsWith('file://')) {
+ return envDbUrl;
+ }
+
+ return new URL(envDbUrl, pathToFileURL(process.cwd()) + '/').toString();
+ } else {
+ // This is going to be a file URL always,
+ return defaultDbUrl;
+ }
+}