mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-15 20:17:22 +00:00
add sqlocal connection including example
This commit is contained in:
29
packages/sqlocal/README.md
Normal file
29
packages/sqlocal/README.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# SQLocal adapter for `bknd` (experimental)
|
||||
This packages adds an adapter to use a SQLocal database with `bknd`. It is based on [`sqlocal`](https://github.com/DallasHoff/sqlocal) and the driver included for `kysely`.
|
||||
|
||||
## Installation
|
||||
Install the adapter with:
|
||||
```bash
|
||||
npm install @bknd/sqlocal
|
||||
```
|
||||
|
||||
## Usage
|
||||
Create a connection:
|
||||
|
||||
```ts
|
||||
import { SQLocalConnection } from "@bknd/sqlocal";
|
||||
|
||||
const connection = new SQLocalConnection({
|
||||
databasePath: "db.sqlite"
|
||||
});
|
||||
```
|
||||
|
||||
Use the connection depending on which framework or runtime you are using. E.g., when using `createApp`, you can use the connection as follows:
|
||||
|
||||
```ts
|
||||
import { createApp } from "bknd";
|
||||
import { SQLocalConnection } from "@bknd/sqlocal";
|
||||
|
||||
const connection = new SQLocalConnection();
|
||||
const app = createApp({ connection });
|
||||
```
|
||||
39
packages/sqlocal/package.json
Normal file
39
packages/sqlocal/package.json
Normal file
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"name": "@bknd/sqlocal",
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"module": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "tsup",
|
||||
"test": "vitest",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"prepublishOnly": "bun run test && bun run typecheck && bun run build"
|
||||
},
|
||||
"dependencies": {
|
||||
"sqlocal": "^0.14.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitest/browser": "^3.0.8",
|
||||
"@vitest/ui": "^3.0.8",
|
||||
"@types/node": "^22.13.10",
|
||||
"bknd": "workspace:*",
|
||||
"kysely": "^0.27.6",
|
||||
"tsup": "^8.4.0",
|
||||
"typescript": "^5.6.3",
|
||||
"vitest": "^3.0.8",
|
||||
"webdriverio": "^9.12.0"
|
||||
},
|
||||
"tsup": {
|
||||
"entry": ["src/index.ts"],
|
||||
"format": ["esm"],
|
||||
"target": "es2022",
|
||||
"clean": true,
|
||||
"minify": true,
|
||||
"dts": true,
|
||||
"metafile": true,
|
||||
"external": ["bknd", "kysely"]
|
||||
},
|
||||
"files": ["dist", "README.md", "!*.map", "!metafile*.json"]
|
||||
}
|
||||
51
packages/sqlocal/src/SQLocalConnection.ts
Normal file
51
packages/sqlocal/src/SQLocalConnection.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { Kysely, ParseJSONResultsPlugin } from "kysely";
|
||||
import { SqliteConnection, SqliteIntrospector } from "bknd/data";
|
||||
import { SQLocalKysely } from "sqlocal/kysely";
|
||||
import type { ClientConfig } from "sqlocal";
|
||||
|
||||
const plugins = [new ParseJSONResultsPlugin()];
|
||||
|
||||
export type SQLocalConnectionConfig = Omit<ClientConfig, "databasePath"> & {
|
||||
// make it optional
|
||||
databasePath?: ClientConfig["databasePath"];
|
||||
};
|
||||
|
||||
export class SQLocalConnection extends SqliteConnection {
|
||||
private _client: SQLocalKysely | undefined;
|
||||
|
||||
constructor(private config: SQLocalConnectionConfig) {
|
||||
super(null as any, {}, plugins);
|
||||
}
|
||||
|
||||
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!;
|
||||
}
|
||||
}
|
||||
1
packages/sqlocal/src/index.ts
Normal file
1
packages/sqlocal/src/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { SQLocalConnection, type SQLocalConnectionConfig } from "./SQLocalConnection";
|
||||
10
packages/sqlocal/test/base.test.ts
Normal file
10
packages/sqlocal/test/base.test.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { SQLocal } from "sqlocal";
|
||||
|
||||
describe("base", () => {
|
||||
const { sql } = new SQLocal(":memory:");
|
||||
|
||||
it("works", async () => {
|
||||
expect(await sql`SELECT 1`).toEqual([{ "1": 1 }]);
|
||||
});
|
||||
});
|
||||
16
packages/sqlocal/test/connection.test.ts
Normal file
16
packages/sqlocal/test/connection.test.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { SQLocalConnection, type SQLocalConnectionConfig } from "../src";
|
||||
|
||||
describe(SQLocalConnection, () => {
|
||||
function create(config: SQLocalConnectionConfig = {}) {
|
||||
return new SQLocalConnection(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 }]);
|
||||
});
|
||||
});
|
||||
100
packages/sqlocal/test/integration.test.ts
Normal file
100
packages/sqlocal/test/integration.test.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { SQLocalConnection, type SQLocalConnectionConfig } from "../src";
|
||||
import { createApp } from "bknd";
|
||||
import * as proto from "bknd/data";
|
||||
|
||||
describe("integration", () => {
|
||||
function create(config: SQLocalConnectionConfig = { databasePath: ":memory:" }) {
|
||||
return new SQLocalConnection(config);
|
||||
}
|
||||
|
||||
it("should create app and ping", async () => {
|
||||
const app = createApp({
|
||||
connection: create(),
|
||||
});
|
||||
await app.build();
|
||||
|
||||
expect(app.version()).toBeDefined();
|
||||
expect(await app.em.ping()).toBe(true);
|
||||
});
|
||||
|
||||
it("should create a basic schema", async () => {
|
||||
const schema = proto.em(
|
||||
{
|
||||
posts: proto.entity("posts", {
|
||||
title: proto.text().required(),
|
||||
content: proto.text(),
|
||||
}),
|
||||
comments: proto.entity("comments", {
|
||||
content: proto.text(),
|
||||
}),
|
||||
},
|
||||
(fns, s) => {
|
||||
fns.relation(s.comments).manyToOne(s.posts);
|
||||
fns.index(s.posts).on(["title"], true);
|
||||
},
|
||||
);
|
||||
|
||||
const app = createApp({
|
||||
connection: create(),
|
||||
initialConfig: {
|
||||
data: schema.toJSON(),
|
||||
},
|
||||
});
|
||||
|
||||
await app.build();
|
||||
|
||||
expect(app.em.entities.length).toBe(2);
|
||||
expect(app.em.entities.map((e) => e.name)).toEqual(["posts", "comments"]);
|
||||
|
||||
const api = app.getApi();
|
||||
|
||||
expect(
|
||||
(
|
||||
await api.data.createMany("posts", [
|
||||
{
|
||||
title: "Hello",
|
||||
content: "World",
|
||||
},
|
||||
{
|
||||
title: "Hello 2",
|
||||
content: "World 2",
|
||||
},
|
||||
])
|
||||
).data,
|
||||
).toEqual([
|
||||
{
|
||||
id: 1,
|
||||
title: "Hello",
|
||||
content: "World",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "Hello 2",
|
||||
content: "World 2",
|
||||
},
|
||||
] as any);
|
||||
|
||||
// try to create an existing
|
||||
expect(
|
||||
(
|
||||
await api.data.createOne("posts", {
|
||||
title: "Hello",
|
||||
})
|
||||
).ok,
|
||||
).toBe(false);
|
||||
|
||||
// add a comment to a post
|
||||
await api.data.createOne("comments", {
|
||||
content: "Hello",
|
||||
posts_id: 1,
|
||||
});
|
||||
|
||||
// and then query using a `with` property
|
||||
const result = await api.data.readMany("posts", { with: ["comments"] });
|
||||
expect(result.length).toBe(2);
|
||||
expect(result[0].comments.length).toBe(1);
|
||||
expect(result[0].comments[0].content).toBe("Hello");
|
||||
expect(result[1].comments.length).toBe(0);
|
||||
});
|
||||
});
|
||||
29
packages/sqlocal/tsconfig.json
Normal file
29
packages/sqlocal/tsconfig.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": false,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": false,
|
||||
"target": "ES2022",
|
||||
"noImplicitAny": false,
|
||||
"allowJs": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"declaration": true,
|
||||
"strict": true,
|
||||
"allowUnusedLabels": false,
|
||||
"allowUnreachableCode": false,
|
||||
"exactOptionalPropertyTypes": false,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noImplicitOverride": true,
|
||||
"noImplicitReturns": true,
|
||||
"noPropertyAccessFromIndexSignature": false,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"isolatedModules": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": ["./src/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
36
packages/sqlocal/vitest.config.ts
Normal file
36
packages/sqlocal/vitest.config.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
/// <reference types="vitest" />
|
||||
/// <reference types="@vitest/browser/providers/webdriverio" />
|
||||
import { defineConfig } from "vite";
|
||||
|
||||
// https://github.com/DallasHoff/sqlocal/blob/main/vite.config.ts
|
||||
export default defineConfig({
|
||||
test: {
|
||||
testTimeout: 1000,
|
||||
hookTimeout: 1000,
|
||||
teardownTimeout: 1000,
|
||||
includeTaskLocation: true,
|
||||
browser: {
|
||||
enabled: true,
|
||||
headless: true,
|
||||
screenshotFailures: false,
|
||||
provider: "webdriverio",
|
||||
instances: [{ browser: "chrome" }],
|
||||
},
|
||||
},
|
||||
optimizeDeps: {
|
||||
exclude: ["@sqlite.org/sqlite-wasm"],
|
||||
},
|
||||
plugins: [
|
||||
{
|
||||
enforce: "pre",
|
||||
name: "configure-response-headers",
|
||||
configureServer: (server) => {
|
||||
server.middlewares.use((_req, res, next) => {
|
||||
res.setHeader("Cross-Origin-Embedder-Policy", "require-corp");
|
||||
res.setHeader("Cross-Origin-Opener-Policy", "same-origin");
|
||||
next();
|
||||
});
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
Reference in New Issue
Block a user