Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,5 +44,8 @@ jobs:
- name: Restore lockfile (tests may modify it due to Deno version differences)
run: git checkout deno.lock

- name: Build generated files
run: deno task build:kit-string

- name: Publish to JSR
run: deno publish
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ This matches the sqlite-proxy specification. For most use cases, prefer the quer

### Prerequisites

- Deno 2.x (with `--unstable-bundle` for building kit-string)
- Docker and Docker Compose
- Make

Expand All @@ -161,6 +162,21 @@ git config core.hooksPath .githooks && \
chmod +x .githooks/pre-commit
```

### Building Generated Files

The `./kit-string` export is generated from `src/kit.ts` and must be built before publishing:

```bash
# Generate dist/kit-string.ts (required before deno publish)
deno task build:kit-string
```

CI runs this automatically before `deno publish`. For local publishing:

```bash
deno task build:kit-string && deno publish
```

### Running Tests

Tests run in Docker containers to ensure consistent environments.
Expand Down
17 changes: 10 additions & 7 deletions deno.jsonc
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
{
"name": "@hotsauce/drizzle-runtime-sqlite",
"version": "0.1.1",
"version": "0.1.2",
"exports": {
".": "./mod.ts",
"./kit": "./src/kit.ts"
"./kit": "./src/kit.ts",
"./kit-string": "./dist/kit-string.ts"
},
"imports": {
"drizzle-orm": "npm:drizzle-orm@0.45.1",
Expand All @@ -16,17 +17,19 @@
},
"tasks": {
"test": "deno run -A npm:vitest run --config vitest.config.deno.ts",
"lint": "deno lint",
"fmt": "deno fmt",
"check": "deno fmt --check && deno lint"
"lint": "deno lint --ignore=dist/ --ignore=repos/",
"fmt": "deno fmt --ignore=dist/ --ignore=repos/",
"check": "deno fmt --check && deno lint",
"build:kit-string": "deno run --allow-read=src --allow-write=dist --unstable-bundle scripts/build-kit-string.ts",
"prepublish": "deno task build:kit-string"
},
"lint": {
"include": ["src/", "tests/", "mod.ts"],
"exclude": ["repos/"]
"exclude": ["repos/", "dist/"]
},
"fmt": {
"include": ["src/", "tests/", "mod.ts"],
"exclude": ["repos/"]
"exclude": ["repos/", "dist/"]
},
"compilerOptions": {
"strict": true,
Expand Down
51 changes: 51 additions & 0 deletions deno.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

49 changes: 49 additions & 0 deletions scripts/build-kit-string.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/**
* Build script to generate kit-string.ts from the actual kit.ts source.
*
* Usage: deno run --allow-read=src --allow-write=dist scripts/build-kit-string.ts
*/

const result = await Deno.bundle({
entrypoints: ["./src/kit.ts"],
write: false,
minify: false,
external: ["drizzle-orm", "drizzle-orm/*"],
});

const bundledCode = result.outputFiles![0].text();

// Remove the export statement at the end
const codeWithoutExport = bundledCode.replace(/\nexport\s*\{[^}]*\};\s*$/, "\n");

// Validate the export was stripped (fail fast if bundler output format changes)
if (codeWithoutExport.includes("export {")) {
console.error("ERROR: Failed to strip export statement from bundle. Bundler output format may have changed.");
Deno.exit(1);
}

// Escape backticks and ${} for template literal
const escaped = codeWithoutExport
.replace(/\\/g, "\\\\")
.replace(/`/g, "\\`")
.replace(/\$\{/g, "\\${");

// Generate .ts file with explicit type annotation
const tsOutput = `// deno-lint-ignore-file
/**
* Drizzle Kit driver for node:sqlite as an injectable string.
*
* AUTO-GENERATED - DO NOT EDIT
* Run: deno task build:kit-string
*
* This exports the driver code as a string that can be injected into
* drizzle-kit's connections.ts at build time.
*/

export const drizzleKitDriverBlock: string = \`
${escaped}\`;
`;

await Deno.mkdir("dist", { recursive: true });
await Deno.writeTextFile("dist/kit-string.ts", tsOutput);
console.log("Generated dist/kit-string.ts");
39 changes: 24 additions & 15 deletions src/kit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,12 @@
* Provides a drop-in replacement for better-sqlite3 in drizzle-kit's connections.
*/

import { DatabaseSync } from "node:sqlite";
import type { MigrationConfig } from "drizzle-orm/migrator";
import {
asExtended,
executeWithArrayMode,
hasArrayMode,
normaliseSQLiteUrl,
prepareSqliteParams,
hasColumns,
rowToArray,
} from "./shared.ts";

Expand All @@ -25,6 +23,13 @@ export interface SQLiteDB {
* Proxy params matching drizzle-kit's ProxyParams.
* This is a practical subset - drizzle-kit's full type has mode/method required,
* but SQLite proxy paths don't always provide them.
*
* Method behaviors:
* - `run`: executes statement, returns []
* - `all`: returns all rows as array
* - `get`: returns all rows (drizzle-kit expects array, not single row)
* - `values`: same as `all`
* - `execute`: treated as `run` (executes statement, returns [])
*/
export interface ProxyParams {
sql: string;
Expand All @@ -47,11 +52,6 @@ export type TransactionProxy = (
queries: TransactionQuery[],
) => Promise<(unknown[] | Error)[]>;

/** Credentials for SQLite connection */
export interface SqliteCredentials {
url: string;
}

/** Return type of createDrizzleKitDriver */
export interface DrizzleKitDriver extends SQLiteDB {
packageName: "node:sqlite";
Expand All @@ -66,24 +66,27 @@ export interface DrizzleKitDriver extends SQLiteDB {
* This provides the same interface as drizzle-kit's better-sqlite3 driver,
* allowing it to be used as a drop-in replacement.
*
* @param credentials - SQLite credentials with url property
* @param dbPath - Path to the SQLite database file (already normalised)
* @param prepareSqliteParams - Function to prepare params (from drizzle-kit)
* @returns Driver object compatible with drizzle-kit
*
* @example
* ```ts
* import { createDrizzleKitDriver } from "@hotsauce/drizzle-runtime-sqlite/kit";
*
* const driver = await createDrizzleKitDriver({ url: "./database.db" });
* const driver = await createDrizzleKitDriver("./database.db", prepareSqliteParams);
* // Use with drizzle-kit internals
* ```
*/
export async function createDrizzleKitDriver(
credentials: SqliteCredentials,
export async function createNodeSqlDriver(
dbPath: string,
prepareSqliteParams: (params: unknown[]) => unknown[],
): Promise<DrizzleKitDriver> {
const { DatabaseSync } = await import("node:sqlite");
const { drizzle } = await import("drizzle-orm/sqlite-proxy");
const { migrate } = await import("drizzle-orm/sqlite-proxy/migrator");

const sqlite = new DatabaseSync(normaliseSQLiteUrl(credentials.url));
const sqlite = new DatabaseSync(dbPath);

/**
* Callback for sqlite-proxy following its expected semantics.
Expand All @@ -100,9 +103,10 @@ export async function createDrizzleKitDriver(
return executeWithArrayMode(stmt, params, method);
};

// Create drizzle instance for migrations (internal use only)
// Create drizzle instance for migrations (internal use only).
// This instance is NOT exposed for general queries - only migrate() uses it.
// Note: drizzle's AsyncRemoteCallback expects { rows: any[] }, but sqlite-proxy
// semantics return undefined for get() with no row. We coerce here since
// semantics return undefined for get() with no row. We coerce to [] since
// migrations only use "run" method which always returns { rows: [] }.
const drzl = drizzle((sql, params, method) => {
const result = sqliteProxyCallback(sql, params as unknown[], method);
Expand Down Expand Up @@ -155,6 +159,11 @@ export async function createDrizzleKitDriver(

if (params.mode === "array" && !useArrayMode && rows.length > 0) {
// Fallback: convert objects to arrays using column metadata
if (!hasColumns(stmt)) {
throw new Error(
"node:sqlite: stmt.columns() not available. Upgrade to Deno 2.6+ or Node.js 22.5+.",
);
}
const columns = asExtended(stmt).columns();
return Promise.resolve(
(rows as Record<string, unknown>[]).map((row) =>
Expand Down
52 changes: 0 additions & 52 deletions src/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,55 +125,3 @@ export function executeWithArrayMode(
throw new Error(`Unknown method: ${method}`);
}
}

/**
* Prepare SQLite params, converting binary objects to byte arrays.
* Matches drizzle-kit's prepareSqliteParams behavior.
*/
export function prepareSqliteParams(params: unknown[]): unknown[] {
return params.map((param) => {
if (
param &&
typeof param === "object" &&
"type" in param &&
"value" in param &&
(param as { type: string }).type === "binary"
) {
const value = typeof (param as { value: unknown }).value === "object"
? JSON.stringify((param as { value: unknown }).value)
: ((param as { value: unknown }).value as string);
// Use TextEncoder for cross-runtime compatibility (works in Node, Deno, browsers)
return new TextEncoder().encode(value);
}
return param;
});
}

/**
* Normalise SQLite URL for node:sqlite.
* Handles file: URLs and plain paths.
*
* - `file:./relative.db` -> `./relative.db`
* - `file:///absolute/path.db` -> `/absolute/path.db`
* - `file://localhost/path.db` -> `/path.db`
* - `./relative.db` -> `./relative.db` (unchanged)
*/
export function normaliseSQLiteUrl(url: string): string {
if (!url.startsWith("file:")) {
return url;
}

// Handle file:// URLs (with authority)
if (url.startsWith("file://")) {
try {
const parsed = new URL(url);
return parsed.pathname;
} catch {
// If URL parsing fails, strip prefix and return
return url.slice(7);
}
}

// Handle file: prefix without // (e.g., file:./db.sqlite)
return url.slice(5);
}
Loading