init opfs and sqlocal as another browser adapter

This commit is contained in:
dswbx
2025-11-25 16:21:16 +01:00
parent 5e5dc62304
commit 36e1bb1867
17 changed files with 844 additions and 110 deletions

View File

@@ -16,12 +16,12 @@
"prepublishOnly": "bun run typecheck && bun run test && bun run build"
},
"dependencies": {
"sqlocal": "^0.14.0"
"sqlocal": "^0.16.0"
},
"devDependencies": {
"@vitest/browser": "^3.0.8",
"@vitest/ui": "^3.0.8",
"@types/node": "^22.13.10",
"@types/node": "^24.10.1",
"bknd": "workspace:*",
"kysely": "^0.27.6",
"tsup": "^8.4.0",

View File

@@ -1,51 +1,44 @@
import { Kysely, ParseJSONResultsPlugin } from "kysely";
import { SqliteConnection, SqliteIntrospector } from "bknd/data";
import { SQLocalKysely } from "sqlocal/kysely";
import type { ClientConfig } from "sqlocal";
import { SqliteConnection, SqliteIntrospector, type DB } from "bknd";
import type { SQLocalKysely } from "sqlocal/kysely";
const plugins = [new ParseJSONResultsPlugin()];
export type SQLocalConnectionConfig = Omit<ClientConfig, "databasePath"> & {
// make it optional
databasePath?: ClientConfig["databasePath"];
};
export class SQLocalConnection extends SqliteConnection<SQLocalKysely> {
private connected: boolean = false;
export class SQLocalConnection extends SqliteConnection {
private _client: SQLocalKysely | undefined;
constructor(private config: SQLocalConnectionConfig) {
super(null as any, {}, plugins);
constructor(client: SQLocalKysely) {
// @ts-expect-error - config is protected
client.config.onConnect = () => {
// we need to listen for the connection, it will be awaited in init()
this.connected = true;
};
super({
kysely: new Kysely<any>({
dialect: {
...client.dialect,
createIntrospector: (db: Kysely<DB>) => {
return new SqliteIntrospector(db as any, {
plugins,
});
},
},
plugins,
}) as any,
});
this.client = client;
}
override async init() {
if (this.initialized) return;
await new Promise((resolve) => {
this._client = new SQLocalKysely({
...this.config,
databasePath: this.config.databasePath ?? "session",
onConnect: (r) => {
this.kysely = new Kysely<any>({
dialect: {
...this._client!.dialect,
createIntrospector: (db: Kysely<any>) => {
return new SqliteIntrospector(db, {
plugins,
});
},
},
plugins,
});
this.config.onConnect?.(r);
resolve(1);
},
});
});
super.init();
}
get client(): SQLocalKysely {
if (!this._client) throw new Error("Client not initialized");
return this._client!;
let tries = 0;
while (!this.connected && tries < 100) {
tries++;
await new Promise((resolve) => setTimeout(resolve, 5));
}
if (!this.connected) {
throw new Error("Failed to connect to SQLite database");
}
this.initialized = true;
}
}

View File

@@ -1 +1 @@
export { SQLocalConnection, type SQLocalConnectionConfig } from "./SQLocalConnection";
export { SQLocalConnection } from "./SQLocalConnection";

View File

@@ -1,14 +1,15 @@
import { describe, expect, it } from "vitest";
import { SQLocalConnection, type SQLocalConnectionConfig } from "../src";
import { SQLocalConnection } from "../src";
import type { ClientConfig } from "sqlocal";
import { SQLocalKysely } from "sqlocal/kysely";
describe(SQLocalConnection, () => {
function create(config: SQLocalConnectionConfig = {}) {
return new SQLocalConnection(config);
function create(config: ClientConfig = { databasePath: ":memory:" }) {
return new SQLocalConnection(new SQLocalKysely(config));
}
it("constructs", async () => {
const connection = create();
expect(() => connection.client).toThrow();
await connection.init();
expect(connection.client).toBeDefined();
expect(await connection.client.sql`SELECT 1`).toEqual([{ "1": 1 }]);

View File

@@ -1,11 +1,12 @@
import { describe, expect, it } from "vitest";
import { SQLocalConnection, type SQLocalConnectionConfig } from "../src";
import { createApp } from "bknd";
import * as proto from "bknd/data";
import { describe, expect, it } from "bun:test";
import { SQLocalConnection } from "../src";
import { createApp, em, entity, text } from "bknd";
import type { ClientConfig } from "sqlocal";
import { SQLocalKysely } from "sqlocal/kysely";
describe("integration", () => {
function create(config: SQLocalConnectionConfig = { databasePath: ":memory:" }) {
return new SQLocalConnection(config);
function create(config: ClientConfig = { databasePath: ":memory:" }) {
return new SQLocalConnection(new SQLocalKysely(config));
}
it("should create app and ping", async () => {
@@ -19,14 +20,14 @@ describe("integration", () => {
});
it("should create a basic schema", async () => {
const schema = proto.em(
const schema = em(
{
posts: proto.entity("posts", {
title: proto.text().required(),
content: proto.text(),
posts: entity("posts", {
title: text().required(),
content: text(),
}),
comments: proto.entity("comments", {
content: proto.text(),
comments: entity("comments", {
content: text(),
}),
},
(fns, s) => {

View File

@@ -1,6 +1,6 @@
/// <reference types="vitest" />
/// <reference types="@vitest/browser/providers/webdriverio" />
import { defineConfig } from "vite";
import { defineConfig } from "vitest/config";
// https://github.com/DallasHoff/sqlocal/blob/main/vite.config.ts
export default defineConfig({