refactored adapters to run test suites (#126)

* refactored adapters to run test suites

* fix bun version for tests

* added missing adapter tests and refactored examples to use `bknd.config.ts` where applicable
This commit is contained in:
dswbx
2025-04-01 11:43:11 +02:00
committed by GitHub
parent 36e4224b33
commit 3f26c45dd9
55 changed files with 1130 additions and 647 deletions

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

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",
});
});

View File

@@ -19,6 +19,7 @@
"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",

View File

@@ -68,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(

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("*");
});
}
}

View File

@@ -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 }),
});
});

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);
}; };
} }

View File

@@ -1,8 +1,11 @@
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;
export type AwsLambdaBkndConfig<Env extends AwsLambdaEnv = AwsLambdaEnv> =
RuntimeBkndConfig<Env> & {
assets?: assets?:
| { | {
mode: "local"; mode: "local";
@@ -12,15 +15,13 @@ export type AwsLambdaBkndConfig = RuntimeBkndConfig & {
mode: "url"; mode: "url";
url: string; 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 = {}) {
if (!app) {
let additional: Partial<RuntimeBkndConfig> = { let additional: Partial<RuntimeBkndConfig> = {
adminOptions, adminOptions,
}; };
@@ -31,7 +32,7 @@ export async function createApp({
// @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");
@@ -50,19 +51,26 @@ export async function createApp({
} }
} }
app = await createRuntimeApp({ return await createRuntimeApp(
{
...config, ...config,
...additional, ...additional,
}); },
} args ?? process.env,
opts,
return app; );
} }
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;

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);
},
});
});

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,
});
});

View File

