diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 716ae5f..411a1e2 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -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 diff --git a/README.md b/README.md index 8ce4db8..907e377 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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. diff --git a/deno.jsonc b/deno.jsonc index 8200a40..fec0d47 100644 --- a/deno.jsonc +++ b/deno.jsonc @@ -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", @@ -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, diff --git a/deno.lock b/deno.lock index 25a5e79..3903d13 100644 --- a/deno.lock +++ b/deno.lock @@ -1,10 +1,61 @@ { "version": "5", "specifiers": { + "jsr:@deno/cache-dir@0.13.2": "0.13.2", + "jsr:@deno/emit@*": "0.46.0", + "jsr:@std/assert@0.223": "0.223.0", + "jsr:@std/bytes@0.223": "0.223.0", + "jsr:@std/fmt@0.223": "0.223.0", + "jsr:@std/fs@0.223": "0.223.0", + "jsr:@std/io@0.223": "0.223.0", + "jsr:@std/path@0.223": "0.223.0", "npm:drizzle-orm@0.45.1": "0.45.1", "npm:typescript@^5.4.0": "5.9.3", "npm:vitest@^1.6.0": "1.6.1" }, + "jsr": { + "@deno/cache-dir@0.13.2": { + "integrity": "c22419dfe27ab85f345bee487aaaadba498b005cce3644e9d2528db035c5454d", + "dependencies": [ + "jsr:@std/fmt", + "jsr:@std/fs", + "jsr:@std/io", + "jsr:@std/path" + ] + }, + "@deno/emit@0.46.0": { + "integrity": "e276be2c77bac1b93caf775762e2a49a54cb00da2d48ca2b01ed8d7cba9d082c", + "dependencies": [ + "jsr:@deno/cache-dir", + "jsr:@std/path" + ] + }, + "@std/assert@0.223.0": { + "integrity": "eb8d6d879d76e1cc431205bd346ed4d88dc051c6366365b1af47034b0670be24" + }, + "@std/bytes@0.223.0": { + "integrity": "84b75052cd8680942c397c2631318772b295019098f40aac5c36cead4cba51a8" + }, + "@std/fmt@0.223.0": { + "integrity": "6deb37794127dfc7d7bded2586b9fc6f5d50e62a8134846608baf71ffc1a5208" + }, + "@std/fs@0.223.0": { + "integrity": "3b4b0550b2c524cbaaa5a9170c90e96cbb7354e837ad1bdaf15fc9df1ae9c31c" + }, + "@std/io@0.223.0": { + "integrity": "2d8c3c2ab3a515619b90da2c6ff5ea7b75a94383259ef4d02116b228393f84f1", + "dependencies": [ + "jsr:@std/assert", + "jsr:@std/bytes" + ] + }, + "@std/path@0.223.0": { + "integrity": "593963402d7e6597f5a6e620931661053572c982fc014000459edc1f93cc3989", + "dependencies": [ + "jsr:@std/assert" + ] + } + }, "npm": { "@esbuild/aix-ppc64@0.21.5": { "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", diff --git a/scripts/build-kit-string.ts b/scripts/build-kit-string.ts new file mode 100644 index 0000000..914536e --- /dev/null +++ b/scripts/build-kit-string.ts @@ -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"); diff --git a/src/kit.ts b/src/kit.ts index 34ebd5c..0f2dac1 100644 --- a/src/kit.ts +++ b/src/kit.ts @@ -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"; @@ -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; @@ -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"; @@ -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 { + 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. @@ -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); @@ -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[]).map((row) => diff --git a/src/shared.ts b/src/shared.ts index 8e2a1d7..e025368 100644 --- a/src/shared.ts +++ b/src/shared.ts @@ -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); -}