mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-15 20:17:22 +00:00
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:
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
@@ -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
|
||||||
|
|||||||
62
app/__test__/adapter/adapter.test.ts
Normal file
62
app/__test__/adapter/adapter.test.ts
Normal 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",
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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",
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
90
app/src/adapter/adapter-test-suite.ts
Normal file
90
app/src/adapter/adapter-test-suite.ts
Normal 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("*");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
15
app/src/adapter/astro/astro.adapter.spec.ts
Normal file
15
app/src/adapter/astro/astro.adapter.spec.ts
Normal 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 }),
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
app/src/adapter/aws/aws.adapter.spec.ts
Normal file
19
app/src/adapter/aws/aws.adapter.spec.ts
Normal 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
app/src/adapter/bun/bun.adapter.spec.ts
Normal file
15
app/src/adapter/bun/bun.adapter.spec.ts
Normal 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,
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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
app/src/adapter/bun/test.ts
Normal file
7
app/src/adapter/bun/test.ts
Normal 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,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) {
|
||||||
|
|||||||
64
app/src/adapter/cloudflare/config.ts
Normal file
64
app/src/adapter/cloudflare/config.ts
Normal 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,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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 } }) => {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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());
|
|
||||||
},
|
|
||||||
{
|
|
||||||
mode: "sync",
|
|
||||||
id: constants.exec_async_event_id,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
await config.onBuilt?.(app);
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
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(
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
16
app/src/adapter/nextjs/nextjs.adapter.spec.ts
Normal file
16
app/src/adapter/nextjs/nextjs.adapter.spec.ts
Normal 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,
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
15
app/src/adapter/node/node.adapter.native-spec.ts
Normal file
15
app/src/adapter/node/node.adapter.native-spec.ts
Normal 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,
|
||||||
|
});
|
||||||
|
});
|
||||||
15
app/src/adapter/node/node.adapter.spec.ts
Normal file
15
app/src/adapter/node/node.adapter.spec.ts
Normal 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,
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>, {
|
||||||
|
|||||||
15
app/src/adapter/react-router/react-router.adapter.spec.ts
Normal file
15
app/src/adapter/react-router/react-router.adapter.spec.ts
Normal 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 }),
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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);
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
65
examples/astro/bknd.config.ts
Normal file
65
examples/astro/bknd.config.ts
Normal 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;
|
||||||
23
examples/astro/src/bknd.ts
Normal file
23
examples/astro/src/bknd.ts
Normal 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;
|
||||||
|
|||||||
@@ -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,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";
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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,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
32
examples/bun/bun.lock
Normal 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=="],
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
import { createApp } from "bknd";
|
|
||||||
|
|
||||||
const app = createApp();
|
|
||||||
await app.build();
|
|
||||||
|
|
||||||
export default app;
|
|
||||||
@@ -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
examples/nextjs/bknd.config.ts
Normal file
74
examples/nextjs/bknd.config.ts
Normal 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;
|
||||||
@@ -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 }) {
|
||||||
|
|||||||
@@ -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,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
examples/react-router/bknd.config.ts
Normal file
64
examples/react-router/bknd.config.ts
Normal 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 }>;
|
||||||
Reference in New Issue
Block a user