Merge remote-tracking branch 'origin/release/0.11' into feat/init-e2e

# Conflicts:
#	app/.gitignore
#	app/package.json
#	app/vite.dev.ts
#	bun.lock
This commit is contained in:
dswbx
2025-04-02 20:24:18 +02:00
86 changed files with 1768 additions and 815 deletions
+1 -1
View File
@@ -15,7 +15,7 @@ jobs:
- name: Setup Bun - name: Setup Bun
uses: oven-sh/setup-bun@v1 uses: oven-sh/setup-bun@v1
with: with:
bun-version: latest bun-version: "1.2.5"
- name: Install dependencies - name: Install dependencies
working-directory: ./app working-directory: ./app
+2 -1
View File
@@ -1,2 +1,3 @@
playwright-report playwright-report
test-results test-results
bknd.config.*
+62
View File
@@ -0,0 +1,62 @@
import { expect, describe, it, beforeAll, afterAll } from "bun:test";
import * as adapter from "adapter";
import { disableConsoleLog, enableConsoleLog } from "core/utils";
import { adapterTestSuite } from "adapter/adapter-test-suite";
import { bunTestRunner } from "adapter/bun/test";
beforeAll(disableConsoleLog);
afterAll(enableConsoleLog);
describe("adapter", () => {
it("makes config", () => {
expect(adapter.makeConfig({})).toEqual({});
expect(adapter.makeConfig({}, { env: { TEST: "test" } })).toEqual({});
// merges everything returned from `app` with the config
expect(adapter.makeConfig({ app: (a) => a as any }, { env: { TEST: "test" } })).toEqual({
env: { TEST: "test" },
} as any);
});
it("reuses apps correctly", async () => {
const id = crypto.randomUUID();
const first = await adapter.createAdapterApp(
{
initialConfig: { server: { cors: { origin: "random" } } },
},
undefined,
{ id },
);
const second = await adapter.createAdapterApp();
const third = await adapter.createAdapterApp(undefined, undefined, { id });
await first.build();
await second.build();
await third.build();
expect(first.toJSON().server.cors.origin).toEqual("random");
expect(first).toBe(third);
expect(first).not.toBe(second);
expect(second).not.toBe(third);
expect(second.toJSON().server.cors.origin).toEqual("*");
// recreate the first one
const first2 = await adapter.createAdapterApp(undefined, undefined, { id, force: true });
await first2.build();
expect(first2).not.toBe(first);
expect(first2).not.toBe(third);
expect(first2).not.toBe(second);
expect(first2.toJSON().server.cors.origin).toEqual("*");
});
adapterTestSuite(bunTestRunner, {
makeApp: adapter.createFrameworkApp,
label: "framework app",
});
adapterTestSuite(bunTestRunner, {
makeApp: adapter.createRuntimeApp,
label: "runtime app",
});
});
+11 -1
View File
@@ -5,7 +5,8 @@ import { DataApi } from "../../src/data/api/DataApi";
import { DataController } from "../../src/data/api/DataController"; import { DataController } from "../../src/data/api/DataController";
import { dataConfigSchema } from "../../src/data/data-schema"; import { dataConfigSchema } from "../../src/data/data-schema";
import * as proto from "../../src/data/prototype"; import * as proto from "../../src/data/prototype";
import { disableConsoleLog, enableConsoleLog, schemaToEm } from "../helper"; import { schemaToEm } from "../helper";
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
beforeAll(disableConsoleLog); beforeAll(disableConsoleLog);
afterAll(enableConsoleLog); afterAll(enableConsoleLog);
@@ -64,6 +65,15 @@ describe("DataApi", () => {
const res = await req; const res = await req;
expect(res.data).toEqual(payload as any); expect(res.data).toEqual(payload as any);
} }
{
// make sure sort is working
const req = await api.readMany("posts", {
select: ["title"],
sort: "-id",
});
expect(req.data).toEqual(payload.reverse() as any);
}
}); });
it("updates many", async () => { it("updates many", async () => {
+84 -1
View File
@@ -1,6 +1,6 @@
import { describe, expect, mock, test } from "bun:test"; import { describe, expect, mock, test } from "bun:test";
import type { ModuleBuildContext } from "../../src"; import type { ModuleBuildContext } from "../../src";
import { type App, createApp } from "../../src/App"; import { App, createApp } from "../../src/App";
import * as proto from "../../src/data/prototype"; import * as proto from "../../src/data/prototype";
describe("App", () => { describe("App", () => {
@@ -51,4 +51,87 @@ describe("App", () => {
expect(todos[0]?.title).toBe("ctx"); expect(todos[0]?.title).toBe("ctx");
expect(todos[1]?.title).toBe("api"); expect(todos[1]?.title).toBe("api");
}); });
test("lifecycle events are triggered", async () => {
const firstBoot = mock(() => null);
const configUpdate = mock(() => null);
const appBuilt = mock(() => null);
const appRequest = mock(() => null);
const beforeResponse = mock(() => null);
const app = createApp();
app.emgr.onEvent(
App.Events.AppFirstBoot,
(event) => {
expect(event).toBeInstanceOf(App.Events.AppFirstBoot);
expect(event.params.app.version()).toBe(app.version());
firstBoot();
},
"sync",
);
app.emgr.onEvent(
App.Events.AppBuiltEvent,
(event) => {
expect(event).toBeInstanceOf(App.Events.AppBuiltEvent);
expect(event.params.app.version()).toBe(app.version());
appBuilt();
},
"sync",
);
app.emgr.onEvent(
App.Events.AppConfigUpdatedEvent,
() => {
configUpdate();
},
"sync",
);
app.emgr.onEvent(
App.Events.AppRequest,
(event) => {
expect(event).toBeInstanceOf(App.Events.AppRequest);
expect(event.params.app.version()).toBe(app.version());
expect(event.params.request).toBeInstanceOf(Request);
appRequest();
},
"sync",
);
app.emgr.onEvent(
App.Events.AppBeforeResponse,
(event) => {
expect(event).toBeInstanceOf(App.Events.AppBeforeResponse);
expect(event.params.app.version()).toBe(app.version());
expect(event.params.response).toBeInstanceOf(Response);
beforeResponse();
},
"sync",
);
await app.build();
expect(firstBoot).toHaveBeenCalled();
expect(appBuilt).toHaveBeenCalled();
//expect(configUpdate).toHaveBeenCalled();
expect(appRequest).not.toHaveBeenCalled();
expect(beforeResponse).not.toHaveBeenCalled();
});
test("emgr exec modes", async () => {
const called = mock(() => null);
const app = createApp({
options: {
asyncEventsMode: "sync",
},
});
// register async listener
app.emgr.onEvent(App.Events.AppFirstBoot, async () => {
called();
});
await app.build();
await app.server.request(new Request("http://localhost"));
// expect async listeners to be executed sync after request
expect(called).toHaveBeenCalled();
});
}); });
+8 -6
View File
@@ -70,6 +70,9 @@ describe("EventManager", async () => {
new SpecialEvent({ foo: "bar" }); new SpecialEvent({ foo: "bar" });
new InformationalEvent(); new InformationalEvent();
// execute asyncs
await emgr.executeAsyncs();
expect(call).toHaveBeenCalledTimes(2); expect(call).toHaveBeenCalledTimes(2);
expect(delayed).toHaveBeenCalled(); expect(delayed).toHaveBeenCalled();
}); });
@@ -80,15 +83,11 @@ describe("EventManager", async () => {
call(); call();
return Promise.all(p); return Promise.all(p);
}; };
const emgr = new EventManager( const emgr = new EventManager({ InformationalEvent });
{ InformationalEvent },
{
asyncExecutor,
},
);
emgr.onEvent(InformationalEvent, async () => {}); emgr.onEvent(InformationalEvent, async () => {});
await emgr.emit(new InformationalEvent()); await emgr.emit(new InformationalEvent());
await emgr.executeAsyncs(asyncExecutor);
expect(call).toHaveBeenCalled(); expect(call).toHaveBeenCalled();
}); });
@@ -125,6 +124,9 @@ describe("EventManager", async () => {
const e2 = await emgr.emit(new ReturnEvent({ foo: "bar" })); const e2 = await emgr.emit(new ReturnEvent({ foo: "bar" }));
expect(e2.returned).toBe(true); expect(e2.returned).toBe(true);
expect(e2.params.foo).toBe("bar-1-0"); expect(e2.params.foo).toBe("bar-1-0");
await emgr.executeAsyncs();
expect(onInvalidReturn).toHaveBeenCalled(); expect(onInvalidReturn).toHaveBeenCalled();
expect(asyncEventCallback).toHaveBeenCalled(); expect(asyncEventCallback).toHaveBeenCalled();
}); });
+3
View File
@@ -288,14 +288,17 @@ describe("[data] Mutator (Events)", async () => {
test("events were fired", async () => { test("events were fired", async () => {
const { data } = await mutator.insertOne({ label: "test" }); const { data } = await mutator.insertOne({ label: "test" });
await mutator.emgr.executeAsyncs();
expect(events.has(MutatorEvents.MutatorInsertBefore.slug)).toBeTrue(); expect(events.has(MutatorEvents.MutatorInsertBefore.slug)).toBeTrue();
expect(events.has(MutatorEvents.MutatorInsertAfter.slug)).toBeTrue(); expect(events.has(MutatorEvents.MutatorInsertAfter.slug)).toBeTrue();
await mutator.updateOne(data.id, { label: "test2" }); await mutator.updateOne(data.id, { label: "test2" });
await mutator.emgr.executeAsyncs();
expect(events.has(MutatorEvents.MutatorUpdateBefore.slug)).toBeTrue(); expect(events.has(MutatorEvents.MutatorUpdateBefore.slug)).toBeTrue();
expect(events.has(MutatorEvents.MutatorUpdateAfter.slug)).toBeTrue(); expect(events.has(MutatorEvents.MutatorUpdateAfter.slug)).toBeTrue();
await mutator.deleteOne(data.id); await mutator.deleteOne(data.id);
await mutator.emgr.executeAsyncs();
expect(events.has(MutatorEvents.MutatorDeleteBefore.slug)).toBeTrue(); expect(events.has(MutatorEvents.MutatorDeleteBefore.slug)).toBeTrue();
expect(events.has(MutatorEvents.MutatorDeleteAfter.slug)).toBeTrue(); expect(events.has(MutatorEvents.MutatorDeleteAfter.slug)).toBeTrue();
}); });
+55 -6
View File
@@ -1,6 +1,6 @@
import { afterAll, describe, expect, test } from "bun:test"; import { afterAll, describe, expect, test } from "bun:test";
import type { Kysely, Transaction } from "kysely"; import type { Kysely, Transaction } from "kysely";
import { Perf } from "../../../src/core/utils"; import { Perf } from "core/utils";
import { import {
Entity, Entity,
EntityManager, EntityManager,
@@ -8,7 +8,10 @@ import {
ManyToOneRelation, ManyToOneRelation,
RepositoryEvents, RepositoryEvents,
TextField, TextField,
} from "../../../src/data"; entity as $entity,
text as $text,
em as $em,
} from "data";
import { getDummyConnection } from "../helper"; import { getDummyConnection } from "../helper";
type E = Kysely<any> | Transaction<any>; type E = Kysely<any> | Transaction<any>;
@@ -177,6 +180,47 @@ describe("[Repository]", async () => {
const res5 = await em.repository(items).exists({}); const res5 = await em.repository(items).exists({});
expect(res5.exists).toBe(true); expect(res5.exists).toBe(true);
}); });
test("option: silent", async () => {
const em = $em({
items: $entity("items", {
label: $text(),
}),
}).proto.withConnection(getDummyConnection().dummyConnection);
// should throw because table doesn't exist
expect(em.repo("items").findMany({})).rejects.toThrow(/no such table/);
// should silently return empty result
expect(
em
.repo("items", { silent: true })
.findMany({})
.then((r) => r.data),
).resolves.toEqual([]);
});
test("option: includeCounts", async () => {
const em = $em({
items: $entity("items", {
label: $text(),
}),
}).proto.withConnection(getDummyConnection().dummyConnection);
await em.schema().sync({ force: true });
expect(
em
.repo("items")
.findMany({})
.then((r) => [r.meta.count, r.meta.total]),
).resolves.toEqual([0, 0]);
expect(
em
.repo("items", { includeCounts: false })
.findMany({})
.then((r) => [r.meta.count, r.meta.total]),
).resolves.toEqual([undefined, undefined]);
});
}); });
describe("[data] Repository (Events)", async () => { describe("[data] Repository (Events)", async () => {
@@ -198,22 +242,27 @@ describe("[data] Repository (Events)", async () => {
}); });
test("events were fired", async () => { test("events were fired", async () => {
await em.repository(items).findId(1); const repo = em.repository(items);
await repo.findId(1);
await repo.emgr.executeAsyncs();
expect(events.has(RepositoryEvents.RepositoryFindOneBefore.slug)).toBeTrue(); expect(events.has(RepositoryEvents.RepositoryFindOneBefore.slug)).toBeTrue();
expect(events.has(RepositoryEvents.RepositoryFindOneAfter.slug)).toBeTrue(); expect(events.has(RepositoryEvents.RepositoryFindOneAfter.slug)).toBeTrue();
events.clear(); events.clear();
await em.repository(items).findOne({ id: 1 }); await repo.findOne({ id: 1 });
await repo.emgr.executeAsyncs();
expect(events.has(RepositoryEvents.RepositoryFindOneBefore.slug)).toBeTrue(); expect(events.has(RepositoryEvents.RepositoryFindOneBefore.slug)).toBeTrue();
expect(events.has(RepositoryEvents.RepositoryFindOneAfter.slug)).toBeTrue(); expect(events.has(RepositoryEvents.RepositoryFindOneAfter.slug)).toBeTrue();
events.clear(); events.clear();
await em.repository(items).findMany({ where: { id: 1 } }); await repo.findMany({ where: { id: 1 } });
await repo.emgr.executeAsyncs();
expect(events.has(RepositoryEvents.RepositoryFindManyBefore.slug)).toBeTrue(); expect(events.has(RepositoryEvents.RepositoryFindManyBefore.slug)).toBeTrue();
expect(events.has(RepositoryEvents.RepositoryFindManyAfter.slug)).toBeTrue(); expect(events.has(RepositoryEvents.RepositoryFindManyAfter.slug)).toBeTrue();
events.clear(); events.clear();
await em.repository(items).findManyByReference(1, "categories"); await repo.findManyByReference(1, "categories");
await repo.emgr.executeAsyncs();
expect(events.has(RepositoryEvents.RepositoryFindManyBefore.slug)).toBeTrue(); expect(events.has(RepositoryEvents.RepositoryFindManyBefore.slug)).toBeTrue();
expect(events.has(RepositoryEvents.RepositoryFindManyAfter.slug)).toBeTrue(); expect(events.has(RepositoryEvents.RepositoryFindManyAfter.slug)).toBeTrue();
events.clear(); events.clear();
+1
View File
@@ -78,6 +78,7 @@ export const assetsTmpPath = `${import.meta.dir}/_assets/tmp`;
export async function enableFetchLogging() { export async function enableFetchLogging() {
const originalFetch = global.fetch; const originalFetch = global.fetch;
// @ts-ignore
global.fetch = async (input: RequestInfo | URL, init?: RequestInit) => { global.fetch = async (input: RequestInfo | URL, init?: RequestInit) => {
const response = await originalFetch(input, init); const response = await originalFetch(input, init);
const url = input instanceof URL || typeof input === "string" ? input : input.url; const url = input instanceof URL || typeof input === "string" ? input : input.url;
+5 -3
View File
@@ -1,8 +1,9 @@
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from "bun:test";
import { type FileBody, Storage, type StorageAdapter } from "../../src/media/storage/Storage"; import { type FileBody, Storage } from "../../src/media/storage/Storage";
import * as StorageEvents from "../../src/media/storage/events"; import * as StorageEvents from "../../src/media/storage/events";
import { StorageAdapter } from "media";
class TestAdapter implements StorageAdapter { class TestAdapter extends StorageAdapter {
files: Record<string, FileBody> = {}; files: Record<string, FileBody> = {};
getName() { getName() {
@@ -61,7 +62,7 @@ describe("Storage", async () => {
test("uploads a file", async () => { test("uploads a file", async () => {
const { const {
meta: { type, size }, meta: { type, size },
} = await storage.uploadFile("hello", "world.txt"); } = await storage.uploadFile("hello" as any, "world.txt");
expect({ type, size }).toEqual({ type: "text/plain", size: 0 }); expect({ type, size }).toEqual({ type: "text/plain", size: 0 });
}); });
@@ -71,6 +72,7 @@ describe("Storage", async () => {
}); });
test("events were fired", async () => { test("events were fired", async () => {
await storage.emgr.executeAsyncs();
expect(events.has(StorageEvents.FileUploadedEvent.slug)).toBeTrue(); expect(events.has(StorageEvents.FileUploadedEvent.slug)).toBeTrue();
expect(events.has(StorageEvents.FileDeletedEvent.slug)).toBeTrue(); expect(events.has(StorageEvents.FileDeletedEvent.slug)).toBeTrue();
// @todo: file access must be tested in controllers // @todo: file access must be tested in controllers
+5
View File
@@ -96,5 +96,10 @@ describe("media/mime-types", () => {
`getRandomizedFilename(): ${filename} should end with ${ext}`, `getRandomizedFilename(): ${filename} should end with ${ext}`,
).toBe(ext); ).toBe(ext);
} }
// make sure it keeps the extension, even if the file has a different type
const file = new File([""], "image.jpg", { type: "text/plain" });
const [, ext] = getRandomizedFilename(file).split(".");
expect(ext).toBe("jpg");
}); });
}); });
+22 -21
View File
@@ -3,7 +3,7 @@
"type": "module", "type": "module",
"sideEffects": false, "sideEffects": false,
"bin": "./dist/cli/index.js", "bin": "./dist/cli/index.js",
"version": "0.10.3-rc.1", "version": "0.11.0-rc.2",
"description": "Lightweight Firebase/Supabase alternative built to run anywhere — incl. Next.js, React Router, Astro, Cloudflare, Bun, Node, AWS Lambda & more.", "description": "Lightweight Firebase/Supabase alternative built to run anywhere — incl. Next.js, React Router, Astro, Cloudflare, Bun, Node, AWS Lambda & more.",
"homepage": "https://bknd.io", "homepage": "https://bknd.io",
"repository": { "repository": {
@@ -15,10 +15,11 @@
}, },
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"test": "bun run test:unit && bun run test:e2e", "test": "ALL_TESTS=1 bun test --bail",
"test:all": "bun run test && bun run test:node", "test:all": "bun run test && bun run test:node",
"test:bun": "ALL_TESTS=1 bun test --bail", "test:bun": "ALL_TESTS=1 bun test --bail",
"test:node": "tsx --test $(find . -type f -name '*.native-spec.ts')", "test:node": "tsx --test $(find . -type f -name '*.native-spec.ts')",
"test:adapters": "bun test src/adapter/**/*.adapter.spec.ts --bail",
"test:coverage": "ALL_TESTS=1 bun test --bail --coverage", "test:coverage": "ALL_TESTS=1 bun test --bail --coverage",
"build": "NODE_ENV=production bun run build.ts --minify --types", "build": "NODE_ENV=production bun run build.ts --minify --types",
"build:all": "rm -rf dist && bun run build:static && NODE_ENV=production bun run build.ts --minify --types --clean && bun run build:cli", "build:all": "rm -rf dist && bun run build:static && NODE_ENV=production bun run build.ts --minify --types --clean && bun run build:cli",
@@ -31,7 +32,7 @@
"build:types": "tsc -p tsconfig.build.json --emitDeclarationOnly && tsc-alias", "build:types": "tsc -p tsconfig.build.json --emitDeclarationOnly && tsc-alias",
"updater": "bun x npm-check-updates -ui", "updater": "bun x npm-check-updates -ui",
"cli": "LOCAL=1 bun src/cli/index.ts", "cli": "LOCAL=1 bun src/cli/index.ts",
"prepublishOnly": "bun run types && bun run test && bun run build:all && cp ../README.md ./", "prepublishOnly": "bun run types && bun run test && bun run test:node && bun run build:all && cp ../README.md ./",
"postpublish": "rm -f README.md", "postpublish": "rm -f README.md",
"test:unit": "vitest", "test:unit": "vitest",
"test:unit:coverage": "vitest run --coverage", "test:unit:coverage": "vitest run --coverage",
@@ -47,7 +48,7 @@
"@codemirror/lang-json": "^6.0.1", "@codemirror/lang-json": "^6.0.1",
"@codemirror/lang-liquid": "^6.2.2", "@codemirror/lang-liquid": "^6.2.2",
"@hello-pangea/dnd": "^18.0.1", "@hello-pangea/dnd": "^18.0.1",
"@libsql/client": "^0.14.0", "@libsql/client": "^0.15.2",
"@mantine/core": "^7.17.1", "@mantine/core": "^7.17.1",
"@mantine/hooks": "^7.17.1", "@mantine/hooks": "^7.17.1",
"@sinclair/typebox": "^0.34.30", "@sinclair/typebox": "^0.34.30",
@@ -136,52 +137,52 @@
".": { ".": {
"types": "./dist/types/index.d.ts", "types": "./dist/types/index.d.ts",
"import": "./dist/index.js", "import": "./dist/index.js",
"require": "./dist/index.cjs" "require": "./dist/index.js"
}, },
"./ui": { "./ui": {
"types": "./dist/types/ui/index.d.ts", "types": "./dist/types/ui/index.d.ts",
"import": "./dist/ui/index.js", "import": "./dist/ui/index.js",
"require": "./dist/ui/index.cjs" "require": "./dist/ui/index.js"
}, },
"./elements": { "./elements": {
"types": "./dist/types/ui/elements/index.d.ts", "types": "./dist/types/ui/elements/index.d.ts",
"import": "./dist/ui/elements/index.js", "import": "./dist/ui/elements/index.js",
"require": "./dist/ui/elements/index.cjs" "require": "./dist/ui/elements/index.js"
}, },
"./client": { "./client": {
"types": "./dist/types/ui/client/index.d.ts", "types": "./dist/types/ui/client/index.d.ts",
"import": "./dist/ui/client/index.js", "import": "./dist/ui/client/index.js",
"require": "./dist/ui/client/index.cjs" "require": "./dist/ui/client/index.js"
}, },
"./data": { "./data": {
"types": "./dist/types/data/index.d.ts", "types": "./dist/types/data/index.d.ts",
"import": "./dist/data/index.js", "import": "./dist/data/index.js",
"require": "./dist/data/index.cjs" "require": "./dist/data/index.js"
}, },
"./core": { "./core": {
"types": "./dist/types/core/index.d.ts", "types": "./dist/types/core/index.d.ts",
"import": "./dist/core/index.js", "import": "./dist/core/index.js",
"require": "./dist/core/index.cjs" "require": "./dist/core/index.js"
}, },
"./utils": { "./utils": {
"types": "./dist/types/core/utils/index.d.ts", "types": "./dist/types/core/utils/index.d.ts",
"import": "./dist/core/utils/index.js", "import": "./dist/core/utils/index.js",
"require": "./dist/core/utils/index.cjs" "require": "./dist/core/utils/index.js"
}, },
"./cli": { "./cli": {
"types": "./dist/types/cli/index.d.ts", "types": "./dist/types/cli/index.d.ts",
"import": "./dist/cli/index.js", "import": "./dist/cli/index.js",
"require": "./dist/cli/index.cjs" "require": "./dist/cli/index.js"
}, },
"./media": { "./media": {
"types": "./dist/types/media/index.d.ts", "types": "./dist/types/media/index.d.ts",
"import": "./dist/media/index.js", "import": "./dist/media/index.js",
"require": "./dist/media/index.cjs" "require": "./dist/media/index.js"
}, },
"./adapter/cloudflare": { "./adapter/cloudflare": {
"types": "./dist/types/adapter/cloudflare/index.d.ts", "types": "./dist/types/adapter/cloudflare/index.d.ts",
"import": "./dist/adapter/cloudflare/index.js", "import": "./dist/adapter/cloudflare/index.js",
"require": "./dist/adapter/cloudflare/index.cjs" "require": "./dist/adapter/cloudflare/index.js"
}, },
"./adapter": { "./adapter": {
"types": "./dist/types/adapter/index.d.ts", "types": "./dist/types/adapter/index.d.ts",
@@ -190,37 +191,37 @@
"./adapter/vite": { "./adapter/vite": {
"types": "./dist/types/adapter/vite/index.d.ts", "types": "./dist/types/adapter/vite/index.d.ts",
"import": "./dist/adapter/vite/index.js", "import": "./dist/adapter/vite/index.js",
"require": "./dist/adapter/vite/index.cjs" "require": "./dist/adapter/vite/index.js"
}, },
"./adapter/nextjs": { "./adapter/nextjs": {
"types": "./dist/types/adapter/nextjs/index.d.ts", "types": "./dist/types/adapter/nextjs/index.d.ts",
"import": "./dist/adapter/nextjs/index.js", "import": "./dist/adapter/nextjs/index.js",
"require": "./dist/adapter/nextjs/index.cjs" "require": "./dist/adapter/nextjs/index.js"
}, },
"./adapter/react-router": { "./adapter/react-router": {
"types": "./dist/types/adapter/react-router/index.d.ts", "types": "./dist/types/adapter/react-router/index.d.ts",
"import": "./dist/adapter/react-router/index.js", "import": "./dist/adapter/react-router/index.js",
"require": "./dist/adapter/react-router/index.cjs" "require": "./dist/adapter/react-router/index.js"
}, },
"./adapter/bun": { "./adapter/bun": {
"types": "./dist/types/adapter/bun/index.d.ts", "types": "./dist/types/adapter/bun/index.d.ts",
"import": "./dist/adapter/bun/index.js", "import": "./dist/adapter/bun/index.js",
"require": "./dist/adapter/bun/index.cjs" "require": "./dist/adapter/bun/index.js"
}, },
"./adapter/node": { "./adapter/node": {
"types": "./dist/types/adapter/node/index.d.ts", "types": "./dist/types/adapter/node/index.d.ts",
"import": "./dist/adapter/node/index.js", "import": "./dist/adapter/node/index.js",
"require": "./dist/adapter/node/index.cjs" "require": "./dist/adapter/node/index.js"
}, },
"./adapter/astro": { "./adapter/astro": {
"types": "./dist/types/adapter/astro/index.d.ts", "types": "./dist/types/adapter/astro/index.d.ts",
"import": "./dist/adapter/astro/index.js", "import": "./dist/adapter/astro/index.js",
"require": "./dist/adapter/astro/index.cjs" "require": "./dist/adapter/astro/index.js"
}, },
"./adapter/aws": { "./adapter/aws": {
"types": "./dist/types/adapter/aws/index.d.ts", "types": "./dist/types/adapter/aws/index.d.ts",
"import": "./dist/adapter/aws/index.js", "import": "./dist/adapter/aws/index.js",
"require": "./dist/adapter/aws/index.cjs" "require": "./dist/adapter/aws/index.js"
}, },
"./dist/main.css": "./dist/ui/main.css", "./dist/main.css": "./dist/ui/main.css",
"./dist/styles.css": "./dist/ui/styles.css", "./dist/styles.css": "./dist/ui/styles.css",
+71 -33
View File
@@ -4,9 +4,10 @@ import { Event } from "core/events";
import { Connection, type LibSqlCredentials, LibsqlConnection } from "data"; import { Connection, type LibSqlCredentials, LibsqlConnection } from "data";
import type { Hono } from "hono"; import type { Hono } from "hono";
import { import {
ModuleManager,
type InitialModuleConfigs, type InitialModuleConfigs,
type ModuleBuildContext, type ModuleBuildContext,
ModuleManager, type ModuleConfigs,
type ModuleManagerOptions, type ModuleManagerOptions,
type Modules, type Modules,
} from "modules/ModuleManager"; } from "modules/ModuleManager";
@@ -16,6 +17,7 @@ import { SystemController } from "modules/server/SystemController";
// biome-ignore format: must be there // biome-ignore format: must be there
import { Api, type ApiOptions } from "Api"; import { Api, type ApiOptions } from "Api";
import type { ServerEnv } from "modules/Controller";
export type AppPlugin = (app: App) => Promise<void> | void; export type AppPlugin = (app: App) => Promise<void> | void;
@@ -29,12 +31,25 @@ export class AppBuiltEvent extends AppEvent {
export class AppFirstBoot extends AppEvent { export class AppFirstBoot extends AppEvent {
static override slug = "app-first-boot"; static override slug = "app-first-boot";
} }
export const AppEvents = { AppConfigUpdatedEvent, AppBuiltEvent, AppFirstBoot } as const; export class AppRequest extends AppEvent<{ request: Request }> {
static override slug = "app-request";
}
export class AppBeforeResponse extends AppEvent<{ request: Request; response: Response }> {
static override slug = "app-before-response";
}
export const AppEvents = {
AppConfigUpdatedEvent,
AppBuiltEvent,
AppFirstBoot,
AppRequest,
AppBeforeResponse,
} as const;
export type AppOptions = { export type AppOptions = {
plugins?: AppPlugin[]; plugins?: AppPlugin[];
seed?: (ctx: ModuleBuildContext & { app: App }) => Promise<void>; seed?: (ctx: ModuleBuildContext & { app: App }) => Promise<void>;
manager?: Omit<ModuleManagerOptions, "initial" | "onUpdated" | "seed">; manager?: Omit<ModuleManagerOptions, "initial" | "onUpdated" | "seed">;
asyncEventsMode?: "sync" | "async" | "none";
}; };
export type CreateAppConfig = { export type CreateAppConfig = {
connection?: connection?:
@@ -53,12 +68,14 @@ export type AppConfig = InitialModuleConfigs;
export type LocalApiOptions = Request | ApiOptions; export type LocalApiOptions = Request | ApiOptions;
export class App { export class App {
modules: ModuleManager;
static readonly Events = AppEvents; static readonly Events = AppEvents;
modules: ModuleManager;
adminController?: AdminController; adminController?: AdminController;
_id: string = crypto.randomUUID();
private trigger_first_boot = false; private trigger_first_boot = false;
private plugins: AppPlugin[]; private plugins: AppPlugin[];
private _id: string = crypto.randomUUID();
private _building: boolean = false; private _building: boolean = false;
constructor( constructor(
@@ -70,35 +87,9 @@ export class App {
this.modules = new ModuleManager(connection, { this.modules = new ModuleManager(connection, {
...(options?.manager ?? {}), ...(options?.manager ?? {}),
initial: _initialConfig, initial: _initialConfig,
onUpdated: async (key, config) => { onUpdated: this.onUpdated.bind(this),
// if the EventManager was disabled, we assume we shouldn't onFirstBoot: this.onFirstBoot.bind(this),
// respond to events, such as "onUpdated". onServerInit: this.onServerInit.bind(this),
// this is important if multiple changes are done, and then build() is called manually
if (!this.emgr.enabled) {
$console.warn("App config updated, but event manager is disabled, skip.");
return;
}
$console.log("App config updated", key);
// @todo: potentially double syncing
await this.build({ sync: true });
await this.emgr.emit(new AppConfigUpdatedEvent({ app: this }));
},
onFirstBoot: async () => {
$console.log("App first boot");
this.trigger_first_boot = true;
},
onServerInit: async (server) => {
server.use(async (c, next) => {
c.set("app", this);
await next();
try {
// gracefully add the app id
c.res.headers.set("X-bknd-id", this._id);
} catch (e) {}
});
},
}); });
this.modules.ctx().emgr.registerEvents(AppEvents); this.modules.ctx().emgr.registerEvents(AppEvents);
} }
@@ -213,6 +204,53 @@ export class App {
return new Api({ host: "http://localhost", ...(options ?? {}), fetcher }); return new Api({ host: "http://localhost", ...(options ?? {}), fetcher });
} }
async onUpdated<Module extends keyof Modules>(module: Module, config: ModuleConfigs[Module]) {
// if the EventManager was disabled, we assume we shouldn't
// respond to events, such as "onUpdated".
// this is important if multiple changes are done, and then build() is called manually
if (!this.emgr.enabled) {
$console.warn("App config updated, but event manager is disabled, skip.");
return;
}
$console.log("App config updated", module);
// @todo: potentially double syncing
await this.build({ sync: true });
await this.emgr.emit(new AppConfigUpdatedEvent({ app: this }));
}
async onFirstBoot() {
$console.log("App first boot");
this.trigger_first_boot = true;
}
async onServerInit(server: Hono<ServerEnv>) {
server.use(async (c, next) => {
c.set("app", this);
await this.emgr.emit(new AppRequest({ app: this, request: c.req.raw }));
await next();
try {
// gracefully add the app id
c.res.headers.set("X-bknd-id", this._id);
} catch (e) {}
await this.emgr.emit(
new AppBeforeResponse({ app: this, request: c.req.raw, response: c.res }),
);
// execute collected async events (async by default)
switch (this.options?.asyncEventsMode ?? "async") {
case "sync":
await this.emgr.executeAsyncs();
break;
case "async":
this.emgr.executeAsyncs();
break;
}
});
}
} }
export function createApp(config: CreateAppConfig = {}) { export function createApp(config: CreateAppConfig = {}) {
+90
View File
@@ -0,0 +1,90 @@
import type { TestRunner } from "core/test";
import type { BkndConfig, DefaultArgs, FrameworkOptions, RuntimeOptions } from "./index";
import type { App } from "App";
export function adapterTestSuite<
Config extends BkndConfig = BkndConfig,
Args extends DefaultArgs = DefaultArgs,
>(
testRunner: TestRunner,
{
makeApp,
makeHandler,
label = "app",
overrides = {},
}: {
makeApp: (
config: Config,
args?: Args,
opts?: RuntimeOptions | FrameworkOptions,
) => Promise<App>;
makeHandler?: (
config?: Config,
args?: Args,
opts?: RuntimeOptions | FrameworkOptions,
) => (request: Request) => Promise<Response>;
label?: string;
overrides?: {
dbUrl?: string;
};
},
) {
const { test, expect, mock } = testRunner;
const id = crypto.randomUUID();
test(`creates ${label}`, async () => {
const beforeBuild = mock(async () => null) as any;
const onBuilt = mock(async () => null) as any;
const config = {
app: (env) => ({
connection: { url: env.url },
initialConfig: {
server: { cors: { origin: env.origin } },
},
}),
beforeBuild,
onBuilt,
} as const satisfies BkndConfig;
const app = await makeApp(
config as any,
{
url: overrides.dbUrl ?? ":memory:",
origin: "localhost",
} as any,
{ id },
);
expect(app).toBeDefined();
expect(app.toJSON().server.cors.origin).toEqual("localhost");
expect(beforeBuild).toHaveBeenCalledTimes(1);
expect(onBuilt).toHaveBeenCalledTimes(1);
});
if (makeHandler) {
const getConfig = async (fetcher: (r: Request) => Promise<Response>) => {
const res = await fetcher(new Request("http://localhost:3000/api/system/config"));
const data = (await res.json()) as any;
return { res, data };
};
test("responds with the same app id", async () => {
const fetcher = makeHandler(undefined, undefined, { id });
const { res, data } = await getConfig(fetcher);
expect(res.ok).toBe(true);
expect(res.status).toBe(200);
expect(data.server.cors.origin).toEqual("localhost");
});
test("creates fresh & responds to api config", async () => {
// set the same id, but force recreate
const fetcher = makeHandler(undefined, undefined, { id, force: true });
const { res, data } = await getConfig(fetcher);
expect(res.ok).toBe(true);
expect(res.status).toBe(200);
expect(data.server.cors.origin).toEqual("*");
});
}
}
@@ -0,0 +1,15 @@
import { afterAll, beforeAll, describe } from "bun:test";
import * as astro from "./astro.adapter";
import { disableConsoleLog, enableConsoleLog } from "core/utils";
import { adapterTestSuite } from "adapter/adapter-test-suite";
import { bunTestRunner } from "adapter/bun/test";
beforeAll(disableConsoleLog);
afterAll(enableConsoleLog);
describe("astro adapter", () => {
adapterTestSuite(bunTestRunner, {
makeApp: astro.getApp,
makeHandler: (c, a, o) => (request: Request) => astro.serve(c, a, o)({ request }),
});
});
+16 -25
View File
@@ -1,34 +1,25 @@
import type { App } from "bknd"; import { type FrameworkBkndConfig, createFrameworkApp, type FrameworkOptions } from "bknd/adapter";
import { type FrameworkBkndConfig, createFrameworkApp } from "bknd/adapter";
import { Api, type ApiOptions } from "bknd/client";
export type AstroBkndConfig<Args = TAstro> = FrameworkBkndConfig<Args>;
type AstroEnv = NodeJS.ProcessEnv;
type TAstro = { type TAstro = {
request: Request; request: Request;
}; };
export type AstroBkndConfig<Env = AstroEnv> = FrameworkBkndConfig<Env>;
export type Options = { export async function getApp<Env = AstroEnv>(
mode?: "static" | "dynamic"; config: AstroBkndConfig<Env> = {},
} & Omit<ApiOptions, "host"> & { args: Env = {} as Env,
host?: string; opts: FrameworkOptions = {},
}; ) {
return await createFrameworkApp(config, args ?? import.meta.env, opts);
export async function getApi(Astro: TAstro, options: Options = { mode: "static" }) {
const api = new Api({
host: new URL(Astro.request.url).origin,
headers: options.mode === "dynamic" ? Astro.request.headers : undefined,
});
await api.verifyAuth();
return api;
} }
let app: App; export function serve<Env = AstroEnv>(
export function serve<Context extends TAstro = TAstro>(config: AstroBkndConfig<Context> = {}) { config: AstroBkndConfig<Env> = {},
return async (args: Context) => { args: Env = {} as Env,
if (!app) { opts?: FrameworkOptions,
app = await createFrameworkApp(config, args); ) {
} return async (fnArgs: TAstro) => {
return app.fetch(args.request); return (await getApp(config, args, opts)).fetch(fnArgs.request);
}; };
} }
+62 -54
View File
@@ -1,68 +1,76 @@
import type { App } from "bknd"; import type { App } from "bknd";
import { handle } from "hono/aws-lambda"; import { handle } from "hono/aws-lambda";
import { type RuntimeBkndConfig, createRuntimeApp } from "bknd/adapter"; import { serveStatic } from "@hono/node-server/serve-static";
import { type RuntimeBkndConfig, createRuntimeApp, type RuntimeOptions } from "bknd/adapter";
export type AwsLambdaBkndConfig = RuntimeBkndConfig & { type AwsLambdaEnv = object;
assets?: export type AwsLambdaBkndConfig<Env extends AwsLambdaEnv = AwsLambdaEnv> =
| { RuntimeBkndConfig<Env> & {
mode: "local"; assets?:
root: string; | {
} mode: "local";
| { root: string;
mode: "url"; }
url: string; | {
}; mode: "url";
}; url: string;
};
};
let app: App; export async function createApp<Env extends AwsLambdaEnv = AwsLambdaEnv>(
export async function createApp({ { adminOptions = false, assets, ...config }: AwsLambdaBkndConfig<Env> = {},
adminOptions = false, args: Env = {} as Env,
assets, opts?: RuntimeOptions,
...config ): Promise<App> {
}: AwsLambdaBkndConfig = {}) { let additional: Partial<RuntimeBkndConfig> = {
if (!app) { adminOptions,
let additional: Partial<RuntimeBkndConfig> = { };
adminOptions,
};
if (assets?.mode) { if (assets?.mode) {
switch (assets.mode) { switch (assets.mode) {
case "local": case "local":
// @todo: serve static outside app context // @todo: serve static outside app context
additional = { additional = {
adminOptions: adminOptions === false ? undefined : adminOptions, adminOptions: adminOptions === false ? undefined : adminOptions,
serveStatic: (await import("@hono/node-server/serve-static")).serveStatic({ serveStatic: serveStatic({
root: assets.root, root: assets.root,
onFound: (path, c) => { onFound: (path, c) => {
c.res.headers.set("Cache-Control", "public, max-age=31536000"); c.res.headers.set("Cache-Control", "public, max-age=31536000");
}, },
}), }),
}; };
break; break;
case "url": case "url":
additional.adminOptions = { additional.adminOptions = {
...(typeof adminOptions === "object" ? adminOptions : {}), ...(typeof adminOptions === "object" ? adminOptions : {}),
assets_path: assets.url, assets_path: assets.url,
}; };
break; break;
default: default:
throw new Error("Invalid assets mode"); throw new Error("Invalid assets mode");
}
} }
app = await createRuntimeApp({
...config,
...additional,
});
} }
return app; return await createRuntimeApp(
{
...config,
...additional,
},
args ?? process.env,
opts,
);
} }
export function serveLambda(config: AwsLambdaBkndConfig = {}) { export function serve<Env extends AwsLambdaEnv = AwsLambdaEnv>(
console.log("serving lambda"); config: AwsLambdaBkndConfig<Env> = {},
args: Env = {} as Env,
opts?: RuntimeOptions,
) {
return async (event) => { return async (event) => {
const app = await createApp(config); const app = await createApp(config, args, opts);
return await handle(app.server)(event); return await handle(app.server)(event);
}; };
} }
// compatibility with old code
export const serveLambda = serve;
+19
View File
@@ -0,0 +1,19 @@
import { afterAll, beforeAll, describe } from "bun:test";
import * as awsLambda from "./aws-lambda.adapter";
import { disableConsoleLog, enableConsoleLog } from "core/utils";
import { adapterTestSuite } from "adapter/adapter-test-suite";
import { bunTestRunner } from "adapter/bun/test";
beforeAll(disableConsoleLog);
afterAll(enableConsoleLog);
describe("aws adapter", () => {
adapterTestSuite(bunTestRunner, {
makeApp: awsLambda.createApp,
// @todo: add a request to lambda event translator?
makeHandler: (c, a, o) => async (request: Request) => {
const app = await awsLambda.createApp(c, a, o);
return app.fetch(request);
},
});
});
+15
View File
@@ -0,0 +1,15 @@
import { afterAll, beforeAll, describe } from "bun:test";
import * as bun from "./bun.adapter";
import { disableConsoleLog, enableConsoleLog } from "core/utils";
import { adapterTestSuite } from "adapter/adapter-test-suite";
import { bunTestRunner } from "adapter/bun/test";
beforeAll(disableConsoleLog);
afterAll(enableConsoleLog);
describe("bun adapter", () => {
adapterTestSuite(bunTestRunner, {
makeApp: bun.createApp,
makeHandler: bun.createHandler,
});
});
+47 -29
View File
@@ -1,47 +1,64 @@
/// <reference types="bun-types" /> /// <reference types="bun-types" />
import path from "node:path"; import path from "node:path";
import type { App } from "bknd"; import { type RuntimeBkndConfig, createRuntimeApp, type RuntimeOptions } from "bknd/adapter";
import { type RuntimeBkndConfig, createRuntimeApp } from "bknd/adapter";
import { registerLocalMediaAdapter } from "bknd/adapter/node"; import { registerLocalMediaAdapter } from "bknd/adapter/node";
import { config } from "bknd/core"; import { config } from "bknd/core";
import type { ServeOptions } from "bun"; import type { ServeOptions } from "bun";
import { serveStatic } from "hono/bun"; import { serveStatic } from "hono/bun";
let app: App; type BunEnv = Bun.Env;
export type BunBkndConfig<Env = BunEnv> = RuntimeBkndConfig<Env> & Omit<ServeOptions, "fetch">;
export type BunBkndConfig = RuntimeBkndConfig & Omit<ServeOptions, "fetch">; export async function createApp<Env = BunEnv>(
{ distPath, ...config }: BunBkndConfig<Env> = {},
export async function createApp({ distPath, ...config }: RuntimeBkndConfig = {}) { args: Env = {} as Env,
opts?: RuntimeOptions,
) {
const root = path.resolve(distPath ?? "./node_modules/bknd/dist", "static"); const root = path.resolve(distPath ?? "./node_modules/bknd/dist", "static");
registerLocalMediaAdapter();
if (!app) { return await createRuntimeApp(
registerLocalMediaAdapter(); {
app = await createRuntimeApp({
...config, ...config,
serveStatic: serveStatic({ root }), serveStatic: serveStatic({ root }),
}); },
} args ?? (process.env as Env),
opts,
return app; );
} }
export function serve({ export function createHandler<Env = BunEnv>(
distPath, config: BunBkndConfig<Env> = {},
connection, args: Env = {} as Env,
initialConfig, opts?: RuntimeOptions,
options, ) {
port = config.server.default_port, return async (req: Request) => {
onBuilt, const app = await createApp(config, args ?? (process.env as Env), opts);
buildConfig, return app.fetch(req);
adminOptions, };
...serveOptions }
}: BunBkndConfig = {}) {
export function serve<Env = BunEnv>(
{
distPath,
connection,
initialConfig,
options,
port = config.server.default_port,
onBuilt,
buildConfig,
adminOptions,
...serveOptions
}: BunBkndConfig<Env> = {},
args: Env = {} as Env,
opts?: RuntimeOptions,
) {
Bun.serve({ Bun.serve({
...serveOptions, ...serveOptions,
port, port,
fetch: async (request: Request) => { fetch: createHandler(
const app = await createApp({ {
connection, connection,
initialConfig, initialConfig,
options, options,
@@ -49,9 +66,10 @@ export function serve({
buildConfig, buildConfig,
adminOptions, adminOptions,
distPath, distPath,
}); },
return app.fetch(request); args,
}, opts,
),
}); });
console.log(`Server is running on http://localhost:${port}`); console.log(`Server is running on http://localhost:${port}`);
+7
View File
@@ -0,0 +1,7 @@
import { expect, test, mock } from "bun:test";
export const bunTestRunner = {
expect,
test,
mock,
};
@@ -0,0 +1,60 @@
import { afterAll, beforeAll, describe, expect, it } from "bun:test";
import { makeApp } from "./modes/fresh";
import { makeConfig } from "./config";
import { disableConsoleLog, enableConsoleLog } from "core/utils";
import { adapterTestSuite } from "adapter/adapter-test-suite";
import { bunTestRunner } from "adapter/bun/test";
import type { CloudflareBkndConfig } from "./cloudflare-workers.adapter";
beforeAll(disableConsoleLog);
afterAll(enableConsoleLog);
describe("cf adapter", () => {
const DB_URL = ":memory:";
const $ctx = (env?: any, request?: Request, ctx?: ExecutionContext) => ({
request: request ?? (null as any),
env: env ?? { DB_URL },
ctx: ctx ?? (null as any),
});
it("makes config", async () => {
expect(
makeConfig(
{
connection: { url: DB_URL },
},
{},
),
).toEqual({ connection: { url: DB_URL } });
expect(
makeConfig(
{
app: (env) => ({
connection: { url: env.DB_URL },
}),
},
{
DB_URL,
},
),
).toEqual({ connection: { url: DB_URL } });
});
adapterTestSuite<CloudflareBkndConfig, object>(bunTestRunner, {
makeApp,
makeHandler: (c, a, o) => {
return async (request: any) => {
const app = await makeApp(
// needs a fallback, otherwise tries to launch D1
c ?? {
connection: { url: DB_URL },
},
a,
o,
);
return app.fetch(request);
};
},
});
});
@@ -1,18 +1,16 @@
/// <reference types="@cloudflare/workers-types" /> /// <reference types="@cloudflare/workers-types" />
import { type FrameworkBkndConfig, makeConfig } from "bknd/adapter"; import type { FrameworkBkndConfig } from "bknd/adapter";
import { Hono } from "hono"; import { Hono } from "hono";
import { serveStatic } from "hono/cloudflare-workers"; import { serveStatic } from "hono/cloudflare-workers";
import { D1Connection } from "./D1Connection";
import { registerMedia } from "./StorageR2Adapter";
import { getBinding } from "./bindings";
import { getCached } from "./modes/cached"; import { getCached } from "./modes/cached";
import { getDurable } from "./modes/durable"; import { getDurable } from "./modes/durable";
import { getFresh, getWarm } from "./modes/fresh"; import { getFresh, getWarm } from "./modes/fresh";
export type CloudflareBkndConfig<Env = any> = FrameworkBkndConfig<Context<Env>> & { export type CloudflareEnv = object;
export type CloudflareBkndConfig<Env = CloudflareEnv> = FrameworkBkndConfig<Env> & {
mode?: "warm" | "fresh" | "cache" | "durable"; mode?: "warm" | "fresh" | "cache" | "durable";
bindings?: (args: Context<Env>) => { bindings?: (args: Env) => {
kv?: KVNamespace; kv?: KVNamespace;
dobj?: DurableObjectNamespace; dobj?: DurableObjectNamespace;
db?: D1Database; db?: D1Database;
@@ -26,45 +24,15 @@ export type CloudflareBkndConfig<Env = any> = FrameworkBkndConfig<Context<Env>>
html?: string; html?: string;
}; };
export type Context<Env = any> = { export type Context<Env = CloudflareEnv> = {
request: Request; request: Request;
env: Env; env: Env;
ctx: ExecutionContext; ctx: ExecutionContext;
}; };
let media_registered: boolean = false; export function serve<Env extends CloudflareEnv = CloudflareEnv>(
export function makeCfConfig(config: CloudflareBkndConfig, context: Context) { config: CloudflareBkndConfig<Env> = {},
if (!media_registered) { ) {
registerMedia(context.env as any);
media_registered = true;
}
const appConfig = makeConfig(config, context);
const bindings = config.bindings?.(context);
if (!appConfig.connection) {
let db: D1Database | undefined;
if (bindings?.db) {
console.log("Using database from bindings");
db = bindings.db;
} else if (Object.keys(context.env ?? {}).length > 0) {
const binding = getBinding(context.env, "D1Database");
if (binding) {
console.log(`Using database from env "${binding.key}"`);
db = binding.value;
}
}
if (db) {
appConfig.connection = new D1Connection({ binding: db });
} else {
throw new Error("No database connection given");
}
}
return appConfig;
}
export function serve<Env = any>(config: CloudflareBkndConfig<Env> = {}) {
return { return {
async fetch(request: Request, env: Env, ctx: ExecutionContext) { async fetch(request: Request, env: Env, ctx: ExecutionContext) {
const url = new URL(request.url); const url = new URL(request.url);
@@ -99,7 +67,7 @@ export function serve<Env = any>(config: CloudflareBkndConfig<Env> = {}) {
} }
} }
const context = { request, env, ctx } as Context; const context = { request, env, ctx } as Context<Env>;
const mode = config.mode ?? "warm"; const mode = config.mode ?? "warm";
switch (mode) { switch (mode) {
+64
View File
@@ -0,0 +1,64 @@
import { registerMedia } from "./storage/StorageR2Adapter";
import { getBinding } from "./bindings";
import { D1Connection } from "./D1Connection";
import type { CloudflareBkndConfig, CloudflareEnv } from ".";
import { App } from "bknd";
import { makeConfig as makeAdapterConfig } from "bknd/adapter";
import type { ExecutionContext } from "hono";
export const constants = {
exec_async_event_id: "cf_register_waituntil",
cache_endpoint: "/__bknd/cache",
do_endpoint: "/__bknd/do",
};
let media_registered: boolean = false;
export function makeConfig<Env extends CloudflareEnv = CloudflareEnv>(
config: CloudflareBkndConfig<Env>,
args: Env = {} as Env,
) {
if (!media_registered) {
registerMedia(args as any);
media_registered = true;
}
const appConfig = makeAdapterConfig(config, args);
const bindings = config.bindings?.(args);
if (!appConfig.connection) {
let db: D1Database | undefined;
if (bindings?.db) {
console.log("Using database from bindings");
db = bindings.db;
} else if (Object.keys(args).length > 0) {
const binding = getBinding(args, "D1Database");
if (binding) {
console.log(`Using database from env "${binding.key}"`);
db = binding.value;
}
}
if (db) {
appConfig.connection = new D1Connection({ binding: db });
} else {
throw new Error("No database connection given");
}
}
return appConfig;
}
export function registerAsyncsExecutionContext(
app: App,
ctx: { waitUntil: ExecutionContext["waitUntil"] },
) {
app.emgr.onEvent(
App.Events.AppBeforeResponse,
async (event) => {
ctx.waitUntil(event.params.app.emgr.executeAsyncs());
},
{
mode: "sync",
id: constants.exec_async_event_id,
},
);
}
+9 -4
View File
@@ -1,8 +1,12 @@
import { App } from "bknd"; import { App } from "bknd";
import { createRuntimeApp } from "bknd/adapter"; import { createRuntimeApp } from "bknd/adapter";
import { type CloudflareBkndConfig, type Context, makeCfConfig } from "../index"; import type { CloudflareBkndConfig, Context, CloudflareEnv } from "../index";
import { makeConfig, registerAsyncsExecutionContext, constants } from "../config";
export async function getCached(config: CloudflareBkndConfig, { env, ctx, ...args }: Context) { export async function getCached<Env extends CloudflareEnv = CloudflareEnv>(
config: CloudflareBkndConfig<Env>,
{ env, ctx, ...args }: Context<Env>,
) {
const { kv } = config.bindings?.(env)!; const { kv } = config.bindings?.(env)!;
if (!kv) throw new Error("kv namespace is not defined in cloudflare.bindings"); if (!kv) throw new Error("kv namespace is not defined in cloudflare.bindings");
const key = config.key ?? "app"; const key = config.key ?? "app";
@@ -16,10 +20,11 @@ export async function getCached(config: CloudflareBkndConfig, { env, ctx, ...arg
const app = await createRuntimeApp( const app = await createRuntimeApp(
{ {
...makeCfConfig(config, { env, ctx, ...args }), ...makeConfig(config, env),
initialConfig, initialConfig,
onBuilt: async (app) => { onBuilt: async (app) => {
app.module.server.client.get("/__bknd/cache", async (c) => { registerAsyncsExecutionContext(app, ctx);
app.module.server.client.get(constants.cache_endpoint, async (c) => {
await kv.delete(key); await kv.delete(key);
return c.json({ message: "Cache cleared" }); return c.json({ message: "Cache cleared" });
}); });
+9 -8
View File
@@ -1,9 +1,13 @@
import { DurableObject } from "cloudflare:workers"; import { DurableObject } from "cloudflare:workers";
import type { App, CreateAppConfig } from "bknd"; import type { App, CreateAppConfig } from "bknd";
import { createRuntimeApp, makeConfig } from "bknd/adapter"; import { createRuntimeApp, makeConfig } from "bknd/adapter";
import type { CloudflareBkndConfig, Context } from "../index"; import type { CloudflareBkndConfig, Context, CloudflareEnv } from "../index";
import { constants, registerAsyncsExecutionContext } from "../config";
export async function getDurable(config: CloudflareBkndConfig, ctx: Context) { export async function getDurable<Env extends CloudflareEnv = CloudflareEnv>(
config: CloudflareBkndConfig<Env>,
ctx: Context<Env>,
) {
const { dobj } = config.bindings?.(ctx.env)!; const { dobj } = config.bindings?.(ctx.env)!;
if (!dobj) throw new Error("durable object is not defined in cloudflare.bindings"); if (!dobj) throw new Error("durable object is not defined in cloudflare.bindings");
const key = config.key ?? "app"; const key = config.key ?? "app";
@@ -17,7 +21,7 @@ export async function getDurable(config: CloudflareBkndConfig, ctx: Context) {
const id = dobj.idFromName(key); const id = dobj.idFromName(key);
const stub = dobj.get(id) as unknown as DurableBkndApp; const stub = dobj.get(id) as unknown as DurableBkndApp;
const create_config = makeConfig(config, ctx); const create_config = makeConfig(config, ctx.env);
const res = await stub.fire(ctx.request, { const res = await stub.fire(ctx.request, {
config: create_config, config: create_config,
@@ -67,7 +71,8 @@ export class DurableBkndApp extends DurableObject {
this.app = await createRuntimeApp({ this.app = await createRuntimeApp({
...config, ...config,
onBuilt: async (app) => { onBuilt: async (app) => {
app.modules.server.get("/__do", async (c) => { registerAsyncsExecutionContext(app, this.ctx);
app.modules.server.get(constants.do_endpoint, async (c) => {
// @ts-ignore // @ts-ignore
const context: any = c.req.raw.cf ? c.req.raw.cf : c.env.cf; const context: any = c.req.raw.cf ? c.req.raw.cf : c.env.cf;
return c.json({ return c.json({
@@ -92,7 +97,6 @@ export class DurableBkndApp extends DurableObject {
this.keepAlive(options.keepAliveSeconds); this.keepAlive(options.keepAliveSeconds);
} }
console.log("id", this.id);
const res = await this.app!.fetch(request); const res = await this.app!.fetch(request);
const headers = new Headers(res.headers); const headers = new Headers(res.headers);
headers.set("X-BuildTime", buildtime.toString()); headers.set("X-BuildTime", buildtime.toString());
@@ -109,16 +113,13 @@ export class DurableBkndApp extends DurableObject {
async beforeBuild(app: App) {} async beforeBuild(app: App) {}
protected keepAlive(seconds: number) { protected keepAlive(seconds: number) {
console.log("keep alive for", seconds);
if (this.interval) { if (this.interval) {
console.log("clearing, there is a new");
clearInterval(this.interval); clearInterval(this.interval);
} }
let i = 0; let i = 0;
this.interval = setInterval(() => { this.interval = setInterval(() => {
i += 1; i += 1;
//console.log("keep-alive", i);
if (i === seconds) { if (i === seconds) {
console.log("cleared"); console.log("cleared");
clearInterval(this.interval); clearInterval(this.interval);
+37 -16
View File
@@ -1,27 +1,48 @@
import type { App } from "bknd"; import { createRuntimeApp, type RuntimeOptions } from "bknd/adapter";
import { createRuntimeApp } from "bknd/adapter"; import type { CloudflareBkndConfig, Context, CloudflareEnv } from "../index";
import { type CloudflareBkndConfig, type Context, makeCfConfig } from "../index"; import { makeConfig, registerAsyncsExecutionContext } from "../config";
export async function makeApp(config: CloudflareBkndConfig, ctx: Context) { export async function makeApp<Env extends CloudflareEnv = CloudflareEnv>(
return await createRuntimeApp( config: CloudflareBkndConfig<Env>,
args: Env = {} as Env,
opts?: RuntimeOptions,
) {
return await createRuntimeApp<Env>(
{ {
...makeCfConfig(config, ctx), ...makeConfig(config, args),
adminOptions: config.html ? { html: config.html } : undefined, adminOptions: config.html ? { html: config.html } : undefined,
}, },
ctx, args,
opts,
); );
} }
export async function getFresh(config: CloudflareBkndConfig, ctx: Context) { export async function getWarm<Env extends CloudflareEnv = CloudflareEnv>(
const app = await makeApp(config, ctx); config: CloudflareBkndConfig<Env>,
ctx: Context<Env>,
opts: RuntimeOptions = {},
) {
const app = await makeApp(
{
...config,
onBuilt: async (app) => {
registerAsyncsExecutionContext(app, ctx.ctx);
config.onBuilt?.(app);
},
},
ctx.env,
opts,
);
return app.fetch(ctx.request); return app.fetch(ctx.request);
} }
let warm_app: App; export async function getFresh<Env extends CloudflareEnv = CloudflareEnv>(
export async function getWarm(config: CloudflareBkndConfig, ctx: Context) { config: CloudflareBkndConfig<Env>,
if (!warm_app) { ctx: Context<Env>,
warm_app = await makeApp(config, ctx); opts: RuntimeOptions = {},
} ) {
return await getWarm(config, ctx, {
return warm_app.fetch(ctx.request); ...opts,
force: true,
});
} }
@@ -1,7 +1,7 @@
import { createWriteStream, readFileSync } from "node:fs"; import { createWriteStream, readFileSync } from "node:fs";
import { test } from "node:test"; import { test } from "node:test";
import { Miniflare } from "miniflare"; import { Miniflare } from "miniflare";
import { StorageR2Adapter } from "adapter/cloudflare/StorageR2Adapter"; import { StorageR2Adapter } from "./StorageR2Adapter";
import { adapterTestSuite } from "media"; import { adapterTestSuite } from "media";
import { nodeTestRunner } from "adapter/node"; import { nodeTestRunner } from "adapter/node";
import path from "node:path"; import path from "node:path";
@@ -23,7 +23,7 @@ test("StorageR2Adapter", async () => {
const bucket = (await mf.getR2Bucket("BUCKET")) as unknown as R2Bucket; const bucket = (await mf.getR2Bucket("BUCKET")) as unknown as R2Bucket;
const adapter = new StorageR2Adapter(bucket); const adapter = new StorageR2Adapter(bucket);
const basePath = path.resolve(import.meta.dirname, "../_assets"); const basePath = path.resolve(import.meta.dirname, "../../../../__test__/_assets");
const buffer = readFileSync(path.join(basePath, "image.png")); const buffer = readFileSync(path.join(basePath, "image.png"));
const file = new File([buffer], "image.png", { type: "image/png" }); const file = new File([buffer], "image.png", { type: "image/png" });
@@ -1,10 +1,8 @@
import { registries } from "bknd"; import { registries } from "bknd";
import { isDebug } from "bknd/core"; import { isDebug } from "bknd/core";
import { StringEnum, Type } from "bknd/utils"; import { StringEnum, Type } from "bknd/utils";
import type { FileBody } from "media/storage/Storage"; import { guessMimeType as guess, StorageAdapter, type FileBody } from "bknd/media";
import { StorageAdapter } from "media/storage/StorageAdapter"; import { getBindings } from "../bindings";
import { guess } from "media/storage/mime-types-tiny";
import { getBindings } from "./bindings";
export function makeSchema(bindings: string[] = []) { export function makeSchema(bindings: string[] = []) {
return Type.Object( return Type.Object(
+80 -43
View File
@@ -12,76 +12,113 @@ export type BkndConfig<Args = any> = CreateAppConfig & {
export type FrameworkBkndConfig<Args = any> = BkndConfig<Args>; export type FrameworkBkndConfig<Args = any> = BkndConfig<Args>;
export type CreateAdapterAppOptions = {
force?: boolean;
id?: string;
};
export type FrameworkOptions = CreateAdapterAppOptions;
export type RuntimeOptions = CreateAdapterAppOptions;
export type RuntimeBkndConfig<Args = any> = BkndConfig<Args> & { export type RuntimeBkndConfig<Args = any> = BkndConfig<Args> & {
distPath?: string; distPath?: string;
serveStatic?: MiddlewareHandler | [string, MiddlewareHandler]; serveStatic?: MiddlewareHandler | [string, MiddlewareHandler];
adminOptions?: AdminControllerOptions | false; adminOptions?: AdminControllerOptions | false;
}; };
export function makeConfig<Args = any>(config: BkndConfig<Args>, args?: Args): CreateAppConfig { export type DefaultArgs = {
[key: string]: any;
};
export function makeConfig<Args = DefaultArgs>(
config: BkndConfig<Args>,
args?: Args,
): CreateAppConfig {
let additionalConfig: CreateAppConfig = {}; let additionalConfig: CreateAppConfig = {};
if ("app" in config && config.app) { const { app, ...rest } = config;
if (typeof config.app === "function") { if (app) {
if (typeof app === "function") {
if (!args) { if (!args) {
throw new Error("args is required when config.app is a function"); throw new Error("args is required when config.app is a function");
} }
additionalConfig = config.app(args); additionalConfig = app(args);
} else { } else {
additionalConfig = config.app; additionalConfig = app;
} }
} }
return { ...config, ...additionalConfig }; return { ...rest, ...additionalConfig };
} }
export async function createFrameworkApp<Args = any>( // a map that contains all apps by id
config: FrameworkBkndConfig, const apps = new Map<string, App>();
export async function createAdapterApp<Config extends BkndConfig = BkndConfig, Args = DefaultArgs>(
config: Config = {} as Config,
args?: Args, args?: Args,
opts?: CreateAdapterAppOptions,
): Promise<App> { ): Promise<App> {
const app = App.create(makeConfig(config, args)); const id = opts?.id ?? "app";
let app = apps.get(id);
if (!app || opts?.force) {
app = App.create(makeConfig(config, args));
apps.set(id, app);
}
return app;
}
if (config.onBuilt) { export async function createFrameworkApp<Args = DefaultArgs>(
config: FrameworkBkndConfig = {},
args?: Args,
opts?: FrameworkOptions,
): Promise<App> {
const app = await createAdapterApp(config, args, opts);
if (!app.isBuilt()) {
if (config.onBuilt) {
app.emgr.onEvent(
App.Events.AppBuiltEvent,
async () => {
await config.onBuilt?.(app);
},
"sync",
);
}
await config.beforeBuild?.(app);
await app.build(config.buildConfig);
}
return app;
}
export async function createRuntimeApp<Args = DefaultArgs>(
{ serveStatic, adminOptions, ...config }: RuntimeBkndConfig<Args> = {},
args?: Args,
opts?: RuntimeOptions,
): Promise<App> {
const app = await createAdapterApp(config, args, opts);
if (!app.isBuilt()) {
app.emgr.onEvent( app.emgr.onEvent(
App.Events.AppBuiltEvent, App.Events.AppBuiltEvent,
async () => { async () => {
if (serveStatic) {
const [path, handler] = Array.isArray(serveStatic)
? serveStatic
: [$config.server.assets_path + "*", serveStatic];
app.modules.server.get(path, handler);
}
await config.onBuilt?.(app); await config.onBuilt?.(app);
if (adminOptions !== false) {
app.registerAdminController(adminOptions);
}
}, },
"sync", "sync",
); );
await config.beforeBuild?.(app);
await app.build(config.buildConfig);
} }
await config.beforeBuild?.(app);
await app.build(config.buildConfig);
return app;
}
export async function createRuntimeApp<Env = any>(
{ serveStatic, adminOptions, ...config }: RuntimeBkndConfig,
env?: Env,
): Promise<App> {
const app = App.create(makeConfig(config, env));
app.emgr.onEvent(
App.Events.AppBuiltEvent,
async () => {
if (serveStatic) {
const [path, handler] = Array.isArray(serveStatic)
? serveStatic
: [$config.server.assets_path + "*", serveStatic];
app.modules.server.get(path, handler);
}
await config.onBuilt?.(app);
if (adminOptions !== false) {
app.registerAdminController(adminOptions);
}
},
"sync",
);
await config.beforeBuild?.(app);
await app.build(config.buildConfig);
return app; return app;
} }
@@ -0,0 +1,16 @@
import { afterAll, beforeAll, describe } from "bun:test";
import * as nextjs from "./nextjs.adapter";
import { disableConsoleLog, enableConsoleLog } from "core/utils";
import { adapterTestSuite } from "adapter/adapter-test-suite";
import { bunTestRunner } from "adapter/bun/test";
import type { NextjsBkndConfig } from "./nextjs.adapter";
beforeAll(disableConsoleLog);
afterAll(enableConsoleLog);
describe("nextjs adapter", () => {
adapterTestSuite<NextjsBkndConfig>(bunTestRunner, {
makeApp: nextjs.getApp,
makeHandler: nextjs.serve,
});
});
+17 -32
View File
@@ -1,36 +1,19 @@
import type { App } from "bknd"; import { createFrameworkApp, type FrameworkBkndConfig, type FrameworkOptions } from "bknd/adapter";
import { type FrameworkBkndConfig, createFrameworkApp } from "bknd/adapter"; import { isNode } from "bknd/utils";
import { isNode } from "core/utils"; import type { NextApiRequest } from "next";
export type NextjsBkndConfig = FrameworkBkndConfig & { type NextjsEnv = NextApiRequest["env"];
export type NextjsBkndConfig<Env = NextjsEnv> = FrameworkBkndConfig<Env> & {
cleanRequest?: { searchParams?: string[] }; cleanRequest?: { searchParams?: string[] };
}; };
type NextjsContext = { export async function getApp<Env = NextjsEnv>(
env: Record<string, string | undefined>; config: NextjsBkndConfig<Env>,
}; args: Env = {} as Env,
opts?: FrameworkOptions,
let app: App;
let building: boolean = false;
export async function getApp<Args extends NextjsContext = NextjsContext>(
config: NextjsBkndConfig,
args?: Args,
) { ) {
if (building) { return await createFrameworkApp(config, args ?? (process.env as Env), opts);
while (building) {
await new Promise((resolve) => setTimeout(resolve, 5));
}
if (app) return app;
}
building = true;
if (!app) {
app = await createFrameworkApp(config, args);
await app.build();
}
building = false;
return app;
} }
function getCleanRequest(req: Request, cleanRequest: NextjsBkndConfig["cleanRequest"]) { function getCleanRequest(req: Request, cleanRequest: NextjsBkndConfig["cleanRequest"]) {
@@ -56,11 +39,13 @@ function getCleanRequest(req: Request, cleanRequest: NextjsBkndConfig["cleanRequ
}); });
} }
export function serve({ cleanRequest, ...config }: NextjsBkndConfig = {}) { export function serve<Env = NextjsEnv>(
{ cleanRequest, ...config }: NextjsBkndConfig<Env> = {},
args: Env = {} as Env,
opts?: FrameworkOptions,
) {
return async (req: Request) => { return async (req: Request) => {
if (!app) { const app = await getApp(config, args, opts);
app = await getApp(config, { env: process.env ?? {} });
}
const request = getCleanRequest(req, cleanRequest); const request = getCleanRequest(req, cleanRequest);
return app.fetch(request); return app.fetch(request);
}; };
+10 -1
View File
@@ -5,6 +5,15 @@ export * from "./node.adapter";
export { StorageLocalAdapter, type LocalAdapterConfig }; export { StorageLocalAdapter, type LocalAdapterConfig };
export { nodeTestRunner } from "./test"; export { nodeTestRunner } from "./test";
let registered = false;
export function registerLocalMediaAdapter() { export function registerLocalMediaAdapter() {
registries.media.register("local", StorageLocalAdapter); if (!registered) {
registries.media.register("local", StorageLocalAdapter);
registered = true;
}
return (config: Partial<LocalAdapterConfig> = {}) => {
const adapter = new StorageLocalAdapter(config);
return adapter.toJSON(true);
};
} }
@@ -0,0 +1,15 @@
import { describe, before, after } from "node:test";
import * as node from "./node.adapter";
import { adapterTestSuite } from "adapter/adapter-test-suite";
import { nodeTestRunner } from "adapter/node";
import { disableConsoleLog, enableConsoleLog } from "core/utils";
before(() => disableConsoleLog());
after(enableConsoleLog);
describe("node adapter", () => {
adapterTestSuite(nodeTestRunner, {
makeApp: node.createApp,
makeHandler: node.createHandler,
});
});
+15
View File
@@ -0,0 +1,15 @@
import { afterAll, beforeAll, describe } from "bun:test";
import * as node from "./node.adapter";
import { adapterTestSuite } from "adapter/adapter-test-suite";
import { bunTestRunner } from "adapter/bun/test";
import { disableConsoleLog, enableConsoleLog } from "core/utils";
beforeAll(disableConsoleLog);
afterAll(enableConsoleLog);
describe("node adapter (bun)", () => {
adapterTestSuite(bunTestRunner, {
makeApp: node.createApp,
makeHandler: node.createHandler,
});
});
+36 -23
View File
@@ -2,11 +2,11 @@ import path from "node:path";
import { serve as honoServe } from "@hono/node-server"; import { serve as honoServe } from "@hono/node-server";
import { serveStatic } from "@hono/node-server/serve-static"; import { serveStatic } from "@hono/node-server/serve-static";
import { registerLocalMediaAdapter } from "adapter/node/index"; import { registerLocalMediaAdapter } from "adapter/node/index";
import type { App } from "bknd"; import { type RuntimeBkndConfig, createRuntimeApp, type RuntimeOptions } from "bknd/adapter";
import { type RuntimeBkndConfig, createRuntimeApp } from "bknd/adapter";
import { config as $config } from "bknd/core"; import { config as $config } from "bknd/core";
export type NodeBkndConfig = RuntimeBkndConfig & { type NodeEnv = NodeJS.ProcessEnv;
export type NodeBkndConfig<Env = NodeEnv> = RuntimeBkndConfig<Env> & {
port?: number; port?: number;
hostname?: string; hostname?: string;
listener?: Parameters<typeof honoServe>[1]; listener?: Parameters<typeof honoServe>[1];
@@ -14,14 +14,11 @@ export type NodeBkndConfig = RuntimeBkndConfig & {
relativeDistPath?: string; relativeDistPath?: string;
}; };
export function serve({ export async function createApp<Env = NodeEnv>(
distPath, { distPath, relativeDistPath, ...config }: NodeBkndConfig<Env> = {},
relativeDistPath, args: Env = {} as Env,
port = $config.server.default_port, opts?: RuntimeOptions,
hostname, ) {
listener,
...config
}: NodeBkndConfig = {}) {
const root = path.relative( const root = path.relative(
process.cwd(), process.cwd(),
path.resolve(distPath ?? relativeDistPath ?? "./node_modules/bknd/dist", "static"), path.resolve(distPath ?? relativeDistPath ?? "./node_modules/bknd/dist", "static"),
@@ -30,23 +27,39 @@ export function serve({
console.warn("relativeDistPath is deprecated, please use distPath instead"); console.warn("relativeDistPath is deprecated, please use distPath instead");
} }
let app: App; registerLocalMediaAdapter();
return await createRuntimeApp(
{
...config,
serveStatic: serveStatic({ root }),
},
// @ts-ignore
args ?? { env: process.env },
opts,
);
}
export function createHandler<Env = NodeEnv>(
config: NodeBkndConfig<Env> = {},
args: Env = {} as Env,
opts?: RuntimeOptions,
) {
return async (req: Request) => {
const app = await createApp(config, args ?? (process.env as Env), opts);
return app.fetch(req);
};
}
export function serve<Env = NodeEnv>(
{ port = $config.server.default_port, hostname, listener, ...config }: NodeBkndConfig<Env> = {},
args: Env = {} as Env,
opts?: RuntimeOptions,
) {
honoServe( honoServe(
{ {
port, port,
hostname, hostname,
fetch: async (req: Request) => { fetch: createHandler(config, args, opts),
if (!app) {
registerLocalMediaAdapter();
app = await createRuntimeApp({
...config,
serveStatic: serveStatic({ root }),
});
}
return app.fetch(req);
},
}, },
(connInfo) => { (connInfo) => {
console.log(`Server is running on http://localhost:${connInfo.port}`); console.log(`Server is running on http://localhost:${connInfo.port}`);
@@ -3,6 +3,7 @@ import { StorageLocalAdapter } from "./StorageLocalAdapter";
// @ts-ignore // @ts-ignore
import { assetsPath, assetsTmpPath } from "../../../../__test__/helper"; import { assetsPath, assetsTmpPath } from "../../../../__test__/helper";
import { adapterTestSuite } from "media/storage/adapters/adapter-test-suite"; import { adapterTestSuite } from "media/storage/adapters/adapter-test-suite";
import { bunTestRunner } from "adapter/bun/test";
describe("StorageLocalAdapter (bun)", async () => { describe("StorageLocalAdapter (bun)", async () => {
const adapter = new StorageLocalAdapter({ const adapter = new StorageLocalAdapter({
@@ -10,5 +11,5 @@ describe("StorageLocalAdapter (bun)", async () => {
}); });
const file = Bun.file(`${assetsPath}/image.png`); const file = Bun.file(`${assetsPath}/image.png`);
await adapterTestSuite({ test, expect }, adapter, file); await adapterTestSuite(bunTestRunner, adapter, file);
}); });
@@ -7,14 +7,14 @@ export const localAdapterConfig = Type.Object(
{ {
path: Type.String({ default: "./" }), path: Type.String({ default: "./" }),
}, },
{ title: "Local", description: "Local file system storage" }, { title: "Local", description: "Local file system storage", additionalProperties: false },
); );
export type LocalAdapterConfig = Static<typeof localAdapterConfig>; export type LocalAdapterConfig = Static<typeof localAdapterConfig>;
export class StorageLocalAdapter extends StorageAdapter { export class StorageLocalAdapter extends StorageAdapter {
private config: LocalAdapterConfig; private config: LocalAdapterConfig;
constructor(config: any) { constructor(config: Partial<LocalAdapterConfig> = {}) {
super(); super();
this.config = parse(localAdapterConfig, config); this.config = parse(localAdapterConfig, config);
} }
+24
View File
@@ -2,6 +2,17 @@ import nodeAssert from "node:assert/strict";
import { test } from "node:test"; import { test } from "node:test";
import type { Matcher, Test, TestFn, TestRunner } from "core/test"; import type { Matcher, Test, TestFn, TestRunner } from "core/test";
// Track mock function calls
const mockCalls = new WeakMap<Function, number>();
function createMockFunction<T extends (...args: any[]) => any>(fn: T): T {
const mockFn = (...args: Parameters<T>) => {
const currentCalls = mockCalls.get(mockFn) || 0;
mockCalls.set(mockFn, currentCalls + 1);
return fn(...args);
};
return mockFn as T;
}
const nodeTestMatcher = <T = unknown>(actual: T, parentFailMsg?: string) => const nodeTestMatcher = <T = unknown>(actual: T, parentFailMsg?: string) =>
({ ({
toEqual: (expected: T, failMsg = parentFailMsg) => { toEqual: (expected: T, failMsg = parentFailMsg) => {
@@ -23,6 +34,18 @@ const nodeTestMatcher = <T = unknown>(actual: T, parentFailMsg?: string) =>
const e = Array.isArray(expected) ? expected : [expected]; const e = Array.isArray(expected) ? expected : [expected];
nodeAssert.ok(e.includes(actual), failMsg); nodeAssert.ok(e.includes(actual), failMsg);
}, },
toHaveBeenCalled: (failMsg = parentFailMsg) => {
const calls = mockCalls.get(actual as Function) || 0;
nodeAssert.ok(calls > 0, failMsg || "Expected function to have been called at least once");
},
toHaveBeenCalledTimes: (expected: number, failMsg = parentFailMsg) => {
const calls = mockCalls.get(actual as Function) || 0;
nodeAssert.strictEqual(
calls,
expected,
failMsg || `Expected function to have been called ${expected} times`,
);
},
}) satisfies Matcher<T>; }) satisfies Matcher<T>;
const nodeTestResolverProxy = <T = unknown>( const nodeTestResolverProxy = <T = unknown>(
@@ -63,6 +86,7 @@ nodeTest.skipIf = (condition: boolean): Test => {
export const nodeTestRunner: TestRunner = { export const nodeTestRunner: TestRunner = {
test: nodeTest, test: nodeTest,
mock: createMockFunction,
expect: <T = unknown>(actual?: T, failMsg?: string) => ({ expect: <T = unknown>(actual?: T, failMsg?: string) => ({
...nodeTestMatcher(actual, failMsg), ...nodeTestMatcher(actual, failMsg),
resolves: nodeTestResolverProxy(actual as Promise<T>, { resolves: nodeTestResolverProxy(actual as Promise<T>, {
@@ -0,0 +1,15 @@
import { afterAll, beforeAll, describe } from "bun:test";
import * as rr from "./react-router.adapter";
import { disableConsoleLog, enableConsoleLog } from "core/utils";
import { adapterTestSuite } from "adapter/adapter-test-suite";
import { bunTestRunner } from "adapter/bun/test";
beforeAll(disableConsoleLog);
afterAll(enableConsoleLog);
describe("react-router adapter", () => {
adapterTestSuite(bunTestRunner, {
makeApp: rr.getApp,
makeHandler: (c, a, o) => (request: Request) => rr.serve(c, a?.env, o)({ request }),
});
});
@@ -1,39 +1,26 @@
import type { App } from "bknd";
import { type FrameworkBkndConfig, createFrameworkApp } from "bknd/adapter"; import { type FrameworkBkndConfig, createFrameworkApp } from "bknd/adapter";
import type { FrameworkOptions } from "adapter";
type ReactRouterContext = { type ReactRouterEnv = NodeJS.ProcessEnv;
type ReactRouterFunctionArgs = {
request: Request; request: Request;
}; };
export type ReactRouterBkndConfig<Args = ReactRouterContext> = FrameworkBkndConfig<Args>; export type ReactRouterBkndConfig<Env = ReactRouterEnv> = FrameworkBkndConfig<Env>;
let app: App; export async function getApp<Env = ReactRouterEnv>(
let building: boolean = false; config: ReactRouterBkndConfig<Env>,
args: Env = {} as Env,
export async function getApp<Args extends ReactRouterContext = ReactRouterContext>( opts?: FrameworkOptions,
config: ReactRouterBkndConfig<Args>,
args?: Args,
) { ) {
if (building) { return await createFrameworkApp(config, args ?? process.env, opts);
while (building) {
await new Promise((resolve) => setTimeout(resolve, 5));
}
if (app) return app;
}
building = true;
if (!app) {
app = await createFrameworkApp(config, args);
await app.build();
}
building = false;
return app;
} }
export function serve<Args extends ReactRouterContext = ReactRouterContext>( export function serve<Env = ReactRouterEnv>(
config: ReactRouterBkndConfig<Args> = {}, config: ReactRouterBkndConfig<Env> = {},
args: Env = {} as Env,
opts?: FrameworkOptions,
) { ) {
return async (args: Args) => { return async (fnArgs: ReactRouterFunctionArgs) => {
app = await getApp(config, args); return (await getApp(config, args, opts)).fetch(fnArgs.request);
return app.fetch(args.request);
}; };
} }
+3 -3
View File
@@ -1,4 +1,4 @@
import type { Permission } from "core"; import { $console, type Permission } from "core";
import { patternMatch } from "core/utils"; import { patternMatch } from "core/utils";
import type { Context } from "hono"; import type { Context } from "hono";
import { createMiddleware } from "hono/factory"; import { createMiddleware } from "hono/factory";
@@ -49,7 +49,7 @@ export const auth = (options?: {
// make sure to only register once // make sure to only register once
if (authCtx.registered) { if (authCtx.registered) {
skipped = true; skipped = true;
console.warn(`auth middleware already registered for ${getPath(c)}`); $console.warn(`auth middleware already registered for ${getPath(c)}`);
} else { } else {
authCtx.registered = true; authCtx.registered = true;
@@ -93,7 +93,7 @@ export const permission = (
if (app?.module.auth.enabled) { if (app?.module.auth.enabled) {
throw new Error(msg); throw new Error(msg);
} else { } else {
console.warn(msg); $console.warn(msg);
} }
} else if (!authCtx.skip) { } else if (!authCtx.skip) {
const guard = app.modules.ctx().guard; const guard = app.modules.ctx().guard;
+26
View File
@@ -24,6 +24,32 @@ export async function overrideJson<File extends object = object>(
await writeFile(pkgPath, JSON.stringify(newPkg, null, opts?.indent || 2)); await writeFile(pkgPath, JSON.stringify(newPkg, null, opts?.indent || 2));
} }
export async function upsertEnvFile(
kv: Record<string, string | number>,
opts?: { dir?: string; file?: string },
) {
const file = opts?.file ?? ".env";
const envPath = path.resolve(opts?.dir ?? process.cwd(), file);
const current: Record<string, string | number> = {};
try {
const values = await readFile(envPath, "utf-8");
const lines = values.split("\n");
for (const line of lines) {
const [key, value] = line.split("=");
if (key && value) {
current[key] = value;
}
}
} catch (e) {
await writeFile(envPath, "");
}
const newEnv = { ...current, ...kv };
const lines = Object.entries(newEnv).map(([key, value]) => `${key}=${value}`);
await writeFile(envPath, lines.join("\n"));
}
export async function overridePackageJson( export async function overridePackageJson(
fn: (pkg: TPackageJson) => Promise<TPackageJson> | TPackageJson, fn: (pkg: TPackageJson) => Promise<TPackageJson> | TPackageJson,
opts?: { dir?: string }, opts?: { dir?: string },
@@ -0,0 +1,85 @@
import * as $p from "@clack/prompts";
import { upsertEnvFile } from "cli/commands/create/npm";
import { typewriter } from "cli/utils/cli";
import c from "picocolors";
import type { Template } from ".";
import open from "open";
export const aws = {
key: "aws",
title: "AWS Lambda Basic",
integration: "aws",
description: "A basic bknd AWS Lambda starter",
path: "gh:bknd-io/bknd/examples/aws-lambda",
ref: true,
setup: async (ctx) => {
await $p.stream.info(
(async function* () {
yield* typewriter("You need a running LibSQL instance for this adapter to work.");
})(),
);
const choice = await $p.select({
message: "How do you want to proceed?",
options: [
{ label: "Enter instance details", value: "enter" },
{ label: "Create a new instance", value: "new" },
],
});
if ($p.isCancel(choice)) {
process.exit(1);
}
if (choice === "new") {
await $p.stream.info(
(async function* () {
yield* typewriter(c.dim("Proceed on turso.tech to create your instance."));
})(),
);
await open("https://sqlite.new");
}
const url = await $p.text({
message: "Enter database URL",
placeholder: "libsql://<instance>.turso.io",
validate: (v) => {
if (!v) {
return "Invalid URL";
}
return;
},
});
if ($p.isCancel(url)) {
process.exit(1);
}
const token = await $p.text({
message: "Enter database token",
placeholder: "eyJhbGciOiJIUzI1NiIsInR...",
validate: (v) => {
if (!v) {
return "";
}
return;
},
});
if ($p.isCancel(token)) {
process.exit(1);
}
await upsertEnvFile(
{
DB_URL: url,
DB_TOKEN: token ?? "",
},
{ dir: ctx.dir },
);
await $p.stream.info(
(async function* () {
yield* typewriter(`Connection details written to ${c.cyan(".env")}`);
})(),
);
},
} as const satisfies Template;
@@ -78,6 +78,12 @@ async function createD1(ctx: TemplateSetupCtx) {
process.exit(1); process.exit(1);
} }
await $p.stream.info(
(async function* () {
yield* typewriter("Now running wrangler to create a D1 database...");
})(),
);
exec(`npx wrangler d1 create ${name}`); exec(`npx wrangler d1 create ${name}`);
await $p.stream.info( await $p.stream.info(
(async function* () { (async function* () {
+2 -1
View File
@@ -72,7 +72,8 @@ export async function getConfigPath(filePath?: string) {
} }
} }
const paths = ["./bknd.config", "./bknd.config.ts", "./bknd.config.js"]; const exts = ["", ".js", ".ts", ".mjs", ".cjs", ".json"];
const paths = exts.map((e) => `bknd.config${e}`);
for (const p of paths) { for (const p of paths) {
const _p = path.resolve(process.cwd(), p); const _p = path.resolve(process.cwd(), p);
if (await fileExists(_p)) { if (await fileExists(_p)) {
+27 -21
View File
@@ -7,6 +7,7 @@ import { colorizeConsole, config } from "core";
import dotenv from "dotenv"; import dotenv from "dotenv";
import { registries } from "modules/registries"; import { registries } from "modules/registries";
import c from "picocolors"; import c from "picocolors";
import path from "node:path";
import { import {
PLATFORMS, PLATFORMS,
type Platform, type Platform,
@@ -15,8 +16,12 @@ import {
getConnectionCredentialsFromEnv, getConnectionCredentialsFromEnv,
startServer, startServer,
} from "./platform"; } from "./platform";
import { makeConfig } from "adapter";
dotenv.config(); const env_files = [".env", ".dev.vars"];
dotenv.config({
path: env_files.map((file) => path.resolve(process.cwd(), file)),
});
const isBun = typeof Bun !== "undefined"; const isBun = typeof Bun !== "undefined";
export const run: CliCommand = (program) => { export const run: CliCommand = (program) => {
@@ -85,24 +90,12 @@ async function makeApp(config: MakeAppConfig) {
return app; return app;
} }
export async function makeConfigApp(config: CliBkndConfig, platform?: Platform) { export async function makeConfigApp(_config: CliBkndConfig, platform?: Platform) {
const appConfig = typeof config.app === "function" ? config.app(process.env) : config.app; const config = makeConfig(_config, { env: process.env });
const app = App.create(appConfig); return makeApp({
...config,
app.emgr.onEvent( server: { platform },
App.Events.AppBuiltEvent, });
async () => {
await attachServeStatic(app, platform ?? "node");
app.registerAdminController();
await config.onBuilt?.(app);
},
"sync",
);
await config.beforeBuild?.(app);
await app.build(config.buildConfig);
return app;
} }
async function action(options: { async function action(options: {
@@ -118,19 +111,31 @@ async function action(options: {
const configFilePath = await getConfigPath(options.config); const configFilePath = await getConfigPath(options.config);
let app: App | undefined = undefined; let app: App | undefined = undefined;
// first start from arguments if given
if (options.dbUrl) { if (options.dbUrl) {
console.info("Using connection from", c.cyan("--db-url")); console.info("Using connection from", c.cyan("--db-url"));
const connection = options.dbUrl const connection = options.dbUrl
? { url: options.dbUrl, authToken: options.dbToken } ? { url: options.dbUrl, authToken: options.dbToken }
: undefined; : undefined;
app = await makeApp({ connection, server: { platform: options.server } }); app = await makeApp({ connection, server: { platform: options.server } });
// check configuration file to be present
} else if (configFilePath) { } else if (configFilePath) {
console.info("Using config from", c.cyan(configFilePath)); console.info("Using config from", c.cyan(configFilePath));
const config = (await import(configFilePath).then((m) => m.default)) as CliBkndConfig; try {
app = await makeConfigApp(config, options.server); const config = (await import(configFilePath).then((m) => m.default)) as CliBkndConfig;
app = await makeConfigApp(config, options.server);
} catch (e) {
console.error("Failed to load config:", e);
process.exit(1);
}
// try to use an in-memory connection
} else if (options.memory) { } else if (options.memory) {
console.info("Using", c.cyan("in-memory"), "connection"); console.info("Using", c.cyan("in-memory"), "connection");
app = await makeApp({ server: { platform: options.server } }); app = await makeApp({ server: { platform: options.server } });
// finally try to use env variables
} else { } else {
const credentials = getConnectionCredentialsFromEnv(); const credentials = getConnectionCredentialsFromEnv();
if (credentials) { if (credentials) {
@@ -139,6 +144,7 @@ async function action(options: {
} }
} }
// if nothing helps, create a file based app
if (!app) { if (!app) {
const connection = { url: "file:data.db" } as Config; const connection = { url: "file:data.db" } as Config;
console.info("Using connection", c.cyan(connection.url)); console.info("Using connection", c.cyan(connection.url));
+2 -5
View File
@@ -1,12 +1,9 @@
import type { CreateAppConfig } from "App"; import type { BkndConfig } from "adapter";
import type { FrameworkBkndConfig } from "adapter";
import type { Command } from "commander"; import type { Command } from "commander";
export type CliCommand = (program: Command) => void; export type CliCommand = (program: Command) => void;
export type CliBkndConfig<Env = any> = FrameworkBkndConfig & { export type CliBkndConfig<Env = any> = BkndConfig & {
app: CreateAppConfig | ((env: Env) => CreateAppConfig);
setAdminHtml?: boolean;
server?: { server?: {
port?: number; port?: number;
platform?: "node" | "bun"; platform?: "node" | "bun";
+41 -15
View File
@@ -65,27 +65,53 @@ function __tty(_type: any, args: any[]) {
} }
export type TConsoleSeverity = keyof typeof __consoles; export type TConsoleSeverity = keyof typeof __consoles;
const level = env("cli_log_level", "log"); declare global {
var __consoleConfig:
| {
level: TConsoleSeverity;
id?: string;
}
| undefined;
}
// Ensure the config exists only once globally
const defaultLevel = env("cli_log_level", "log") as TConsoleSeverity;
// biome-ignore lint/suspicious/noAssignInExpressions: <explanation>
const config = (globalThis.__consoleConfig ??= {
level: defaultLevel,
//id: crypto.randomUUID(), // for debugging
});
const keys = Object.keys(__consoles); const keys = Object.keys(__consoles);
export const $console = new Proxy( export const $console = new Proxy(config as any, {
{}, get: (_, prop) => {
{ switch (prop) {
get: (_, prop) => { case "original":
if (prop === "original") {
return console; return console;
} case "setLevel":
return (l: TConsoleSeverity) => {
config.level = l;
};
case "resetLevel":
return () => {
config.level = defaultLevel;
};
}
const current = keys.indexOf(level as string); const current = keys.indexOf(config.level);
const requested = keys.indexOf(prop as string); const requested = keys.indexOf(prop as string);
if (prop in __consoles && requested <= current) {
return (...args: any[]) => __tty(prop, args); if (prop in __consoles && requested <= current) {
} return (...args: any[]) => __tty(prop, args);
return () => null; }
}, return () => null;
}, },
) as typeof console & { }) as typeof console & {
original: typeof console; original: typeof console;
} & {
setLevel: (l: TConsoleSeverity) => void;
resetLevel: () => void;
}; };
export function colorizeConsole(con: typeof console) { export function colorizeConsole(con: typeof console) {
+12 -6
View File
@@ -22,6 +22,7 @@ export class EventManager<
protected events: EventClass[] = []; protected events: EventClass[] = [];
protected listeners: EventListener[] = []; protected listeners: EventListener[] = [];
enabled: boolean = true; enabled: boolean = true;
protected asyncs: (() => Promise<void>)[] = [];
constructor( constructor(
events?: RegisteredEvents, events?: RegisteredEvents,
@@ -29,7 +30,6 @@ export class EventManager<
listeners?: EventListener[]; listeners?: EventListener[];
onError?: (event: Event, e: unknown) => void; onError?: (event: Event, e: unknown) => void;
onInvalidReturn?: (event: Event, e: InvalidEventReturn) => void; onInvalidReturn?: (event: Event, e: InvalidEventReturn) => void;
asyncExecutor?: typeof Promise.all;
}, },
) { ) {
if (events) { if (events) {
@@ -176,9 +176,15 @@ export class EventManager<
this.events.forEach((event) => this.onEvent(event, handler, config)); this.events.forEach((event) => this.onEvent(event, handler, config));
} }
protected executeAsyncs(promises: (() => Promise<void>)[]) { protected collectAsyncs(promises: (() => Promise<void>)[]) {
const executor = this.options?.asyncExecutor ?? ((e) => Promise.all(e)); this.asyncs.push(...promises);
executor(promises.map((p) => p())).then(() => void 0); }
async executeAsyncs(executor: typeof Promise.all = (e) => Promise.all(e)): Promise<void> {
if (this.asyncs.length === 0) return;
const asyncs = [...this.asyncs];
this.asyncs = [];
await executor(asyncs.map((p) => p()));
} }
async emit<Actual extends Event<any, any>>(event: Actual): Promise<Actual> { async emit<Actual extends Event<any, any>>(event: Actual): Promise<Actual> {
@@ -209,8 +215,8 @@ export class EventManager<
return !listener.once; return !listener.once;
}); });
// execute asyncs // collect asyncs
this.executeAsyncs(asyncs); this.collectAsyncs(asyncs);
// execute syncs // execute syncs
let _event: Actual = event; let _event: Actual = event;
+3
View File
@@ -5,6 +5,8 @@ export type Matcher<T = unknown> = {
toBeString: (failMsg?: string) => void; toBeString: (failMsg?: string) => void;
toBeOneOf: (expected: T | Array<T> | Iterable<T>, failMsg?: string) => void; toBeOneOf: (expected: T | Array<T> | Iterable<T>, failMsg?: string) => void;
toBeDefined: (failMsg?: string) => void; toBeDefined: (failMsg?: string) => void;
toHaveBeenCalled: (failMsg?: string) => void;
toHaveBeenCalledTimes: (expected: number, failMsg?: string) => void;
}; };
export type TestFn = (() => void | Promise<unknown>) | ((done: (err?: unknown) => void) => void); export type TestFn = (() => void | Promise<unknown>) | ((done: (err?: unknown) => void) => void);
export interface Test { export interface Test {
@@ -15,6 +17,7 @@ export interface Test {
} }
export type TestRunner = { export type TestRunner = {
test: Test; test: Test;
mock: <T extends (...args: any[]) => any>(fn: T) => T | any;
expect: <T = unknown>( expect: <T = unknown>(
actual?: T, actual?: T,
failMsg?: string, failMsg?: string,
+4 -9
View File
@@ -1,3 +1,5 @@
import { $console } from "core";
type ConsoleSeverity = "log" | "warn" | "error"; type ConsoleSeverity = "log" | "warn" | "error";
const _oldConsoles = { const _oldConsoles = {
log: console.log, log: console.log,
@@ -34,21 +36,14 @@ export function disableConsoleLog(severities: ConsoleSeverity[] = ["log", "warn"
severities.forEach((severity) => { severities.forEach((severity) => {
console[severity] = () => null; console[severity] = () => null;
}); });
return enableConsoleLog; $console.setLevel("error");
} }
export function enableConsoleLog() { export function enableConsoleLog() {
Object.entries(_oldConsoles).forEach(([severity, fn]) => { Object.entries(_oldConsoles).forEach(([severity, fn]) => {
console[severity as ConsoleSeverity] = fn; console[severity as ConsoleSeverity] = fn;
}); });
} $console.resetLevel();
export function tryit(fn: () => void, fallback?: any) {
try {
return fn();
} catch (e) {
return fallback || e;
}
} }
export function formatMemoryUsage() { export function formatMemoryUsage() {
@@ -6,6 +6,7 @@ import { type DatabaseIntrospector, Kysely, ParseJSONResultsPlugin } from "kysel
import type { QB } from "../Connection"; import type { QB } from "../Connection";
import { SqliteConnection } from "./SqliteConnection"; import { SqliteConnection } from "./SqliteConnection";
import { SqliteIntrospector } from "./SqliteIntrospector"; import { SqliteIntrospector } from "./SqliteIntrospector";
import { $console } from "core";
export const LIBSQL_PROTOCOLS = ["wss", "https", "libsql"] as const; export const LIBSQL_PROTOCOLS = ["wss", "https", "libsql"] as const;
export type LibSqlCredentials = Config & { export type LibSqlCredentials = Config & {
@@ -33,6 +34,7 @@ export class LibsqlConnection extends SqliteConnection {
constructor(credentials: LibSqlCredentials); constructor(credentials: LibSqlCredentials);
constructor(clientOrCredentials: Client | LibSqlCredentials) { constructor(clientOrCredentials: Client | LibSqlCredentials) {
let client: Client; let client: Client;
let batching_enabled = true;
if (clientOrCredentials && "url" in clientOrCredentials) { if (clientOrCredentials && "url" in clientOrCredentials) {
let { url, authToken, protocol } = clientOrCredentials; let { url, authToken, protocol } = clientOrCredentials;
if (protocol && LIBSQL_PROTOCOLS.includes(protocol)) { if (protocol && LIBSQL_PROTOCOLS.includes(protocol)) {
@@ -42,6 +44,13 @@ export class LibsqlConnection extends SqliteConnection {
} }
client = createClient({ url, authToken }); client = createClient({ url, authToken });
// currently there is an issue in limbo implementation
// that prevents batching from working correctly
if (/\.aws.*turso\.io$/.test(url)) {
$console.warn("Using an Turso AWS endpoint currently disables batching support");
batching_enabled = false;
}
} else { } else {
client = clientOrCredentials; client = clientOrCredentials;
} }
@@ -54,6 +63,7 @@ export class LibsqlConnection extends SqliteConnection {
super(kysely, {}, plugins); super(kysely, {}, plugins);
this.client = client; this.client = client;
this.supported.batching = batching_enabled;
} }
getClient(): Client { getClient(): Client {
+80 -31
View File
@@ -27,9 +27,9 @@ export type RepositoryResponse<T = EntityData[]> = RepositoryRawResponse & {
entity: Entity; entity: Entity;
data: T; data: T;
meta: { meta: {
total: number;
count: number;
items: number; items: number;
total?: number;
count?: number;
time?: number; time?: number;
query?: { query?: {
sql: string; sql: string;
@@ -47,6 +47,7 @@ export type RepositoryExistsResponse = RepositoryRawResponse & {
export type RepositoryOptions = { export type RepositoryOptions = {
silent?: boolean; silent?: boolean;
includeCounts?: boolean;
emgr?: EventManager<any>; emgr?: EventManager<any>;
}; };
@@ -59,13 +60,17 @@ export class Repository<TBD extends object = DefaultDB, TB extends keyof TBD = a
constructor( constructor(
public em: EntityManager<TBD>, public em: EntityManager<TBD>,
public entity: Entity, public entity: Entity,
protected options?: RepositoryOptions, protected options: RepositoryOptions = {},
) { ) {
this.emgr = options?.emgr ?? new EventManager(MutatorEvents); this.emgr = options?.emgr ?? new EventManager(MutatorEvents);
} }
private cloneFor(entity: Entity) { private cloneFor(entity: Entity, opts: Partial<RepositoryOptions> = {}) {
return new Repository(this.em, this.em.entity(entity), { emgr: this.emgr }); return new Repository(this.em, this.em.entity(entity), {
...this.options,
...opts,
emgr: this.emgr,
});
} }
private get conn() { private get conn() {
@@ -172,28 +177,39 @@ export class Repository<TBD extends object = DefaultDB, TB extends keyof TBD = a
if (options.limit) validated.limit = options.limit; if (options.limit) validated.limit = options.limit;
if (options.offset) validated.offset = options.offset; if (options.offset) validated.offset = options.offset;
//$console.debug("Repository: options", { entity: entity.name, options, validated });
return validated; return validated;
} }
protected async performQuery(qb: RepositoryQB): Promise<RepositoryResponse> { protected async executeQb(qb: RepositoryQB) {
const entity = this.entity;
const compiled = qb.compile(); const compiled = qb.compile();
if (this.options?.silent !== true) { if (this.options?.silent !== true) {
$console.debug(`Repository: query\n${compiled.sql}\n`, compiled.parameters); $console.debug(`Repository: query\n${compiled.sql}\n`, compiled.parameters);
} }
const start = performance.now(); let result: any;
const selector = (as = "count") => this.conn.fn.countAll<number>().as(as); try {
const countQuery = qb result = await qb.execute();
.clearSelect() } catch (e) {
.select(selector()) if (this.options?.silent !== true) {
.clearLimit() if (e instanceof Error) {
.clearOffset() $console.error("[ERROR] Repository.executeQb", e.message);
.clearGroupBy() }
.clearOrderBy();
const totalQuery = this.conn.selectFrom(entity.name).select(selector("total")); throw e;
}
}
return {
result,
sql: compiled.sql,
parameters: [...compiled.parameters],
};
}
protected async performQuery(qb: RepositoryQB): Promise<RepositoryResponse> {
const entity = this.entity;
const compiled = qb.compile();
const payload = { const payload = {
entity, entity,
sql: compiled.sql, sql: compiled.sql,
@@ -209,13 +225,53 @@ export class Repository<TBD extends object = DefaultDB, TB extends keyof TBD = a
}, },
}; };
// don't batch (add counts) if `includeCounts` is set to false
// or when explicitly set to true and batching is not supported
if (
this.options?.includeCounts === false ||
(this.options?.includeCounts === true && !this.em.connection.supports("batching"))
) {
const start = performance.now();
const res = await this.executeQb(qb);
const time = Number.parseFloat((performance.now() - start).toFixed(2));
const result = res.result ?? [];
const data = this.em.hydrate(entity.name, result);
return {
...payload,
result,
data,
meta: {
...payload.meta,
total: undefined,
count: undefined,
items: data.length,
time,
},
};
}
if (this.options?.silent !== true) {
$console.debug(`Repository: query\n${compiled.sql}\n`, compiled.parameters);
}
const selector = (as = "count") => this.conn.fn.countAll<number>().as(as);
const countQuery = qb
.clearSelect()
.select(selector())
.clearLimit()
.clearOffset()
.clearGroupBy()
.clearOrderBy();
const totalQuery = this.conn.selectFrom(entity.name).select(selector());
try { try {
const start = performance.now();
const [_count, _total, result] = await this.em.connection.batchQuery([ const [_count, _total, result] = await this.em.connection.batchQuery([
countQuery, countQuery,
totalQuery, totalQuery,
qb, qb,
]); ]);
//$console.log("result", { _count, _total });
const time = Number.parseFloat((performance.now() - start).toFixed(2)); const time = Number.parseFloat((performance.now() - start).toFixed(2));
const data = this.em.hydrate(entity.name, result); const data = this.em.hydrate(entity.name, result);
@@ -227,7 +283,7 @@ export class Repository<TBD extends object = DefaultDB, TB extends keyof TBD = a
meta: { meta: {
...payload.meta, ...payload.meta,
// parsing is important since pg returns string // parsing is important since pg returns string
total: ensureInt(_total[0]?.total), total: ensureInt(_total[0]?.count),
count: ensureInt(_count[0]?.count), count: ensureInt(_count[0]?.count),
items: result.length, items: result.length,
time, time,
@@ -435,8 +491,7 @@ export class Repository<TBD extends object = DefaultDB, TB extends keyof TBD = a
qb = WhereBuilder.addClause(qb, options.where); qb = WhereBuilder.addClause(qb, options.where);
} }
const compiled = qb.compile(); const { result, ...compiled } = await this.executeQb(qb);
const result = await qb.execute();
return { return {
sql: compiled.sql, sql: compiled.sql,
@@ -454,15 +509,9 @@ export class Repository<TBD extends object = DefaultDB, TB extends keyof TBD = a
let qb = this.conn.selectFrom(entity.name).select(selector); let qb = this.conn.selectFrom(entity.name).select(selector);
// add mandatory where // add mandatory where
qb = WhereBuilder.addClause(qb, options.where); qb = WhereBuilder.addClause(qb, options.where).limit(1);
// we only need 1 const { result, ...compiled } = await this.executeQb(qb);
qb = qb.limit(1);
const compiled = qb.compile();
//$console.log("exists query", compiled.sql, compiled.parameters);
const result = await qb.execute();
//$console.log("result", result);
return { return {
sql: compiled.sql, sql: compiled.sql,
+6 -2
View File
@@ -6,6 +6,7 @@ import type { ModuleConfigs } from "modules";
import { import {
BooleanField, BooleanField,
type BooleanFieldConfig, type BooleanFieldConfig,
type Connection,
DateField, DateField,
type DateFieldConfig, type DateFieldConfig,
Entity, Entity,
@@ -171,8 +172,6 @@ export class FieldPrototype {
} }
} }
//type Entity<Fields extends Record<string, Field<any, any>> = {}> = { name: string; fields: Fields };
export function entity< export function entity<
EntityName extends string, EntityName extends string,
Fields extends Record<string, Field<any, any, any>>, Fields extends Record<string, Field<any, any, any>>,
@@ -270,6 +269,10 @@ class EntityManagerPrototype<Entities extends Record<string, Entity>> extends En
) { ) {
super(Object.values(__entities), new DummyConnection(), relations, indices); super(Object.values(__entities), new DummyConnection(), relations, indices);
} }
withConnection(connection: Connection): EntityManager<Schema<Entities>> {
return new EntityManager(this.entities, connection, this.relations.all, this.indices);
}
} }
type Chained<R extends Record<string, (...args: any[]) => any>> = { type Chained<R extends Record<string, (...args: any[]) => any>> = {
@@ -326,6 +329,7 @@ export function em<Entities extends Record<string, Entity>>(
entities: e.__entities, entities: e.__entities,
relations, relations,
indices, indices,
proto: e,
toJSON: () => toJSON: () =>
e.toJSON() as unknown as Pick<ModuleConfigs["data"], "entities" | "relations" | "indices">, e.toJSON() as unknown as Pick<ModuleConfigs["data"], "entities" | "relations" | "indices">,
}; };
@@ -4,6 +4,7 @@ import { config } from "dotenv";
// @ts-ignore // @ts-ignore
import { assetsPath, assetsTmpPath } from "../../../../../__test__/helper"; import { assetsPath, assetsTmpPath } from "../../../../../__test__/helper";
import { adapterTestSuite } from "media/storage/adapters/adapter-test-suite"; import { adapterTestSuite } from "media/storage/adapters/adapter-test-suite";
import { bunTestRunner } from "adapter/bun/test";
const dotenvOutput = config({ path: `${import.meta.dir}/.env` }); const dotenvOutput = config({ path: `${import.meta.dir}/.env` });
const { const {
@@ -43,7 +44,7 @@ describe.skipIf(ALL_TESTS)("StorageCloudinaryAdapter", async () => {
}); });
}); });
await adapterTestSuite({ test, expect }, adapter, file, { await adapterTestSuite(bunTestRunner, adapter, file, {
// eventual consistency // eventual consistency
retries: 20, retries: 20,
retryTimeout: 1000, retryTimeout: 1000,
@@ -4,6 +4,7 @@ import { StorageS3Adapter } from "./StorageS3Adapter";
import { config } from "dotenv"; import { config } from "dotenv";
import { adapterTestSuite } from "media"; import { adapterTestSuite } from "media";
import { assetsPath } from "../../../../../__test__/helper"; import { assetsPath } from "../../../../../__test__/helper";
import { bunTestRunner } from "adapter/bun/test";
//import { enableFetchLogging } from "../../helper"; //import { enableFetchLogging } from "../../helper";
const dotenvOutput = config({ path: `${import.meta.dir}/.env` }); const dotenvOutput = config({ path: `${import.meta.dir}/.env` });
const { R2_ACCESS_KEY, R2_SECRET_ACCESS_KEY, R2_URL, AWS_ACCESS_KEY, AWS_SECRET_KEY, AWS_S3_URL } = const { R2_ACCESS_KEY, R2_SECRET_ACCESS_KEY, R2_URL, AWS_ACCESS_KEY, AWS_SECRET_KEY, AWS_S3_URL } =
@@ -45,6 +46,6 @@ describe.skipIf(ALL_TESTS)("StorageS3Adapter", async () => {
const file = Bun.file(`${assetsPath}/image.png`) as unknown as File; const file = Bun.file(`${assetsPath}/image.png`) as unknown as File;
describe.each(versions)("%s", async (_name, adapter) => { describe.each(versions)("%s", async (_name, adapter) => {
await adapterTestSuite({ test, expect }, adapter, file); await adapterTestSuite(bunTestRunner, adapter, file);
}); });
}); });
+1 -1
View File
@@ -19,7 +19,7 @@ export function getRandomizedFilename(file: File | string, length = 16): string
} }
let ext = getExtensionFromName(filename); let ext = getExtensionFromName(filename);
if (isFile(file) && file.type) { if (!ext && isFile(file) && file.type) {
const _ext = extension(file.type); const _ext = extension(file.type);
if (_ext.length > 0) ext = _ext; if (_ext.length > 0) ext = _ext;
} }
+2
View File
@@ -222,6 +222,8 @@ export class ModuleManager {
return this.__em.repo(__bknd, { return this.__em.repo(__bknd, {
// to prevent exceptions when table doesn't exist // to prevent exceptions when table doesn't exist
silent: true, silent: true,
// disable counts for performance and compatibility
includeCounts: false,
}); });
} }
+4 -4
View File
@@ -66,15 +66,15 @@ const Skeleton = ({ theme }: { theme?: any }) => {
<Logo theme={actualTheme} /> <Logo theme={actualTheme} />
</div> </div>
<nav className="hidden md:flex flex-row gap-2.5 pl-0 p-2.5 items-center"> <nav className="hidden md:flex flex-row gap-2.5 pl-0 p-2.5 items-center">
{[...new Array(5)].map((item, key) => ( {[...new Array(4)].map((item, key) => (
<AppShell.NavLink key={key} as="span" className="active h-full opacity-50"> <AppShell.NavLink key={key} as="span" className="active h-full opacity-50">
<div className="w-10 h-3" /> <div className="w-18 h-3" />
</AppShell.NavLink> </AppShell.NavLink>
))} ))}
</nav> </nav>
<nav className="flex md:hidden flex-row items-center"> <nav className="flex md:hidden flex-row gap-2.5 pl-0 p-2.5 items-center">
<AppShell.NavLink as="span" className="active h-full opacity-50"> <AppShell.NavLink as="span" className="active h-full opacity-50">
<div className="w-10 h-3" /> <div className="w-20 h-3" />
</AppShell.NavLink> </AppShell.NavLink>
</nav> </nav>
<div className="flex flex-grow" /> <div className="flex flex-grow" />
+2 -2
View File
@@ -5,7 +5,7 @@ import { Form } from "json-schema-form-react";
import { transform } from "lodash-es"; import { transform } from "lodash-es";
import type { ComponentPropsWithoutRef } from "react"; import type { ComponentPropsWithoutRef } from "react";
import { Button } from "ui/components/buttons/Button"; import { Button } from "ui/components/buttons/Button";
import { Group, Input, Label } from "ui/components/form/Formy/components"; import { Group, Input, Password, Label } from "ui/components/form/Formy/components";
import { SocialLink } from "./SocialLink"; import { SocialLink } from "./SocialLink";
import type { ValueError } from "@sinclair/typebox/value"; import type { ValueError } from "@sinclair/typebox/value";
import { type TSchema, Value } from "core/utils"; import { type TSchema, Value } from "core/utils";
@@ -99,7 +99,7 @@ export function AuthForm({
</Group> </Group>
<Group> <Group>
<Label htmlFor="password">Password</Label> <Label htmlFor="password">Password</Label>
<Input type="password" name="password" /> <Password name="password" />
</Group> </Group>
<Button <Button
+14 -2
View File
@@ -44,8 +44,7 @@ export function MediaInfoModal({
return ( return (
<div className="flex flex-col md:flex-row"> <div className="flex flex-col md:flex-row">
<div className="flex w-full md:w-[calc(100%-300px)] justify-center items-center bg-lightest min-w-0"> <div className="flex w-full md:w-[calc(100%-300px)] justify-center items-center bg-lightest min-w-0">
{/* @ts-ignore */} <FilePreview file={file} />
<Media.Preview file={file} className="max-h-[70dvh]" controls muted />
</div> </div>
<div className="w-full md:!w-[300px] flex flex-col"> <div className="w-full md:!w-[300px] flex flex-col">
<Item title="ID" value={data?.id} copyValue={origin + mediaUrl} first> <Item title="ID" value={data?.id} copyValue={origin + mediaUrl} first>
@@ -157,6 +156,19 @@ const Item = ({
); );
}; };
const FilePreview = ({ file }: { file: FileState }) => {
if (file.type.startsWith("image/") || file.type.startsWith("video/")) {
// @ts-ignore
return <Media.Preview file={file} className="max-h-[70dvh]" controls muted />;
}
return (
<div className="min-w-96 min-h-48 flex justify-center items-center h-full max-h-[70dvh]">
<span className="opacity-50 font-mono">No Preview Available</span>
</div>
);
};
MediaInfoModal.defaultTitle = undefined; MediaInfoModal.defaultTitle = undefined;
MediaInfoModal.modalProps = { MediaInfoModal.modalProps = {
withCloseButton: false, withCloseButton: false,
+2 -1
View File
@@ -1,4 +1,5 @@
{ {
"extends": "./tsconfig.json", "extends": "./tsconfig.json",
"include": ["./src/**/*.ts", "./src/**/*.tsx"] "include": ["./src/**/*.ts", "./src/**/*.tsx"],
"exclude": ["./node_modules", "./__test__"]
} }
+1 -1
View File
@@ -2,7 +2,7 @@ import { readFile } from "node:fs/promises";
import { serveStatic } from "@hono/node-server/serve-static"; import { serveStatic } from "@hono/node-server/serve-static";
import { showRoutes } from "hono/dev"; import { showRoutes } from "hono/dev";
import { App, registries } from "./src"; import { App, registries } from "./src";
import { StorageLocalAdapter } from "./src/adapter/node/storage/StorageLocalAdapter"; import { StorageLocalAdapter } from "./src/adapter/node";
import { EntityManager, LibsqlConnection } from "data"; import { EntityManager, LibsqlConnection } from "data";
import { __bknd } from "modules/ModuleManager"; import { __bknd } from "modules/ModuleManager";
-26
View File
@@ -4796,26 +4796,6 @@
"babel-plugin-istanbul/istanbul-lib-instrument/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], "babel-plugin-istanbul/istanbul-lib-instrument/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"babel-plugin-istanbul/test-exclude/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
"babel-plugin-istanbul/test-exclude/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
"bknd/vitest/@vitest/expect": ["@vitest/expect@3.0.9", "", { "dependencies": { "@vitest/spy": "3.0.9", "@vitest/utils": "3.0.9", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-5eCqRItYgIML7NNVgJj6TVCmdzE7ZVgJhruW0ziSQV4V7PvLkDL1bBkBdcTs/VuIz0IxPb5da1IDSqc1TR9eig=="],
"bknd/vitest/@vitest/mocker": ["@vitest/mocker@3.0.9", "", { "dependencies": { "@vitest/spy": "3.0.9", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^5.0.0 || ^6.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-ryERPIBOnvevAkTq+L1lD+DTFBRcjueL9lOUfXsLfwP92h4e+Heb+PjiqS3/OURWPtywfafK0kj++yDFjWUmrA=="],
"bknd/vitest/@vitest/pretty-format": ["@vitest/pretty-format@3.0.9", "", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-OW9F8t2J3AwFEwENg3yMyKWweF7oRJlMyHOMIhO5F3n0+cgQAJZBjNgrF8dLwFTEXl5jUqBLXd9QyyKv8zEcmA=="],
"bknd/vitest/@vitest/runner": ["@vitest/runner@3.0.9", "", { "dependencies": { "@vitest/utils": "3.0.9", "pathe": "^2.0.3" } }, "sha512-NX9oUXgF9HPfJSwl8tUZCMP1oGx2+Sf+ru6d05QjzQz4OwWg0psEzwY6VexP2tTHWdOkhKHUIZH+fS6nA7jfOw=="],
"bknd/vitest/@vitest/snapshot": ["@vitest/snapshot@3.0.9", "", { "dependencies": { "@vitest/pretty-format": "3.0.9", "magic-string": "^0.30.17", "pathe": "^2.0.3" } }, "sha512-AiLUiuZ0FuA+/8i19mTYd+re5jqjEc2jZbgJ2up0VY0Ddyyxg/uUtBDpIFAy4uzKaQxOW8gMgBdAJJ2ydhu39A=="],
"bknd/vitest/@vitest/spy": ["@vitest/spy@3.0.9", "", { "dependencies": { "tinyspy": "^3.0.2" } }, "sha512-/CcK2UDl0aQ2wtkp3YVWldrpLRNCfVcIOFGlVGKO4R5eajsH393Z1yiXLVQ7vWsj26JOEjeZI0x5sm5P4OGUNQ=="],
"bknd/vitest/@vitest/utils": ["@vitest/utils@3.0.9", "", { "dependencies": { "@vitest/pretty-format": "3.0.9", "loupe": "^3.1.3", "tinyrainbow": "^2.0.0" } }, "sha512-ilHM5fHhZ89MCp5aAaM9uhfl1c2JdxVxl3McqsdVyVNN6JffnEen8UMCdRTzOhGXNQGo5GNL9QugHrz727Wnng=="],
"bknd/vitest/vite-node": ["vite-node@3.0.9", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.4.0", "es-module-lexer": "^1.6.0", "pathe": "^2.0.3", "vite": "^5.0.0 || ^6.0.0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-w3Gdx7jDcuT9cNn9jExXgOyKmf5UOTb6WMHz8LGAm54eS1Elf5OuBhCxl6zJxGhEeIkgsE1WbHuoL0mj/UXqXg=="],
"bknd/wrangler/@cloudflare/kv-asset-handler": ["@cloudflare/kv-asset-handler@0.4.0", "", { "dependencies": { "mime": "^3.0.0" } }, "sha512-+tv3z+SPp+gqTIcImN9o0hqE9xyfQjI1XD9pL6NuKjua9B1y7mNYv0S9cP+QEbA4ppVgGZEmKOvHX5G5Ei1CVA=="], "bknd/wrangler/@cloudflare/kv-asset-handler": ["@cloudflare/kv-asset-handler@0.4.0", "", { "dependencies": { "mime": "^3.0.0" } }, "sha512-+tv3z+SPp+gqTIcImN9o0hqE9xyfQjI1XD9pL6NuKjua9B1y7mNYv0S9cP+QEbA4ppVgGZEmKOvHX5G5Ei1CVA=="],
"bknd/wrangler/esbuild": ["esbuild@0.24.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.24.2", "@esbuild/android-arm": "0.24.2", "@esbuild/android-arm64": "0.24.2", "@esbuild/android-x64": "0.24.2", "@esbuild/darwin-arm64": "0.24.2", "@esbuild/darwin-x64": "0.24.2", "@esbuild/freebsd-arm64": "0.24.2", "@esbuild/freebsd-x64": "0.24.2", "@esbuild/linux-arm": "0.24.2", "@esbuild/linux-arm64": "0.24.2", "@esbuild/linux-ia32": "0.24.2", "@esbuild/linux-loong64": "0.24.2", "@esbuild/linux-mips64el": "0.24.2", "@esbuild/linux-ppc64": "0.24.2", "@esbuild/linux-riscv64": "0.24.2", "@esbuild/linux-s390x": "0.24.2", "@esbuild/linux-x64": "0.24.2", "@esbuild/netbsd-arm64": "0.24.2", "@esbuild/netbsd-x64": "0.24.2", "@esbuild/openbsd-arm64": "0.24.2", "@esbuild/openbsd-x64": "0.24.2", "@esbuild/sunos-x64": "0.24.2", "@esbuild/win32-arm64": "0.24.2", "@esbuild/win32-ia32": "0.24.2", "@esbuild/win32-x64": "0.24.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA=="], "bknd/wrangler/esbuild": ["esbuild@0.24.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.24.2", "@esbuild/android-arm": "0.24.2", "@esbuild/android-arm64": "0.24.2", "@esbuild/android-x64": "0.24.2", "@esbuild/darwin-arm64": "0.24.2", "@esbuild/darwin-x64": "0.24.2", "@esbuild/freebsd-arm64": "0.24.2", "@esbuild/freebsd-x64": "0.24.2", "@esbuild/linux-arm": "0.24.2", "@esbuild/linux-arm64": "0.24.2", "@esbuild/linux-ia32": "0.24.2", "@esbuild/linux-loong64": "0.24.2", "@esbuild/linux-mips64el": "0.24.2", "@esbuild/linux-ppc64": "0.24.2", "@esbuild/linux-riscv64": "0.24.2", "@esbuild/linux-s390x": "0.24.2", "@esbuild/linux-x64": "0.24.2", "@esbuild/netbsd-arm64": "0.24.2", "@esbuild/netbsd-x64": "0.24.2", "@esbuild/openbsd-arm64": "0.24.2", "@esbuild/openbsd-x64": "0.24.2", "@esbuild/sunos-x64": "0.24.2", "@esbuild/win32-arm64": "0.24.2", "@esbuild/win32-ia32": "0.24.2", "@esbuild/win32-x64": "0.24.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA=="],
@@ -5314,12 +5294,6 @@
"@verdaccio/middleware/express/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], "@verdaccio/middleware/express/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
"@vitest/coverage-v8/vitest/@vitest/mocker/estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
"babel-plugin-istanbul/test-exclude/minimatch/brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="],
"bknd/vitest/@vitest/mocker/estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
"bknd/wrangler/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.24.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA=="], "bknd/wrangler/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.24.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA=="],
"bknd/wrangler/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.24.2", "", { "os": "android", "cpu": "arm" }, "sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q=="], "bknd/wrangler/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.24.2", "", { "os": "android", "cpu": "arm" }, "sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q=="],
+79 -19
View File
@@ -3,8 +3,7 @@ title: 'Using the CLI'
description: 'How to start a bknd instance using the CLI.' description: 'How to start a bknd instance using the CLI.'
--- ---
Instead of running **bknd** using a framework, you can also use the CLI to quickly spin up a The bknd package includes a command-line interface (CLI) that allows you to run a bknd instance and perform various tasks.
full functional instance. To see all available options, run:
``` ```
npx bknd npx bknd
@@ -15,18 +14,21 @@ Here is the output:
$ npx bknd $ npx bknd
Usage: bknd [options] [command] Usage: bknd [options] [command]
bknd cli bknd cli v0.10.3-rc.1
Options: Options:
-V, --version output the version number -V, --version output the version number
-h, --help display help for command -h, --help display help for command
Commands: Commands:
user <action> create and update user (auth) config [options] get default config
schema [options] get schema copy-assets [options] copy static assets
run [options] create [options] create a new project
config [options] get default config debug <subject> debug bknd
help [command] display help for command run [options] run an instance
schema [options] get schema
user <action> create and update user (auth)
help [command] display help for command
``` ```
## Starting an instance (`run`) ## Starting an instance (`run`)
@@ -38,29 +40,87 @@ Usage: bknd run [options]
Options: Options:
-p, --port <port> port to run on (default: 1337, env: PORT) -p, --port <port> port to run on (default: 1337, env: PORT)
-m, --memory use in-memory database
-c, --config <config> config file -c, --config <config> config file
--db-url <db> database url, can be any valid libsql url --db-url <db> database url, can be any valid libsql url
--db-token <db> database token --db-token <db> database token
--server <server> server type (choices: "node", "bun", default: "node") --server <server> server type (choices: "node", "bun", default: "bun")
--no-open don't open browser window on start
-h, --help display help for command -h, --help display help for command
``` ```
### In-memory database To order in which the connection is determined is as follows:
To start an instance with an ephemeral in-memory database, run the following: 1. `--db-url`
``` 2. `--config` or reading the filesystem looking for `bknd.config.[js|ts|mjs|cjs|json]`
npx bknd run 3. `--memory`
``` 4. Environment variables `DB_URL` and `DB_TOKEN` in `.env` or `.dev.vars`
Keep in mind that the database is not persisted and will be lost when the process is terminated. 5. Fallback to file-based database `data.db`
### File-based database ### File-based database
To start an instance with a file-based database, run the following: By default, a file-based database `data.db` is used when running without any arguments. You can specify a different file name or path using the `--db-url` option. The database file will be created in the current working directory if it does not exist.
``` ```
npx bknd run --db-url file:data.db npx bknd run --db-url file:data.db
``` ```
### Using configuration file (`bknd.config.*`)
You can create a configuration file on the working directory that automatically gets picked up: `bknd.config.[js|ts|mjs|cjs|json]`
Here is an example of a `bknd.config.ts` file:
```ts
import type { BkndConfig } from "bknd/adapter";
export default {
// you can either specify the connection directly
connection: {
url: "file:data.db",
},
// or use the `app` function which passes the environment variables
app: ({ env }) => ({
connection: {
url: env.DB_URL,
}
})
} satisfies BkndConfig;
```
The `app` function is useful if you need a cross-platform way to access the environment variables. For example, on Cloudflare Workers, you can only access environment variables inside a request handler. If you're exclusively using a node-like environment, it's safe to access the environment variables directly from `process.env`.
If you're using `npx bknd run`, make sure to create a file in a file format that `node` can load, otherwise you may run into an error that the file couldn't be found:
```sh
[INF] 2025-03-28 18:02:21 Using config from bknd.config.ts
[ERR] 2025-03-28 18:02:21 Failed to load config: Error [ERR_MODULE_NOT_FOUND]: Cannot find package 'bknd.config.ts' imported from [...]
at packageResolve (node:internal/modules/esm/resolve:857:9)
at [...] {
code: 'ERR_MODULE_NOT_FOUND'
}
```
If you still want to use a `.ts` extension, you can start the CLI e.g. using `node` (>=v22.6.0):
```sh
node --experimental-strip-types node_modules/.bin/bknd run
```
Or with `tsx`:
```sh
npx tsx node_modules/.bin/bknd run
```
### Turso/LibSQL database ### Turso/LibSQL database
To start an instance with a Turso/LibSQL database, run the following: To start an instance with a Turso/LibSQL database, run the following:
``` ```
npx bknd run --db-url libsql://your-db.turso.io --db-token <your-token> npx bknd run --db-url libsql://your-db.turso.io --db-token <your-token>
``` ```
The `--db-token` option is optional and only required if the database is protected. The `--db-token` option is optional and only required if the database is protected.
### In-memory database
To start an instance with an ephemeral in-memory database, run the following:
```
npx bknd run --memory
```
Keep in mind that the database is not persisted and will be lost when the process is terminated.
+65
View File
@@ -0,0 +1,65 @@
import type { AstroBkndConfig } from "bknd/adapter/astro";
import { registerLocalMediaAdapter } from "bknd/adapter/node";
import { boolean, em, entity, text } from "bknd/data";
import { secureRandomString } from "bknd/utils";
// since we're running in node, we can register the local media adapter
const local = registerLocalMediaAdapter();
// the em() function makes it easy to create an initial schema
const schema = em({
todos: entity("todos", {
title: text(),
done: boolean(),
}),
});
// register your schema to get automatic type completion
type Database = (typeof schema)["DB"];
declare module "bknd/core" {
interface DB extends Database {}
}
export default {
// we can use any libsql config, and if omitted, uses in-memory
app: (env) => ({
connection: {
url: env.DB_URL ?? "file:data.db",
},
}),
// an initial config is only applied if the database is empty
initialConfig: {
data: schema.toJSON(),
// we're enabling auth ...
auth: {
enabled: true,
jwt: {
issuer: "bknd-astro-example",
secret: secureRandomString(64),
},
},
// ... and media
media: {
enabled: true,
adapter: local({
path: "./public",
}),
},
},
options: {
// the seed option is only executed if the database was empty
seed: async (ctx) => {
// create some entries
await ctx.em.mutator("todos").insertMany([
{ title: "Learn bknd", done: true },
{ title: "Build something cool", done: false },
]);
// and create a user
await ctx.app.module.auth.createUser({
email: "test@bknd.io",
password: "12345678",
});
},
},
} as const satisfies AstroBkndConfig;
+5 -5
View File
@@ -11,13 +11,13 @@
}, },
"dependencies": { "dependencies": {
"@astrojs/check": "^0.9.4", "@astrojs/check": "^0.9.4",
"@astrojs/react": "^3.6.3", "@astrojs/react": "^4.2.2",
"@types/react": "^18.3.12", "@types/react": "^19.0.12",
"@types/react-dom": "^18.3.1", "@types/react-dom": "^19.0.4",
"astro": "^4.16.16", "astro": "^4.16.16",
"bknd": "file:../../app", "bknd": "file:../../app",
"react": "^18.3.1", "react": "^19.1.0",
"react-dom": "^18.3.1", "react-dom": "^19.1.0",
"typescript": "^5.7.2" "typescript": "^5.7.2"
} }
} }
+23
View File
@@ -0,0 +1,23 @@
import type { AstroGlobal } from "astro";
import { getApp as getBkndApp } from "bknd/adapter/astro";
import config from "../bknd.config";
export { config };
export async function getApp() {
return await getBkndApp(config);
}
export async function getApi(
astro: AstroGlobal,
opts?: { mode: "static" } | { mode?: "dynamic"; verify?: boolean },
) {
const app = await getApp();
if (opts?.mode !== "static" && opts?.verify) {
const api = app.getApi({ headers: astro.request.headers });
await api.verifyAuth();
return api;
}
return app.getApi();
}
@@ -2,9 +2,9 @@
import { Admin } from "bknd/ui"; import { Admin } from "bknd/ui";
import "bknd/dist/styles.css"; import "bknd/dist/styles.css";
import { getApi } from "bknd/adapter/astro"; import { getApi } from "../../bknd";
const api = await getApi(Astro, { mode: "dynamic" }); const api = await getApi(Astro, { verify: true });
const user = api.getUser(); const user = api.getUser();
export const prerender = false; export const prerender = false;
+2 -73
View File
@@ -1,77 +1,6 @@
import type { APIContext } from "astro"; import type { APIContext } from "astro";
import { App } from "bknd";
import { serve } from "bknd/adapter/astro"; import { serve } from "bknd/adapter/astro";
import { registerLocalMediaAdapter } from "bknd/adapter/node"; import { config } from "../../bknd";
import { boolean, em, entity, text } from "bknd/data";
import { secureRandomString } from "bknd/utils";
export const prerender = false; export const prerender = false;
export const ALL = serve<APIContext>(config);
// since we're running in node, we can register the local media adapter
registerLocalMediaAdapter();
// the em() function makes it easy to create an initial schema
const schema = em({
todos: entity("todos", {
title: text(),
done: boolean(),
}),
});
// register your schema to get automatic type completion
type Database = (typeof schema)["DB"];
declare module "bknd/core" {
interface DB extends Database {}
}
export const ALL = serve<APIContext>({
// we can use any libsql config, and if omitted, uses in-memory
connection: {
url: "file:data.db",
},
// an initial config is only applied if the database is empty
initialConfig: {
data: schema.toJSON(),
// we're enabling auth ...
auth: {
enabled: true,
jwt: {
issuer: "bknd-astro-example",
secret: secureRandomString(64),
},
},
// ... and media
media: {
enabled: true,
adapter: {
type: "local",
config: {
path: "./public",
},
},
},
},
options: {
// the seed option is only executed if the database was empty
seed: async (ctx) => {
await ctx.em.mutator("todos").insertMany([
{ title: "Learn bknd", done: true },
{ title: "Build something cool", done: false },
]);
},
},
// here we can hook into the app lifecycle events ...
beforeBuild: async (app) => {
app.emgr.onEvent(
App.Events.AppFirstBoot,
async () => {
// ... to create an initial user
await app.module.auth.createUser({
email: "test@bknd.io",
password: "12345678",
});
},
"sync",
);
},
});
+1 -1
View File
@@ -1,5 +1,5 @@
--- ---
import { getApi } from "bknd/adapter/astro"; import { getApi } from "../bknd";
import Card from "../components/Card.astro"; import Card from "../components/Card.astro";
import Layout from "../layouts/Layout.astro"; import Layout from "../layouts/Layout.astro";
+2 -2
View File
@@ -1,8 +1,8 @@
--- ---
import { getApi } from "bknd/adapter/astro"; import { getApi } from "../bknd";
import Card from "../components/Card.astro"; import Card from "../components/Card.astro";
import Layout from "../layouts/Layout.astro"; import Layout from "../layouts/Layout.astro";
const api = await getApi(Astro, { mode: "dynamic" }); const api = await getApi(Astro, { verify: true });
const { data } = await api.data.readMany("todos"); const { data } = await api.data.readMany("todos");
const user = api.getUser(); const user = api.getUser();
+6 -2
View File
@@ -1,3 +1,7 @@
{ {
"extends": "astro/tsconfigs/strict" "extends": "astro/tsconfigs/strict",
} "compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "react"
}
}
+2 -2
View File
@@ -1,6 +1,6 @@
import { serveLambda } from "bknd/adapter/aws"; import { serve } from "bknd/adapter/aws";
export const handler = serveLambda({ export const handler = serve({
// to get local assets, run `npx bknd copy-assets` // to get local assets, run `npx bknd copy-assets`
// this is automatically done in `deploy.sh` // this is automatically done in `deploy.sh`
assets: { assets: {
+19 -19
View File
@@ -1,21 +1,21 @@
{ {
"name": "aws-lambda", "name": "aws-lambda",
"version": "1.0.0", "version": "1.0.0",
"main": "index.mjs", "main": "index.mjs",
"scripts": { "scripts": {
"test": "esbuild index.mjs --bundle --format=cjs --platform=node --external:fs --outfile=dist/index.js && node test.js", "test": "esbuild index.mjs --bundle --format=cjs --platform=node --external:fs --outfile=dist/index.js && node test.js",
"deploy": "./deploy.sh", "deploy": "./deploy.sh",
"clean": "./clean.sh" "clean": "./clean.sh"
}, },
"keywords": [], "keywords": [],
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"description": "", "description": "",
"dependencies": { "dependencies": {
"bknd": "file:../../app/bknd-0.9.0-rc.1-11.tgz" "bknd": "file:../../app"
}, },
"devDependencies": { "devDependencies": {
"esbuild": "^0.25.0", "esbuild": "^0.25.0",
"dotenv": "^16.4.7" "dotenv": "^16.4.7"
} }
} }
+3 -3
View File
@@ -3,11 +3,11 @@ const handler = require("./dist/index.js").handler;
const event = { const event = {
httpMethod: "GET", httpMethod: "GET",
path: "/", //path: "/",
//path: "/api/system/config", path: "/api/system/config",
//path: "/assets/main-B6sEDlfs.js", //path: "/assets/main-B6sEDlfs.js",
headers: { headers: {
//"Content-Type": "application/json", "Content-Type": "application/json",
"User-Agent": "curl/7.64.1", "User-Agent": "curl/7.64.1",
Accept: "*/*", Accept: "*/*",
}, },
+32
View File
@@ -0,0 +1,32 @@
{
"lockfileVersion": 1,
"workspaces": {
"": {
"name": "bun",
"dependencies": {
"bknd": "file:../../app",
},
"devDependencies": {
"@types/bun": "latest",
},
"peerDependencies": {
"typescript": "^5.0.0",
},
},
},
"packages": {
"@types/bun": ["@types/bun@1.2.2", "", { "dependencies": { "bun-types": "1.2.2" } }, "sha512-tr74gdku+AEDN5ergNiBnplr7hpDp3V1h7fqI2GcR/rsUaM39jpSeKH0TFibRvU0KwniRx5POgaYnaXbk0hU+w=="],
"@types/node": ["@types/node@22.13.4", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-ywP2X0DYtX3y08eFVx5fNIw7/uIv8hYUKgXoK8oayJlLnKcRfEYCxWMVE1XagUdVtCJlZT1AU4LXEABW+L1Peg=="],
"@types/ws": ["@types/ws@8.5.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw=="],
"bknd": ["/app@file:../../app", { "devDependencies": { "@types/node": "^22.10.0" }, "bin": { "bknd": "dist/cli/index.js" } }],
"bun-types": ["bun-types@1.2.2", "", { "dependencies": { "@types/node": "*", "@types/ws": "~8.5.10" } }, "sha512-RCbMH5elr9gjgDGDhkTTugA21XtJAy/9jkKe/G3WR2q17VPGhcquf9Sir6uay9iW+7P/BV0CAHA1XlHXMAVKHg=="],
"typescript": ["typescript@5.7.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw=="],
"undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
}
}
+2 -3
View File
@@ -1,4 +1,3 @@
// @ts-ignore somehow causes types:build issues on app
import { type BunBkndConfig, serve } from "bknd/adapter/bun"; import { type BunBkndConfig, serve } from "bknd/adapter/bun";
// Actually, all it takes is the following line: // Actually, all it takes is the following line:
@@ -7,8 +6,8 @@ import { type BunBkndConfig, serve } from "bknd/adapter/bun";
// this is optional, if omitted, it uses an in-memory database // this is optional, if omitted, it uses an in-memory database
const config: BunBkndConfig = { const config: BunBkndConfig = {
connection: { connection: {
url: "file:data.db" url: "file:data.db",
} },
}; };
serve(config); serve(config);
-6
View File
@@ -1,6 +0,0 @@
import { createApp } from "bknd";
const app = createApp();
await app.build();
export default app;
+1 -1
View File
@@ -6,5 +6,5 @@ export default serve({
mode: "warm", mode: "warm",
onBuilt: async (app) => { onBuilt: async (app) => {
app.modules.server.get("/custom", (c) => c.json({ hello: "world" })); app.modules.server.get("/custom", (c) => c.json({ hello: "world" }));
} },
}); });
+74
View File
@@ -0,0 +1,74 @@
import type { NextjsBkndConfig } from "bknd/adapter/nextjs";
import { boolean, em, entity, text } from "bknd/data";
import { registerLocalMediaAdapter } from "bknd/adapter/node";
import { secureRandomString } from "bknd/utils";
// The local media adapter works well in development, and server based
// deployments. However, on vercel or any other serverless deployments,
// you shouldn't use a filesystem based media adapter.
//
// Additionally, if you run the bknd api on the "edge" runtime,
// this would not work as well.
//
// For production, it is recommended to uncomment the line below.
const local = registerLocalMediaAdapter();
const schema = em({
todos: entity("todos", {
title: text(),
done: boolean(),
}),
});
// register your schema to get automatic type completion
type Database = (typeof schema)["DB"];
declare module "bknd/core" {
interface DB extends Database {}
}
export default {
app: (env) => ({
connection: {
url: env.DB_URL ?? "file:data.db",
},
}),
// an initial config is only applied if the database is empty
initialConfig: {
data: schema.toJSON(),
// we're enabling auth ...
auth: {
enabled: true,
jwt: {
issuer: "bknd-nextjs-example",
secret: secureRandomString(64),
},
cookie: {
pathSuccess: "/ssr",
pathLoggedOut: "/ssr",
},
},
// ... and media
media: {
enabled: true,
adapter: local({
path: "./public",
}),
},
},
options: {
// the seed option is only executed if the database was empty
seed: async (ctx) => {
// create some entries
await ctx.em.mutator("todos").insertMany([
{ title: "Learn bknd", done: true },
{ title: "Build something cool", done: false },
]);
// and create a user
await ctx.app.module.auth.createUser({
email: "test@bknd.io",
password: "12345678",
});
},
},
} as const satisfies NextjsBkndConfig;
+4 -83
View File
@@ -1,90 +1,11 @@
import { type NextjsBkndConfig, getApp as getBkndApp } from "bknd/adapter/nextjs"; import { getApp as getBkndApp } from "bknd/adapter/nextjs";
import { App } from "bknd";
import { boolean, em, entity, text } from "bknd/data";
import { registerLocalMediaAdapter } from "bknd/adapter/node";
import { secureRandomString } from "bknd/utils";
import { headers } from "next/headers"; import { headers } from "next/headers";
import config from "../bknd.config";
// The local media adapter works well in development, and server based export { config };
// deployments. However, on vercel or any other serverless deployments,
// you shouldn't use a filesystem based media adapter.
//
// Additionally, if you run the bknd api on the "edge" runtime,
// this would not work as well.
//
// For production, it is recommended to uncomment the line below.
registerLocalMediaAdapter();
const schema = em({
todos: entity("todos", {
title: text(),
done: boolean(),
}),
});
// register your schema to get automatic type completion
type Database = (typeof schema)["DB"];
declare module "bknd/core" {
interface DB extends Database {}
}
export const config = {
connection: {
url: "file:data.db",
},
// an initial config is only applied if the database is empty
initialConfig: {
data: schema.toJSON(),
// we're enabling auth ...
auth: {
enabled: true,
jwt: {
issuer: "bknd-nextjs-example",
secret: secureRandomString(64),
},
cookie: {
pathSuccess: "/ssr",
pathLoggedOut: "/ssr",
},
},
// ... and media
media: {
enabled: true,
adapter: {
type: "local",
config: {
path: "./public",
},
},
},
},
options: {
// the seed option is only executed if the database was empty
seed: async (ctx) => {
await ctx.em.mutator("todos").insertMany([
{ title: "Learn bknd", done: true },
{ title: "Build something cool", done: false },
]);
},
},
// here we can hook into the app lifecycle events ...
beforeBuild: async (app) => {
app.emgr.onEvent(
App.Events.AppFirstBoot,
async () => {
// ... to create an initial user
await app.module.auth.createUser({
email: "test@bknd.io",
password: "12345678",
});
},
"sync",
);
},
} as const satisfies NextjsBkndConfig;
export async function getApp() { export async function getApp() {
return await getBkndApp(config); return await getBkndApp(config, process.env);
} }
export async function getApi(opts?: { verify?: boolean }) { export async function getApi(opts?: { verify?: boolean }) {
+4 -75
View File
@@ -1,79 +1,8 @@
import { App } from "bknd"; import { getApp as getBkndApp } from "bknd/adapter/react-router";
import { registerLocalMediaAdapter } from "bknd/adapter/node"; import config from "../bknd.config";
import { type ReactRouterBkndConfig, getApp as getBkndApp } from "bknd/adapter/react-router";
import { boolean, em, entity, text } from "bknd/data";
import { secureRandomString } from "bknd/utils";
// since we're running in node, we can register the local media adapter export async function getApp() {
registerLocalMediaAdapter(); return await getBkndApp(config, process.env as any);
const schema = em({
todos: entity("todos", {
title: text(),
done: boolean(),
}),
});
// register your schema to get automatic type completion
type Database = (typeof schema)["DB"];
declare module "bknd/core" {
interface DB extends Database {}
}
const config = {
// we can use any libsql config, and if omitted, uses in-memory
connection: {
url: "file:test.db",
},
// an initial config is only applied if the database is empty
initialConfig: {
data: schema.toJSON(),
// we're enabling auth ...
auth: {
enabled: true,
jwt: {
issuer: "bknd-remix-example",
secret: secureRandomString(64),
},
},
// ... and media
media: {
enabled: true,
adapter: {
type: "local",
config: {
path: "./public",
},
},
},
},
options: {
// the seed option is only executed if the database was empty
seed: async (ctx) => {
await ctx.em.mutator("todos").insertMany([
{ title: "Learn bknd", done: true },
{ title: "Build something cool", done: false },
]);
},
},
// here we can hook into the app lifecycle events ...
beforeBuild: async (app) => {
app.emgr.onEvent(
App.Events.AppFirstBoot,
async () => {
// ... to create an initial user
await app.module.auth.createUser({
email: "test@bknd.io",
password: "12345678",
});
},
"sync",
);
},
} as const satisfies ReactRouterBkndConfig;
export async function getApp(args?: { request: Request }) {
return await getBkndApp(config, args);
} }
export async function getApi(args?: { request: Request }, opts?: { verify?: boolean }) { export async function getApi(args?: { request: Request }, opts?: { verify?: boolean }) {
+1 -1
View File
@@ -1,7 +1,7 @@
import { getApp } from "~/bknd"; import { getApp } from "~/bknd";
const handler = async (args: { request: Request }) => { const handler = async (args: { request: Request }) => {
const app = await getApp(args); const app = await getApp();
return app.fetch(args.request); return app.fetch(args.request);
}; };
+64
View File
@@ -0,0 +1,64 @@
import { registerLocalMediaAdapter } from "bknd/adapter/node";
import type { ReactRouterBkndConfig } from "bknd/adapter/react-router";
import { boolean, em, entity, text } from "bknd/data";
import { secureRandomString } from "bknd/utils";
// since we're running in node, we can register the local media adapter
const local = registerLocalMediaAdapter();
const schema = em({
todos: entity("todos", {
title: text(),
done: boolean(),
}),
});
// register your schema to get automatic type completion
type Database = (typeof schema)["DB"];
declare module "bknd/core" {
interface DB extends Database {}
}
export default {
// we can use any libsql config, and if omitted, uses in-memory
app: (env) => ({
connection: {
url: env?.DB_URL ?? "file:data.db",
},
}),
// an initial config is only applied if the database is empty
initialConfig: {
data: schema.toJSON(),
// we're enabling auth ...
auth: {
enabled: true,
jwt: {
issuer: "bknd-remix-example",
secret: secureRandomString(64),
},
},
// ... and media
media: {
enabled: true,
adapter: local({
path: "./public",
}),
},
},
options: {
// the seed option is only executed if the database was empty
seed: async (ctx) => {
// create some entries
await ctx.em.mutator("todos").insertMany([
{ title: "Learn bknd", done: true },
{ title: "Build something cool", done: false },
]);
// and create a user
await ctx.app.module.auth.createUser({
email: "test@bknd.io",
password: "12345678",
});
},
},
} as const satisfies ReactRouterBkndConfig<{ DB_URL?: string }>;