@@ -1,32 +1,46 @@
/// <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");
if (!app) {
registerLocalMediaAdapter(); registerLocalMediaAdapter();
app = await createRuntimeApp({
return 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>(
config: BunBkndConfig<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 = BunEnv>(
{
distPath, distPath,
connection, connection,
initialConfig, initialConfig,
@@ -36,12 +50,15 @@ export function serve({
buildConfig, buildConfig,
adminOptions, adminOptions,
...serveOptions ...serveOptions
}: BunBkndConfig = {}) { }: 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}`);

View File

@@ -0,0 +1,7 @@
import { expect, test, mock } from "bun:test";
export const bunTestRunner = {
expect,
test,
mock,
};

View File

@@ -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);
};
},
});
});

View File

@@ -1,19 +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";
import type { CreateAppConfig } from "App";
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;
@@ -27,58 +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;
}; };
export const constants = { export function serve<Env extends CloudflareEnv = CloudflareEnv>(
exec_async_event_id: "cf_register_waituntil", config: CloudflareBkndConfig<Env> = {},
cache_endpoint: "/__bknd/cache", ) {
do_endpoint: "/__bknd/do",
};
let media_registered: boolean = false;
export function makeCfConfig(config: CloudflareBkndConfig, context: Context): CreateAppConfig {
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,
options: {
...appConfig.options,
// if not specified explicitly, disable it to use ExecutionContext's waitUntil
asyncEventsMode: config.options?.asyncEventsMode ?? "none",
},
};
}
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);
@@ -113,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) {

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,
},
);
}

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, constants, 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,9 +20,10 @@ 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) => {
registerAsyncsExecutionContext(app, ctx);
app.module.server.client.get(constants.cache_endpoint, async (c) => { 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" });
@@ -26,16 +31,6 @@ export async function getCached(config: CloudflareBkndConfig, { env, ctx, ...arg
await config.onBuilt?.(app); await config.onBuilt?.(app);
}, },
beforeBuild: async (app) => { beforeBuild: async (app) => {
app.emgr.onEvent(
App.Events.AppBeforeResponse,
async (event) => {
ctx.waitUntil(event.params.app.emgr.executeAsyncs());
},
{
mode: "sync",
id: constants.exec_async_event_id,
},
);
app.emgr.onEvent( app.emgr.onEvent(
App.Events.AppConfigUpdatedEvent, App.Events.AppConfigUpdatedEvent,
async ({ params: { app } }) => { async ({ params: { app } }) => {

View File

@@ -1,9 +1,13 @@
import { DurableObject } from "cloudflare:workers"; import { DurableObject } from "cloudflare:workers";
import { App, type CreateAppConfig } from "bknd"; import type { App, CreateAppConfig } from "bknd";
import { createRuntimeApp, makeConfig } from "bknd/adapter"; import { createRuntimeApp, makeConfig } from "bknd/adapter";
import { type CloudflareBkndConfig, type Context, constants } 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,16 +71,7 @@ export class DurableBkndApp extends DurableObject {
this.app = await createRuntimeApp({ this.app = await createRuntimeApp({
...config, ...config,
onBuilt: async (app) => { onBuilt: async (app) => {
app.emgr.onEvent( registerAsyncsExecutionContext(app, this.ctx);
App.Events.AppBeforeResponse,
async (event) => {
this.ctx.waitUntil(event.params.app.emgr.executeAsyncs());
},
{
mode: "sync",
id: constants.exec_async_event_id,
},
);
app.modules.server.get(constants.do_endpoint, async (c) => { 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;

View File

@@ -1,40 +1,48 @@
import { 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, constants } 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,
onBuilt: async (app) => {
app.emgr.onEvent(
App.Events.AppBeforeResponse,
async (event) => {
ctx.ctx.waitUntil(event.params.app.emgr.executeAsyncs());
}, },
{ args,
mode: "sync", opts,
id: constants.exec_async_event_id,
},
);
await config.onBuilt?.(app);
},
},
ctx,
); );
} }
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,
});
} }

View File

@@ -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" });

View File

@@ -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(

View File

@@ -12,34 +12,67 @@ 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;
}
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) { if (config.onBuilt) {
app.emgr.onEvent( app.emgr.onEvent(
App.Events.AppBuiltEvent, App.Events.AppBuiltEvent,
@@ -52,16 +85,19 @@ export async function createFrameworkApp<Args = any>(
await config.beforeBuild?.(app); await config.beforeBuild?.(app);
await app.build(config.buildConfig); await app.build(config.buildConfig);
}
return app; return app;
} }
export async function createRuntimeApp<Env = any>( export async function createRuntimeApp<Args = DefaultArgs>(
{ serveStatic, adminOptions, ...config }: RuntimeBkndConfig, { serveStatic, adminOptions, ...config }: RuntimeBkndConfig<Args> = {},
env?: Env, args?: Args,
opts?: RuntimeOptions,
): Promise<App> { ): Promise<App> {
const app = App.create(makeConfig(config, env)); const app = await createAdapterApp(config, args, opts);
if (!app.isBuilt()) {
app.emgr.onEvent( app.emgr.onEvent(
App.Events.AppBuiltEvent, App.Events.AppBuiltEvent,
async () => { async () => {
@@ -82,6 +118,7 @@ export async function createRuntimeApp<Env = any>(
await config.beforeBuild?.(app); await config.beforeBuild?.(app);
await app.build(config.buildConfig); await app.build(config.buildConfig);
}
return app; return app;
} }

View File

@@ -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,
});
});

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);
}; };

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() {
if (!registered) {
registries.media.register("local", StorageLocalAdapter); registries.media.register("local", StorageLocalAdapter);
registered = true;
}
return (config: Partial<LocalAdapterConfig> = {}) => {
const adapter = new StorageLocalAdapter(config);
return adapter.toJSON(true);
};
} }

View File

@@ -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,
});
});

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,
});
});

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}`);

View File

@@ -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);
}); });

View 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);
} }

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>, {

View File

@@ -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 }),
});
});

View File

@@ -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);
}; };
} }

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) => { get: (_, prop) => {
if (prop === "original") { switch (prop) {
case "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) { if (prop in __consoles && requested <= current) {
return (...args: any[]) => __tty(prop, args); 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) {

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,

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,13 +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) { export function tryit(fn: () => void, fallback?: any) {

View File

@@ -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,

View File

@@ -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);
}); });
}); });

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;

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();
}

View File

@@ -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;

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",
);
},
});

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";

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();

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: {

View File

@@ -12,7 +12,7 @@
"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",

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
examples/bun/bun.lock Normal file
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=="],
}
}

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);

View File

@@ -1,6 +0,0 @@
import { createApp } from "bknd";
const app = createApp();
await app.build();
export default app;

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" }));
} },
}); });

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;

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 }) {

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 }) {

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);
}; };

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 }>;