mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-15 20:17:22 +00:00
feat: move postgres as part of the main repo
This commit is contained in:
17
.github/workflows/test.yml
vendored
17
.github/workflows/test.yml
vendored
@@ -9,6 +9,21 @@ jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:17
|
||||
env:
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_DB: bknd
|
||||
ports:
|
||||
- 5430:5432
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
@@ -20,7 +35,7 @@ jobs:
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v1
|
||||
with:
|
||||
bun-version: "1.2.22"
|
||||
bun-version: "1.3.1"
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: ./app
|
||||
|
||||
@@ -15,7 +15,7 @@ const mockedBackend = new Hono()
|
||||
.get("/file/:name", async (c) => {
|
||||
const { name } = c.req.param();
|
||||
const file = Bun.file(`${assetsPath}/${name}`);
|
||||
return new Response(file, {
|
||||
return new Response(new File([await file.bytes()], name, { type: file.type }), {
|
||||
headers: {
|
||||
"Content-Type": file.type,
|
||||
"Content-Length": file.size.toString(),
|
||||
@@ -67,7 +67,7 @@ describe("MediaApi", () => {
|
||||
const res = await mockedBackend.request("/api/media/file/" + name);
|
||||
await Bun.write(path, res);
|
||||
|
||||
const file = await Bun.file(path);
|
||||
const file = Bun.file(path);
|
||||
expect(file.size).toBeGreaterThan(0);
|
||||
expect(file.type).toBe("image/png");
|
||||
await file.delete();
|
||||
@@ -154,15 +154,12 @@ describe("MediaApi", () => {
|
||||
}
|
||||
|
||||
// upload via readable from bun
|
||||
await matches(await api.upload(file.stream(), { filename: "readable.png" }), "readable.png");
|
||||
await matches(api.upload(file.stream(), { filename: "readable.png" }), "readable.png");
|
||||
|
||||
// upload via readable from response
|
||||
{
|
||||
const response = (await mockedBackend.request(url)) as Response;
|
||||
await matches(
|
||||
await api.upload(response.body!, { filename: "readable.png" }),
|
||||
"readable.png",
|
||||
);
|
||||
await matches(api.upload(response.body!, { filename: "readable.png" }), "readable.png");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import { describe, expect, mock, test } from "bun:test";
|
||||
import { describe, expect, mock, test, beforeAll, afterAll } from "bun:test";
|
||||
import { createApp as internalCreateApp, type CreateAppConfig } from "bknd";
|
||||
import { getDummyConnection } from "../../__test__/helper";
|
||||
import { ModuleManager } from "modules/ModuleManager";
|
||||
import { em, entity, text } from "data/prototype";
|
||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||
|
||||
beforeAll(disableConsoleLog);
|
||||
afterAll(enableConsoleLog);
|
||||
|
||||
async function createApp(config: CreateAppConfig = {}) {
|
||||
const app = internalCreateApp({
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { AppEvents } from "App";
|
||||
import { describe, test, expect, beforeAll, mock } from "bun:test";
|
||||
import { describe, test, expect, beforeAll, mock, afterAll } from "bun:test";
|
||||
import { type App, createApp, createMcpToolCaller } from "core/test/utils";
|
||||
import type { McpServer } from "bknd/utils";
|
||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||
|
||||
beforeAll(disableConsoleLog);
|
||||
afterAll(enableConsoleLog);
|
||||
|
||||
/**
|
||||
* - [x] system_config
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { describe, expect, test, beforeAll, afterAll } from "bun:test";
|
||||
import { Guard, type GuardConfig } from "auth/authorize/Guard";
|
||||
import { Permission } from "auth/authorize/Permission";
|
||||
import { Role, type RoleSchema } from "auth/authorize/Role";
|
||||
import { objectTransform, s } from "bknd/utils";
|
||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||
|
||||
beforeAll(disableConsoleLog);
|
||||
afterAll(enableConsoleLog);
|
||||
|
||||
function createGuard(
|
||||
permissionNames: string[],
|
||||
|
||||
@@ -7,8 +7,8 @@ import type { App, DB } from "bknd";
|
||||
import type { CreateUserPayload } from "auth/AppAuth";
|
||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||
|
||||
beforeAll(() => disableConsoleLog());
|
||||
afterAll(() => enableConsoleLog());
|
||||
beforeAll(disableConsoleLog);
|
||||
afterAll(enableConsoleLog);
|
||||
|
||||
async function makeApp(config: Partial<CreateAppConfig["config"]> = {}) {
|
||||
const app = createApp({
|
||||
|
||||
78
app/__test__/data/postgres.test.ts
Normal file
78
app/__test__/data/postgres.test.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { describe, beforeAll, afterAll, expect, test, it, afterEach } from "bun:test";
|
||||
import type { PostgresConnection } from "data/connection/postgres";
|
||||
import { createApp, em, entity, text, pg, postgresJs } from "bknd";
|
||||
import { disableConsoleLog, enableConsoleLog, $waitUntil } from "bknd/utils";
|
||||
import { $ } from "bun";
|
||||
import { connectionTestSuite } from "data/connection/connection-test-suite";
|
||||
import { bunTestRunner } from "adapter/bun/test";
|
||||
|
||||
const credentials = {
|
||||
host: "localhost",
|
||||
port: 5430,
|
||||
user: "postgres",
|
||||
password: "postgres",
|
||||
database: "bknd",
|
||||
};
|
||||
|
||||
async function cleanDatabase(connection: InstanceType<typeof PostgresConnection>) {
|
||||
const kysely = connection.kysely;
|
||||
|
||||
// drop all tables+indexes & create new schema
|
||||
await kysely.schema.dropSchema("public").ifExists().cascade().execute();
|
||||
await kysely.schema.dropIndex("public").ifExists().cascade().execute();
|
||||
await kysely.schema.createSchema("public").execute();
|
||||
}
|
||||
|
||||
async function isPostgresRunning() {
|
||||
try {
|
||||
await $`docker exec bknd-test-postgres pg_isready -U ${credentials.user}`;
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
describe("postgres", () => {
|
||||
beforeAll(async () => {
|
||||
if (!(await isPostgresRunning())) {
|
||||
await $`docker run --rm --name bknd-test-postgres -d -e POSTGRES_PASSWORD=${credentials.password} -e POSTGRES_USER=${credentials.user} -e POSTGRES_DB=${credentials.database} -p ${credentials.port}:5432 postgres:17`;
|
||||
await $waitUntil("Postgres is running", isPostgresRunning);
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
}
|
||||
|
||||
disableConsoleLog();
|
||||
});
|
||||
afterAll(async () => {
|
||||
if (await isPostgresRunning()) {
|
||||
await $`docker stop bknd-test-postgres`;
|
||||
}
|
||||
|
||||
enableConsoleLog();
|
||||
});
|
||||
|
||||
describe.serial.each([
|
||||
["pg", () => pg(credentials)],
|
||||
["postgresjs", () => postgresJs(credentials)],
|
||||
])("%s", (name, createConnection) => {
|
||||
connectionTestSuite(
|
||||
{
|
||||
...bunTestRunner,
|
||||
test: test.serial,
|
||||
},
|
||||
{
|
||||
makeConnection: () => {
|
||||
const connection = createConnection();
|
||||
return {
|
||||
connection,
|
||||
dispose: async () => {
|
||||
await cleanDatabase(connection);
|
||||
await connection.close();
|
||||
},
|
||||
};
|
||||
},
|
||||
rawDialectDetails: [],
|
||||
disableConsoleLog: false,
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -59,7 +59,7 @@ describe("SqliteIntrospector", () => {
|
||||
dataType: "INTEGER",
|
||||
isNullable: false,
|
||||
isAutoIncrementing: true,
|
||||
hasDefaultValue: false,
|
||||
hasDefaultValue: true,
|
||||
comment: undefined,
|
||||
},
|
||||
{
|
||||
@@ -89,7 +89,7 @@ describe("SqliteIntrospector", () => {
|
||||
dataType: "INTEGER",
|
||||
isNullable: false,
|
||||
isAutoIncrementing: true,
|
||||
hasDefaultValue: false,
|
||||
hasDefaultValue: true,
|
||||
comment: undefined,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -10,7 +10,7 @@ import { assetsPath, assetsTmpPath } from "../helper";
|
||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||
|
||||
beforeAll(() => {
|
||||
//disableConsoleLog();
|
||||
disableConsoleLog();
|
||||
registries.media.register("local", StorageLocalAdapter);
|
||||
});
|
||||
afterAll(enableConsoleLog);
|
||||
|
||||
@@ -10,12 +10,6 @@ beforeAll(disableConsoleLog);
|
||||
afterAll(enableConsoleLog);
|
||||
|
||||
describe("AppAuth", () => {
|
||||
test.skip("...", () => {
|
||||
const auth = new AppAuth({});
|
||||
console.log(auth.toJSON());
|
||||
console.log(auth.config);
|
||||
});
|
||||
|
||||
moduleTestSuite(AppAuth);
|
||||
|
||||
let ctx: ModuleBuildContext;
|
||||
@@ -39,11 +33,9 @@ describe("AppAuth", () => {
|
||||
await auth.build();
|
||||
|
||||
const oldConfig = auth.toJSON(true);
|
||||
//console.log(oldConfig);
|
||||
await auth.schema().patch("enabled", true);
|
||||
await auth.build();
|
||||
const newConfig = auth.toJSON(true);
|
||||
//console.log(newConfig);
|
||||
expect(newConfig.jwt.secret).not.toBe(oldConfig.jwt.secret);
|
||||
});
|
||||
|
||||
@@ -69,7 +61,6 @@ describe("AppAuth", () => {
|
||||
const app = new AuthController(auth).getController();
|
||||
|
||||
{
|
||||
disableConsoleLog();
|
||||
const res = await app.request("/password/register", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
@@ -80,7 +71,6 @@ describe("AppAuth", () => {
|
||||
password: "12345678",
|
||||
}),
|
||||
});
|
||||
enableConsoleLog();
|
||||
expect(res.status).toBe(200);
|
||||
|
||||
const { data: users } = await ctx.em.repository("users").findMany();
|
||||
@@ -119,7 +109,6 @@ describe("AppAuth", () => {
|
||||
const app = new AuthController(auth).getController();
|
||||
|
||||
{
|
||||
disableConsoleLog();
|
||||
const res = await app.request("/password/register", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
@@ -130,7 +119,6 @@ describe("AppAuth", () => {
|
||||
password: "12345678",
|
||||
}),
|
||||
});
|
||||
enableConsoleLog();
|
||||
expect(res.status).toBe(200);
|
||||
|
||||
const { data: users } = await ctx.em.repository("users").findMany();
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { describe, expect, test, beforeAll, afterAll } from "bun:test";
|
||||
import { createApp } from "core/test/utils";
|
||||
import { em, entity, text } from "data/prototype";
|
||||
import { registries } from "modules/registries";
|
||||
import { StorageLocalAdapter } from "adapter/node/storage/StorageLocalAdapter";
|
||||
import { AppMedia } from "../../src/media/AppMedia";
|
||||
import { moduleTestSuite } from "./module-test-suite";
|
||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||
|
||||
beforeAll(disableConsoleLog);
|
||||
afterAll(enableConsoleLog);
|
||||
|
||||
describe("AppMedia", () => {
|
||||
test.skip("...", () => {
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { it, expect, describe } from "bun:test";
|
||||
import { it, expect, describe, beforeAll, afterAll } from "bun:test";
|
||||
import { DbModuleManager } from "modules/db/DbModuleManager";
|
||||
import { getDummyConnection } from "../helper";
|
||||
import { TABLE_NAME } from "modules/db/migrations";
|
||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||
|
||||
beforeAll(disableConsoleLog);
|
||||
afterAll(enableConsoleLog);
|
||||
|
||||
describe("DbModuleManager", () => {
|
||||
it("should extract secrets", async () => {
|
||||
|
||||
@@ -11,7 +11,7 @@ import { s, stripMark } from "core/utils/schema";
|
||||
import { Connection } from "data/connection/Connection";
|
||||
import { entity, text } from "data/prototype";
|
||||
|
||||
beforeAll(disableConsoleLog);
|
||||
beforeAll(() => disableConsoleLog());
|
||||
afterAll(enableConsoleLog);
|
||||
|
||||
describe("ModuleManager", async () => {
|
||||
@@ -82,7 +82,6 @@ describe("ModuleManager", async () => {
|
||||
},
|
||||
},
|
||||
} as any;
|
||||
//const { version, ...json } = mm.toJSON() as any;
|
||||
|
||||
const { dummyConnection } = getDummyConnection();
|
||||
const db = dummyConnection.kysely;
|
||||
@@ -97,10 +96,6 @@ describe("ModuleManager", async () => {
|
||||
|
||||
await mm2.build();
|
||||
|
||||
/* console.log({
|
||||
json,
|
||||
configs: mm2.configs(),
|
||||
}); */
|
||||
//expect(stripMark(json)).toEqual(stripMark(mm2.configs()));
|
||||
expect(mm2.configs().data.entities?.test).toBeDefined();
|
||||
expect(mm2.configs().data.entities?.test?.fields?.content).toBeDefined();
|
||||
@@ -228,8 +223,6 @@ describe("ModuleManager", async () => {
|
||||
const c = getDummyConnection();
|
||||
const mm = new ModuleManager(c.dummyConnection);
|
||||
await mm.build();
|
||||
console.log("==".repeat(30));
|
||||
console.log("");
|
||||
const json = mm.configs();
|
||||
|
||||
const c2 = getDummyConnection();
|
||||
@@ -275,7 +268,6 @@ describe("ModuleManager", async () => {
|
||||
}
|
||||
|
||||
override async build() {
|
||||
//console.log("building FailingModule", this.config);
|
||||
if (this.config.value && this.config.value < 0) {
|
||||
throw new Error("value must be positive, given: " + this.config.value);
|
||||
}
|
||||
@@ -296,9 +288,6 @@ describe("ModuleManager", async () => {
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(() => disableConsoleLog(["log", "warn", "error"]));
|
||||
afterEach(enableConsoleLog);
|
||||
|
||||
test("it builds", async () => {
|
||||
const { dummyConnection } = getDummyConnection();
|
||||
const mm = new TestModuleManager(dummyConnection);
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
"bugs": {
|
||||
"url": "https://github.com/bknd-io/bknd/issues"
|
||||
},
|
||||
"packageManager": "bun@1.2.22",
|
||||
"packageManager": "bun@1.3.1",
|
||||
"engines": {
|
||||
"node": ">=22.13"
|
||||
},
|
||||
@@ -104,8 +104,8 @@
|
||||
"dotenv": "^16.4.7",
|
||||
"jotai": "^2.12.2",
|
||||
"jsdom": "^26.0.0",
|
||||
"kysely-d1": "^0.3.0",
|
||||
"kysely-generic-sqlite": "^1.2.1",
|
||||
"kysely-postgres-js": "^2.0.0",
|
||||
"libsql-stateless-easy": "^1.8.0",
|
||||
"open": "^10.1.0",
|
||||
"openapi-types": "^12.1.3",
|
||||
@@ -139,7 +139,17 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=19",
|
||||
"react-dom": ">=19"
|
||||
"react-dom": ">=19",
|
||||
"pg": "*",
|
||||
"postgres": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"pg": {
|
||||
"optional": true
|
||||
},
|
||||
"postgres": {
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.js",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { MaybePromise } from "core/types";
|
||||
import { getRuntimeKey as honoGetRuntimeKey } from "hono/adapter";
|
||||
|
||||
/**
|
||||
@@ -77,3 +78,21 @@ export function threw(fn: () => any, instance?: new (...args: any[]) => Error) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export async function $waitUntil(
|
||||
message: string,
|
||||
condition: () => MaybePromise<boolean>,
|
||||
delay = 100,
|
||||
maxAttempts = 10,
|
||||
) {
|
||||
let attempts = 0;
|
||||
while (attempts < maxAttempts) {
|
||||
if (await condition()) {
|
||||
return;
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, delay));
|
||||
attempts++;
|
||||
}
|
||||
|
||||
throw new Error(`$waitUntil: "${message}" failed after ${maxAttempts} attempts`);
|
||||
}
|
||||
|
||||
@@ -14,27 +14,31 @@ export function connectionTestSuite(
|
||||
{
|
||||
makeConnection,
|
||||
rawDialectDetails,
|
||||
disableConsoleLog: _disableConsoleLog = true,
|
||||
}: {
|
||||
makeConnection: () => MaybePromise<{
|
||||
connection: Connection;
|
||||
dispose: () => MaybePromise<void>;
|
||||
}>;
|
||||
rawDialectDetails: string[];
|
||||
disableConsoleLog?: boolean;
|
||||
},
|
||||
) {
|
||||
const { test, expect, describe, beforeEach, afterEach, afterAll, beforeAll } = testRunner;
|
||||
beforeAll(() => disableConsoleLog());
|
||||
afterAll(() => enableConsoleLog());
|
||||
if (_disableConsoleLog) {
|
||||
beforeAll(() => disableConsoleLog());
|
||||
afterAll(() => enableConsoleLog());
|
||||
}
|
||||
|
||||
describe("base", () => {
|
||||
let ctx: Awaited<ReturnType<typeof makeConnection>>;
|
||||
beforeEach(async () => {
|
||||
ctx = await makeConnection();
|
||||
});
|
||||
afterEach(async () => {
|
||||
await ctx.dispose();
|
||||
});
|
||||
let ctx: Awaited<ReturnType<typeof makeConnection>>;
|
||||
beforeEach(async () => {
|
||||
ctx = await makeConnection();
|
||||
});
|
||||
afterEach(async () => {
|
||||
await ctx.dispose();
|
||||
});
|
||||
|
||||
describe("base", async () => {
|
||||
test("pings", async () => {
|
||||
const res = await ctx.connection.ping();
|
||||
expect(res).toBe(true);
|
||||
@@ -98,52 +102,54 @@ export function connectionTestSuite(
|
||||
});
|
||||
|
||||
describe("schema", async () => {
|
||||
const { connection, dispose } = await makeConnection();
|
||||
afterAll(async () => {
|
||||
await dispose();
|
||||
});
|
||||
const makeSchema = async () => {
|
||||
const fields = [
|
||||
{
|
||||
type: "integer",
|
||||
name: "id",
|
||||
primary: true,
|
||||
},
|
||||
{
|
||||
type: "text",
|
||||
name: "text",
|
||||
},
|
||||
{
|
||||
type: "json",
|
||||
name: "json",
|
||||
},
|
||||
] as const satisfies FieldSpec[];
|
||||
|
||||
const fields = [
|
||||
{
|
||||
type: "integer",
|
||||
name: "id",
|
||||
primary: true,
|
||||
},
|
||||
{
|
||||
type: "text",
|
||||
name: "text",
|
||||
},
|
||||
{
|
||||
type: "json",
|
||||
name: "json",
|
||||
},
|
||||
] as const satisfies FieldSpec[];
|
||||
let b = ctx.connection.kysely.schema.createTable("test");
|
||||
for (const field of fields) {
|
||||
// @ts-expect-error
|
||||
b = b.addColumn(...ctx.connection.getFieldSchema(field));
|
||||
}
|
||||
await b.execute();
|
||||
|
||||
let b = connection.kysely.schema.createTable("test");
|
||||
for (const field of fields) {
|
||||
// @ts-expect-error
|
||||
b = b.addColumn(...connection.getFieldSchema(field));
|
||||
}
|
||||
await b.execute();
|
||||
|
||||
// add index
|
||||
await connection.kysely.schema.createIndex("test_index").on("test").columns(["id"]).execute();
|
||||
// add index
|
||||
await ctx.connection.kysely.schema
|
||||
.createIndex("test_index")
|
||||
.on("test")
|
||||
.columns(["id"])
|
||||
.execute();
|
||||
};
|
||||
|
||||
test("executes query", async () => {
|
||||
await connection.kysely
|
||||
await makeSchema();
|
||||
await ctx.connection.kysely
|
||||
.insertInto("test")
|
||||
.values({ id: 1, text: "test", json: JSON.stringify({ a: 1 }) })
|
||||
.execute();
|
||||
|
||||
const expected = { id: 1, text: "test", json: { a: 1 } };
|
||||
|
||||
const qb = connection.kysely.selectFrom("test").selectAll();
|
||||
const res = await connection.executeQuery(qb);
|
||||
const qb = ctx.connection.kysely.selectFrom("test").selectAll();
|
||||
const res = await ctx.connection.executeQuery(qb);
|
||||
expect(res.rows).toEqual([expected]);
|
||||
expect(rawDialectDetails.every((detail) => getPath(res, detail) !== undefined)).toBe(true);
|
||||
|
||||
{
|
||||
const res = await connection.executeQueries(qb, qb);
|
||||
const res = await ctx.connection.executeQueries(qb, qb);
|
||||
expect(res.length).toBe(2);
|
||||
res.map((r) => {
|
||||
expect(r.rows).toEqual([expected]);
|
||||
@@ -155,15 +161,21 @@ export function connectionTestSuite(
|
||||
});
|
||||
|
||||
test("introspects", async () => {
|
||||
const tables = await connection.getIntrospector().getTables({
|
||||
await makeSchema();
|
||||
const tables = await ctx.connection.getIntrospector().getTables({
|
||||
withInternalKyselyTables: false,
|
||||
});
|
||||
const clean = tables.map((t) => ({
|
||||
...t,
|
||||
columns: t.columns.map((c) => ({
|
||||
...c,
|
||||
dataType: undefined,
|
||||
})),
|
||||
columns: t.columns
|
||||
.map((c) => ({
|
||||
...c,
|
||||
// ignore data type
|
||||
dataType: undefined,
|
||||
// ignore default value if "id"
|
||||
hasDefaultValue: c.name !== "id" ? c.hasDefaultValue : undefined,
|
||||
}))
|
||||
.sort((a, b) => a.name.localeCompare(b.name)),
|
||||
}));
|
||||
|
||||
expect(clean).toEqual([
|
||||
@@ -176,14 +188,8 @@ export function connectionTestSuite(
|
||||
dataType: undefined,
|
||||
isNullable: false,
|
||||
isAutoIncrementing: true,
|
||||
hasDefaultValue: false,
|
||||
},
|
||||
{
|
||||
name: "text",
|
||||
dataType: undefined,
|
||||
isNullable: true,
|
||||
isAutoIncrementing: false,
|
||||
hasDefaultValue: false,
|
||||
hasDefaultValue: undefined,
|
||||
comment: undefined,
|
||||
},
|
||||
{
|
||||
name: "json",
|
||||
@@ -191,25 +197,34 @@ export function connectionTestSuite(
|
||||
isNullable: true,
|
||||
isAutoIncrementing: false,
|
||||
hasDefaultValue: false,
|
||||
comment: undefined,
|
||||
},
|
||||
{
|
||||
name: "text",
|
||||
dataType: undefined,
|
||||
isNullable: true,
|
||||
isAutoIncrementing: false,
|
||||
hasDefaultValue: false,
|
||||
comment: undefined,
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
expect(await ctx.connection.getIntrospector().getIndices()).toEqual([
|
||||
{
|
||||
name: "test_index",
|
||||
table: "test",
|
||||
isUnique: false,
|
||||
columns: [
|
||||
{
|
||||
name: "id",
|
||||
order: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
expect(await connection.getIntrospector().getIndices()).toEqual([
|
||||
{
|
||||
name: "test_index",
|
||||
table: "test",
|
||||
isUnique: false,
|
||||
columns: [
|
||||
{
|
||||
name: "id",
|
||||
order: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
describe("integration", async () => {
|
||||
|
||||
@@ -6,9 +6,8 @@ import $pg from "pg";
|
||||
|
||||
export type PgPostgresConnectionConfig = $pg.PoolConfig;
|
||||
|
||||
export class PgPostgresConnection extends PostgresConnection {
|
||||
export class PgPostgresConnection extends PostgresConnection<$pg.Pool> {
|
||||
override name = "pg";
|
||||
private pool: $pg.Pool;
|
||||
|
||||
constructor(config: PgPostgresConnectionConfig) {
|
||||
const pool = new $pg.Pool(config);
|
||||
@@ -20,11 +19,11 @@ export class PgPostgresConnection extends PostgresConnection {
|
||||
});
|
||||
|
||||
super(kysely);
|
||||
this.pool = pool;
|
||||
this.client = pool;
|
||||
}
|
||||
|
||||
override async close(): Promise<void> {
|
||||
await this.pool.end();
|
||||
await this.client.end();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ export type QB = SelectQueryBuilder<any, any, any>;
|
||||
|
||||
export const plugins = [new ParseJSONResultsPlugin()];
|
||||
|
||||
export abstract class PostgresConnection extends Connection {
|
||||
export abstract class PostgresConnection<Client = unknown> extends Connection<Client> {
|
||||
protected override readonly supported = {
|
||||
batching: true,
|
||||
softscans: true,
|
||||
@@ -68,7 +68,7 @@ export abstract class PostgresConnection extends Connection {
|
||||
type,
|
||||
(col: ColumnDefinitionBuilder) => {
|
||||
if (spec.primary) {
|
||||
return col.primaryKey();
|
||||
return col.primaryKey().notNull();
|
||||
}
|
||||
if (spec.references) {
|
||||
return col
|
||||
@@ -76,7 +76,7 @@ export abstract class PostgresConnection extends Connection {
|
||||
.onDelete(spec.onDelete ?? "set null")
|
||||
.onUpdate(spec.onUpdate ?? "no action");
|
||||
}
|
||||
return spec.nullable ? col : col.notNull();
|
||||
return col;
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -102,26 +102,27 @@ export class PostgresIntrospector extends BaseIntrospector {
|
||||
return tables.map((table) => ({
|
||||
name: table.name,
|
||||
isView: table.type === "VIEW",
|
||||
columns: table.columns.map((col) => {
|
||||
return {
|
||||
name: col.name,
|
||||
dataType: col.type,
|
||||
isNullable: !col.notnull,
|
||||
// @todo: check default value on 'nextval' see https://www.postgresql.org/docs/17/datatype-numeric.html#DATATYPE-SERIAL
|
||||
isAutoIncrementing: true, // just for now
|
||||
hasDefaultValue: col.dflt != null,
|
||||
comment: undefined,
|
||||
};
|
||||
}),
|
||||
indices: table.indices.map((index) => ({
|
||||
name: index.name,
|
||||
table: table.name,
|
||||
isUnique: index.sql?.match(/unique/i) != null,
|
||||
columns: index.columns.map((col) => ({
|
||||
name: col.name,
|
||||
order: col.seqno,
|
||||
})),
|
||||
columns: table.columns.map((col) => ({
|
||||
name: col.name,
|
||||
dataType: col.type,
|
||||
isNullable: !col.notnull,
|
||||
isAutoIncrementing: col.dflt?.toLowerCase().includes("nextval") ?? false,
|
||||
hasDefaultValue: col.dflt != null,
|
||||
comment: undefined,
|
||||
})),
|
||||
indices: table.indices
|
||||
// filter out db-managed primary key index
|
||||
.filter((index) => index.name !== `${table.name}_pkey`)
|
||||
.map((index) => ({
|
||||
name: index.name,
|
||||
table: table.name,
|
||||
isUnique: index.sql?.match(/unique/i) != null,
|
||||
columns: index.columns.map((col) => ({
|
||||
name: col.name,
|
||||
// seqno starts at 1
|
||||
order: col.seqno - 1,
|
||||
})),
|
||||
})),
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -7,12 +7,10 @@ import $postgresJs, { type Sql, type Options, type PostgresType } from "postgres
|
||||
|
||||
export type PostgresJsConfig = Options<Record<string, PostgresType>>;
|
||||
|
||||
export class PostgresJsConnection extends PostgresConnection {
|
||||
export class PostgresJsConnection extends PostgresConnection<$postgresJs.Sql> {
|
||||
override name = "postgres-js";
|
||||
|
||||
private postgres: Sql;
|
||||
|
||||
constructor(opts: { postgres: Sql }) {
|
||||
constructor(opts: { postgres: $postgresJs.Sql }) {
|
||||
const kysely = new Kysely({
|
||||
dialect: customIntrospector(PostgresJSDialect, PostgresIntrospector, {
|
||||
excludeTables: [],
|
||||
@@ -21,11 +19,11 @@ export class PostgresJsConnection extends PostgresConnection {
|
||||
});
|
||||
|
||||
super(kysely);
|
||||
this.postgres = opts.postgres;
|
||||
this.client = opts.postgres;
|
||||
}
|
||||
|
||||
override async close(): Promise<void> {
|
||||
await this.postgres.end();
|
||||
await this.client.end();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ import { customIntrospector } from "../Connection";
|
||||
import { SqliteIntrospector } from "./SqliteIntrospector";
|
||||
import type { Field } from "data/fields/Field";
|
||||
|
||||
// @todo: add pragmas
|
||||
export type SqliteConnectionConfig<
|
||||
CustomDialect extends Constructor<Dialect> = Constructor<Dialect>,
|
||||
> = {
|
||||
|
||||
@@ -83,7 +83,7 @@ export class SqliteIntrospector extends BaseIntrospector {
|
||||
dataType: col.type,
|
||||
isNullable: !col.notnull,
|
||||
isAutoIncrementing: col.name === autoIncrementCol,
|
||||
hasDefaultValue: col.dflt_value != null,
|
||||
hasDefaultValue: col.name === autoIncrementCol ? true : col.dflt_value != null,
|
||||
comment: undefined,
|
||||
};
|
||||
}) ?? [],
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { test, describe, expect } from "bun:test";
|
||||
import { test, describe, expect, beforeAll, afterAll } from "bun:test";
|
||||
import * as q from "./query";
|
||||
import { parse as $parse, type ParseOptions } from "bknd/utils";
|
||||
import type { PrimaryFieldType } from "modules";
|
||||
import type { Generated } from "kysely";
|
||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||
|
||||
const parse = (v: unknown, o: ParseOptions = {}) =>
|
||||
$parse(q.repoQuery, v, {
|
||||
@@ -15,6 +16,9 @@ const decode = (input: any, output: any) => {
|
||||
expect(parse(input)).toEqual(output);
|
||||
};
|
||||
|
||||
beforeAll(() => disableConsoleLog());
|
||||
afterAll(() => enableConsoleLog());
|
||||
|
||||
describe("server/query", () => {
|
||||
test("limit & offset", () => {
|
||||
//expect(() => parse({ limit: false })).toThrow();
|
||||
|
||||
@@ -132,6 +132,8 @@ export type * from "data/entities/Entity";
|
||||
export type { EntityManager } from "data/entities/EntityManager";
|
||||
export type { SchemaManager } from "data/schema/SchemaManager";
|
||||
export type * from "data/entities";
|
||||
|
||||
// data connection
|
||||
export {
|
||||
BaseIntrospector,
|
||||
Connection,
|
||||
@@ -144,9 +146,28 @@ export {
|
||||
type ConnQuery,
|
||||
type ConnQueryResults,
|
||||
} from "data/connection";
|
||||
|
||||
// data sqlite
|
||||
export { SqliteConnection } from "data/connection/sqlite/SqliteConnection";
|
||||
export { SqliteIntrospector } from "data/connection/sqlite/SqliteIntrospector";
|
||||
export { SqliteLocalConnection } from "data/connection/sqlite/SqliteLocalConnection";
|
||||
|
||||
// data postgres
|
||||
export {
|
||||
pg,
|
||||
PgPostgresConnection,
|
||||
type PgPostgresConnectionConfig,
|
||||
} from "data/connection/postgres/PgPostgresConnection";
|
||||
export { PostgresIntrospector } from "data/connection/postgres/PostgresIntrospector";
|
||||
export { PostgresConnection } from "data/connection/postgres/PostgresConnection";
|
||||
export {
|
||||
postgresJs,
|
||||
PostgresJsConnection,
|
||||
type PostgresJsConfig,
|
||||
} from "data/connection/postgres/PostgresJsConnection";
|
||||
export { createCustomPostgresConnection } from "data/connection/postgres/custom";
|
||||
|
||||
// data prototype
|
||||
export {
|
||||
text,
|
||||
number,
|
||||
|
||||
12
bun.lock
12
bun.lock
@@ -15,7 +15,7 @@
|
||||
},
|
||||
"app": {
|
||||
"name": "bknd",
|
||||
"version": "0.18.1",
|
||||
"version": "0.19.0",
|
||||
"bin": "./dist/cli/index.js",
|
||||
"dependencies": {
|
||||
"@cfworker/json-schema": "^4.1.1",
|
||||
@@ -74,8 +74,8 @@
|
||||
"dotenv": "^16.4.7",
|
||||
"jotai": "^2.12.2",
|
||||
"jsdom": "^26.0.0",
|
||||
"kysely-d1": "^0.3.0",
|
||||
"kysely-generic-sqlite": "^1.2.1",
|
||||
"kysely-postgres-js": "^2.0.0",
|
||||
"libsql-stateless-easy": "^1.8.0",
|
||||
"miniflare": "^4.20250913.0",
|
||||
"open": "^10.1.0",
|
||||
@@ -108,9 +108,15 @@
|
||||
"@hono/node-server": "^1.14.3",
|
||||
},
|
||||
"peerDependencies": {
|
||||
"pg": "*",
|
||||
"postgres": "*",
|
||||
"react": ">=19",
|
||||
"react-dom": ">=19",
|
||||
},
|
||||
"optionalPeers": [
|
||||
"pg",
|
||||
"postgres",
|
||||
],
|
||||
},
|
||||
"packages/cli": {
|
||||
"name": "bknd-cli",
|
||||
@@ -2549,8 +2555,6 @@
|
||||
|
||||
"kysely": ["kysely@0.27.6", "", {}, "sha512-FIyV/64EkKhJmjgC0g2hygpBv5RNWVPyNCqSAD7eTCv6eFWNIi4PN1UvdSJGicN/o35bnevgis4Y0UDC0qi8jQ=="],
|
||||
|
||||
"kysely-d1": ["kysely-d1@0.3.0", "", { "peerDependencies": { "kysely": "*" } }, "sha512-9wTbE6ooLiYtBa4wPg9e4fjfcmvRtgE/2j9pAjYrIq+iz+EsH/Hj9YbtxpEXA6JoRgfulVQ1EtGj6aycGGRpYw=="],
|
||||
|
||||
"kysely-generic-sqlite": ["kysely-generic-sqlite@1.2.1", "", { "peerDependencies": { "kysely": ">=0.26" } }, "sha512-/Bs3/Uktn04nQ9g/4oSphLMEtSHkS5+j5hbKjK5gMqXQfqr/v3V3FKtoN4pLTmo2W35hNdrIpQnBukGL1zZc6g=="],
|
||||
|
||||
"kysely-neon": ["kysely-neon@1.3.0", "", { "peerDependencies": { "@neondatabase/serverless": "^0.4.3", "kysely": "0.x.x", "ws": "^8.13.0" }, "optionalPeers": ["ws"] }, "sha512-CIIlbmqpIXVJDdBEYtEOwbmALag0jmqYrGfBeM4cHKb9AgBGs+X1SvXUZ8TqkDacQEqEZN2XtsDoUkcMIISjHw=="],
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
import { describe } from "bun:test";
|
||||
import { pg } from "../src/PgPostgresConnection";
|
||||
import { testSuite } from "./suite";
|
||||
|
||||
describe("pg", () => {
|
||||
testSuite({
|
||||
createConnection: () =>
|
||||
pg({
|
||||
host: "localhost",
|
||||
port: 5430,
|
||||
user: "postgres",
|
||||
password: "postgres",
|
||||
database: "bknd",
|
||||
}),
|
||||
});
|
||||
});
|
||||
@@ -1,16 +0,0 @@
|
||||
import { describe } from "bun:test";
|
||||
import { postgresJs } from "../src/PostgresJsConnection";
|
||||
import { testSuite } from "./suite";
|
||||
|
||||
describe("postgresjs", () => {
|
||||
testSuite({
|
||||
createConnection: () =>
|
||||
postgresJs({
|
||||
host: "localhost",
|
||||
port: 5430,
|
||||
user: "postgres",
|
||||
password: "postgres",
|
||||
database: "bknd",
|
||||
}),
|
||||
});
|
||||
});
|
||||
@@ -1,218 +0,0 @@
|
||||
import { describe, beforeAll, afterAll, expect, it, afterEach } from "bun:test";
|
||||
import type { PostgresConnection } from "../src";
|
||||
import { createApp, em, entity, text } from "bknd";
|
||||
import { disableConsoleLog, enableConsoleLog } from "bknd/utils";
|
||||
// @ts-ignore
|
||||
import { connectionTestSuite } from "$bknd/data/connection/connection-test-suite";
|
||||
// @ts-ignore
|
||||
import { bunTestRunner } from "$bknd/adapter/bun/test";
|
||||
|
||||
export type TestSuiteConfig = {
|
||||
createConnection: () => InstanceType<typeof PostgresConnection>;
|
||||
cleanDatabase?: (connection: InstanceType<typeof PostgresConnection>) => Promise<void>;
|
||||
};
|
||||
|
||||
export async function defaultCleanDatabase(connection: InstanceType<typeof PostgresConnection>) {
|
||||
const kysely = connection.kysely;
|
||||
|
||||
// drop all tables+indexes & create new schema
|
||||
await kysely.schema.dropSchema("public").ifExists().cascade().execute();
|
||||
await kysely.schema.dropIndex("public").ifExists().cascade().execute();
|
||||
await kysely.schema.createSchema("public").execute();
|
||||
}
|
||||
|
||||
async function cleanDatabase(
|
||||
connection: InstanceType<typeof PostgresConnection>,
|
||||
config: TestSuiteConfig,
|
||||
) {
|
||||
if (config.cleanDatabase) {
|
||||
await config.cleanDatabase(connection);
|
||||
} else {
|
||||
await defaultCleanDatabase(connection);
|
||||
}
|
||||
}
|
||||
|
||||
export function testSuite(config: TestSuiteConfig) {
|
||||
beforeAll(() => disableConsoleLog(["log", "warn", "error"]));
|
||||
afterAll(() => enableConsoleLog());
|
||||
|
||||
// @todo: postgres seems to add multiple indexes, thus failing the test suite
|
||||
/* describe("test suite", () => {
|
||||
connectionTestSuite(bunTestRunner, {
|
||||
makeConnection: () => {
|
||||
const connection = config.createConnection();
|
||||
return {
|
||||
connection,
|
||||
dispose: async () => {
|
||||
await cleanDatabase(connection, config);
|
||||
await connection.close();
|
||||
},
|
||||
};
|
||||
},
|
||||
rawDialectDetails: [],
|
||||
});
|
||||
}); */
|
||||
|
||||
describe("base", () => {
|
||||
it("should connect to the database", async () => {
|
||||
const connection = config.createConnection();
|
||||
expect(await connection.ping()).toBe(true);
|
||||
});
|
||||
|
||||
it("should clean the database", async () => {
|
||||
const connection = config.createConnection();
|
||||
await cleanDatabase(connection, config);
|
||||
|
||||
const tables = await connection.getIntrospector().getTables();
|
||||
expect(tables).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("integration", () => {
|
||||
let connection: PostgresConnection;
|
||||
beforeAll(async () => {
|
||||
connection = config.createConnection();
|
||||
await cleanDatabase(connection, config);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await cleanDatabase(connection, config);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await connection.close();
|
||||
});
|
||||
|
||||
it("should create app and ping", async () => {
|
||||
const app = createApp({
|
||||
connection,
|
||||
});
|
||||
await app.build();
|
||||
|
||||
expect(app.version()).toBeDefined();
|
||||
expect(await app.em.ping()).toBe(true);
|
||||
});
|
||||
|
||||
it("should create a basic schema", async () => {
|
||||
const schema = em(
|
||||
{
|
||||
posts: entity("posts", {
|
||||
title: text().required(),
|
||||
content: text(),
|
||||
}),
|
||||
comments: entity("comments", {
|
||||
content: text(),
|
||||
}),
|
||||
},
|
||||
(fns, s) => {
|
||||
fns.relation(s.comments).manyToOne(s.posts);
|
||||
fns.index(s.posts).on(["title"], true);
|
||||
},
|
||||
);
|
||||
|
||||
const app = createApp({
|
||||
connection,
|
||||
config: {
|
||||
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);
|
||||
});
|
||||
|
||||
it("should support uuid", async () => {
|
||||
const schema = em(
|
||||
{
|
||||
posts: entity(
|
||||
"posts",
|
||||
{
|
||||
title: text().required(),
|
||||
content: text(),
|
||||
},
|
||||
{
|
||||
primary_format: "uuid",
|
||||
},
|
||||
),
|
||||
comments: entity("comments", {
|
||||
content: text(),
|
||||
}),
|
||||
},
|
||||
(fns, s) => {
|
||||
fns.relation(s.comments).manyToOne(s.posts);
|
||||
fns.index(s.posts).on(["title"], true);
|
||||
},
|
||||
);
|
||||
|
||||
const app = createApp({
|
||||
connection,
|
||||
config: {
|
||||
data: schema.toJSON(),
|
||||
},
|
||||
});
|
||||
|
||||
await app.build();
|
||||
const config = app.toJSON();
|
||||
// @ts-expect-error
|
||||
expect(config.data.entities?.posts.fields?.id.config?.format).toBe("uuid");
|
||||
|
||||
const $em = app.em;
|
||||
const mutator = $em.mutator($em.entity("posts"));
|
||||
const data = await mutator.insertOne({ title: "Hello", content: "World" });
|
||||
expect(data.data.id).toBeString();
|
||||
expect(String(data.data.id).length).toBe(36);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user