mirror of
https://github.com/bknd-io/bknd/
synced 2026-08-02 08:06:00 +00:00
Merge remote-tracking branch 'origin/release/0.11' into feat/init-e2e
# Conflicts: # app/.gitignore # app/package.json # app/vite.dev.ts # bun.lock
This commit is contained in:
+2
-1
@@ -1,2 +1,3 @@
|
||||
playwright-report
|
||||
test-results
|
||||
test-results
|
||||
bknd.config.*
|
||||
@@ -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",
|
||||
});
|
||||
});
|
||||
@@ -5,7 +5,8 @@ import { DataApi } from "../../src/data/api/DataApi";
|
||||
import { DataController } from "../../src/data/api/DataController";
|
||||
import { dataConfigSchema } from "../../src/data/data-schema";
|
||||
import * as proto from "../../src/data/prototype";
|
||||
import { disableConsoleLog, enableConsoleLog, schemaToEm } from "../helper";
|
||||
import { schemaToEm } from "../helper";
|
||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||
|
||||
beforeAll(disableConsoleLog);
|
||||
afterAll(enableConsoleLog);
|
||||
@@ -64,6 +65,15 @@ describe("DataApi", () => {
|
||||
const res = await req;
|
||||
expect(res.data).toEqual(payload as any);
|
||||
}
|
||||
|
||||
{
|
||||
// make sure sort is working
|
||||
const req = await api.readMany("posts", {
|
||||
select: ["title"],
|
||||
sort: "-id",
|
||||
});
|
||||
expect(req.data).toEqual(payload.reverse() as any);
|
||||
}
|
||||
});
|
||||
|
||||
it("updates many", async () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, mock, test } from "bun:test";
|
||||
import type { ModuleBuildContext } from "../../src";
|
||||
import { type App, createApp } from "../../src/App";
|
||||
import { App, createApp } from "../../src/App";
|
||||
import * as proto from "../../src/data/prototype";
|
||||
|
||||
describe("App", () => {
|
||||
@@ -51,4 +51,87 @@ describe("App", () => {
|
||||
expect(todos[0]?.title).toBe("ctx");
|
||||
expect(todos[1]?.title).toBe("api");
|
||||
});
|
||||
|
||||
test("lifecycle events are triggered", async () => {
|
||||
const firstBoot = mock(() => null);
|
||||
const configUpdate = mock(() => null);
|
||||
const appBuilt = mock(() => null);
|
||||
const appRequest = mock(() => null);
|
||||
const beforeResponse = mock(() => null);
|
||||
|
||||
const app = createApp();
|
||||
|
||||
app.emgr.onEvent(
|
||||
App.Events.AppFirstBoot,
|
||||
(event) => {
|
||||
expect(event).toBeInstanceOf(App.Events.AppFirstBoot);
|
||||
expect(event.params.app.version()).toBe(app.version());
|
||||
firstBoot();
|
||||
},
|
||||
"sync",
|
||||
);
|
||||
app.emgr.onEvent(
|
||||
App.Events.AppBuiltEvent,
|
||||
(event) => {
|
||||
expect(event).toBeInstanceOf(App.Events.AppBuiltEvent);
|
||||
expect(event.params.app.version()).toBe(app.version());
|
||||
appBuilt();
|
||||
},
|
||||
"sync",
|
||||
);
|
||||
app.emgr.onEvent(
|
||||
App.Events.AppConfigUpdatedEvent,
|
||||
() => {
|
||||
configUpdate();
|
||||
},
|
||||
"sync",
|
||||
);
|
||||
app.emgr.onEvent(
|
||||
App.Events.AppRequest,
|
||||
(event) => {
|
||||
expect(event).toBeInstanceOf(App.Events.AppRequest);
|
||||
expect(event.params.app.version()).toBe(app.version());
|
||||
expect(event.params.request).toBeInstanceOf(Request);
|
||||
appRequest();
|
||||
},
|
||||
"sync",
|
||||
);
|
||||
app.emgr.onEvent(
|
||||
App.Events.AppBeforeResponse,
|
||||
(event) => {
|
||||
expect(event).toBeInstanceOf(App.Events.AppBeforeResponse);
|
||||
expect(event.params.app.version()).toBe(app.version());
|
||||
expect(event.params.response).toBeInstanceOf(Response);
|
||||
beforeResponse();
|
||||
},
|
||||
"sync",
|
||||
);
|
||||
|
||||
await app.build();
|
||||
expect(firstBoot).toHaveBeenCalled();
|
||||
expect(appBuilt).toHaveBeenCalled();
|
||||
//expect(configUpdate).toHaveBeenCalled();
|
||||
expect(appRequest).not.toHaveBeenCalled();
|
||||
expect(beforeResponse).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("emgr exec modes", async () => {
|
||||
const called = mock(() => null);
|
||||
const app = createApp({
|
||||
options: {
|
||||
asyncEventsMode: "sync",
|
||||
},
|
||||
});
|
||||
|
||||
// register async listener
|
||||
app.emgr.onEvent(App.Events.AppFirstBoot, async () => {
|
||||
called();
|
||||
});
|
||||
|
||||
await app.build();
|
||||
await app.server.request(new Request("http://localhost"));
|
||||
|
||||
// expect async listeners to be executed sync after request
|
||||
expect(called).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -70,6 +70,9 @@ describe("EventManager", async () => {
|
||||
new SpecialEvent({ foo: "bar" });
|
||||
new InformationalEvent();
|
||||
|
||||
// execute asyncs
|
||||
await emgr.executeAsyncs();
|
||||
|
||||
expect(call).toHaveBeenCalledTimes(2);
|
||||
expect(delayed).toHaveBeenCalled();
|
||||
});
|
||||
@@ -80,15 +83,11 @@ describe("EventManager", async () => {
|
||||
call();
|
||||
return Promise.all(p);
|
||||
};
|
||||
const emgr = new EventManager(
|
||||
{ InformationalEvent },
|
||||
{
|
||||
asyncExecutor,
|
||||
},
|
||||
);
|
||||
const emgr = new EventManager({ InformationalEvent });
|
||||
|
||||
emgr.onEvent(InformationalEvent, async () => {});
|
||||
await emgr.emit(new InformationalEvent());
|
||||
await emgr.executeAsyncs(asyncExecutor);
|
||||
expect(call).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -125,6 +124,9 @@ describe("EventManager", async () => {
|
||||
const e2 = await emgr.emit(new ReturnEvent({ foo: "bar" }));
|
||||
expect(e2.returned).toBe(true);
|
||||
expect(e2.params.foo).toBe("bar-1-0");
|
||||
|
||||
await emgr.executeAsyncs();
|
||||
|
||||
expect(onInvalidReturn).toHaveBeenCalled();
|
||||
expect(asyncEventCallback).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -288,14 +288,17 @@ describe("[data] Mutator (Events)", async () => {
|
||||
|
||||
test("events were fired", async () => {
|
||||
const { data } = await mutator.insertOne({ label: "test" });
|
||||
await mutator.emgr.executeAsyncs();
|
||||
expect(events.has(MutatorEvents.MutatorInsertBefore.slug)).toBeTrue();
|
||||
expect(events.has(MutatorEvents.MutatorInsertAfter.slug)).toBeTrue();
|
||||
|
||||
await mutator.updateOne(data.id, { label: "test2" });
|
||||
await mutator.emgr.executeAsyncs();
|
||||
expect(events.has(MutatorEvents.MutatorUpdateBefore.slug)).toBeTrue();
|
||||
expect(events.has(MutatorEvents.MutatorUpdateAfter.slug)).toBeTrue();
|
||||
|
||||
await mutator.deleteOne(data.id);
|
||||
await mutator.emgr.executeAsyncs();
|
||||
expect(events.has(MutatorEvents.MutatorDeleteBefore.slug)).toBeTrue();
|
||||
expect(events.has(MutatorEvents.MutatorDeleteAfter.slug)).toBeTrue();
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { afterAll, describe, expect, test } from "bun:test";
|
||||
import type { Kysely, Transaction } from "kysely";
|
||||
import { Perf } from "../../../src/core/utils";
|
||||
import { Perf } from "core/utils";
|
||||
import {
|
||||
Entity,
|
||||
EntityManager,
|
||||
@@ -8,7 +8,10 @@ import {
|
||||
ManyToOneRelation,
|
||||
RepositoryEvents,
|
||||
TextField,
|
||||
} from "../../../src/data";
|
||||
entity as $entity,
|
||||
text as $text,
|
||||
em as $em,
|
||||
} from "data";
|
||||
import { getDummyConnection } from "../helper";
|
||||
|
||||
type E = Kysely<any> | Transaction<any>;
|
||||
@@ -177,6 +180,47 @@ describe("[Repository]", async () => {
|
||||
const res5 = await em.repository(items).exists({});
|
||||
expect(res5.exists).toBe(true);
|
||||
});
|
||||
|
||||
test("option: silent", async () => {
|
||||
const em = $em({
|
||||
items: $entity("items", {
|
||||
label: $text(),
|
||||
}),
|
||||
}).proto.withConnection(getDummyConnection().dummyConnection);
|
||||
|
||||
// should throw because table doesn't exist
|
||||
expect(em.repo("items").findMany({})).rejects.toThrow(/no such table/);
|
||||
// should silently return empty result
|
||||
expect(
|
||||
em
|
||||
.repo("items", { silent: true })
|
||||
.findMany({})
|
||||
.then((r) => r.data),
|
||||
).resolves.toEqual([]);
|
||||
});
|
||||
|
||||
test("option: includeCounts", async () => {
|
||||
const em = $em({
|
||||
items: $entity("items", {
|
||||
label: $text(),
|
||||
}),
|
||||
}).proto.withConnection(getDummyConnection().dummyConnection);
|
||||
await em.schema().sync({ force: true });
|
||||
|
||||
expect(
|
||||
em
|
||||
.repo("items")
|
||||
.findMany({})
|
||||
.then((r) => [r.meta.count, r.meta.total]),
|
||||
).resolves.toEqual([0, 0]);
|
||||
|
||||
expect(
|
||||
em
|
||||
.repo("items", { includeCounts: false })
|
||||
.findMany({})
|
||||
.then((r) => [r.meta.count, r.meta.total]),
|
||||
).resolves.toEqual([undefined, undefined]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("[data] Repository (Events)", async () => {
|
||||
@@ -198,22 +242,27 @@ describe("[data] Repository (Events)", async () => {
|
||||
});
|
||||
|
||||
test("events were fired", async () => {
|
||||
await em.repository(items).findId(1);
|
||||
const repo = em.repository(items);
|
||||
await repo.findId(1);
|
||||
await repo.emgr.executeAsyncs();
|
||||
expect(events.has(RepositoryEvents.RepositoryFindOneBefore.slug)).toBeTrue();
|
||||
expect(events.has(RepositoryEvents.RepositoryFindOneAfter.slug)).toBeTrue();
|
||||
events.clear();
|
||||
|
||||
await em.repository(items).findOne({ id: 1 });
|
||||
await repo.findOne({ id: 1 });
|
||||
await repo.emgr.executeAsyncs();
|
||||
expect(events.has(RepositoryEvents.RepositoryFindOneBefore.slug)).toBeTrue();
|
||||
expect(events.has(RepositoryEvents.RepositoryFindOneAfter.slug)).toBeTrue();
|
||||
events.clear();
|
||||
|
||||
await em.repository(items).findMany({ where: { id: 1 } });
|
||||
await repo.findMany({ where: { id: 1 } });
|
||||
await repo.emgr.executeAsyncs();
|
||||
expect(events.has(RepositoryEvents.RepositoryFindManyBefore.slug)).toBeTrue();
|
||||
expect(events.has(RepositoryEvents.RepositoryFindManyAfter.slug)).toBeTrue();
|
||||
events.clear();
|
||||
|
||||
await em.repository(items).findManyByReference(1, "categories");
|
||||
await repo.findManyByReference(1, "categories");
|
||||
await repo.emgr.executeAsyncs();
|
||||
expect(events.has(RepositoryEvents.RepositoryFindManyBefore.slug)).toBeTrue();
|
||||
expect(events.has(RepositoryEvents.RepositoryFindManyAfter.slug)).toBeTrue();
|
||||
events.clear();
|
||||
|
||||
@@ -78,6 +78,7 @@ export const assetsTmpPath = `${import.meta.dir}/_assets/tmp`;
|
||||
export async function enableFetchLogging() {
|
||||
const originalFetch = global.fetch;
|
||||
|
||||
// @ts-ignore
|
||||
global.fetch = async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||
const response = await originalFetch(input, init);
|
||||
const url = input instanceof URL || typeof input === "string" ? input : input.url;
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { type FileBody, Storage, type StorageAdapter } from "../../src/media/storage/Storage";
|
||||
import { type FileBody, Storage } from "../../src/media/storage/Storage";
|
||||
import * as StorageEvents from "../../src/media/storage/events";
|
||||
import { StorageAdapter } from "media";
|
||||
|
||||
class TestAdapter implements StorageAdapter {
|
||||
class TestAdapter extends StorageAdapter {
|
||||
files: Record<string, FileBody> = {};
|
||||
|
||||
getName() {
|
||||
@@ -61,7 +62,7 @@ describe("Storage", async () => {
|
||||
test("uploads a file", async () => {
|
||||
const {
|
||||
meta: { type, size },
|
||||
} = await storage.uploadFile("hello", "world.txt");
|
||||
} = await storage.uploadFile("hello" as any, "world.txt");
|
||||
expect({ type, size }).toEqual({ type: "text/plain", size: 0 });
|
||||
});
|
||||
|
||||
@@ -71,6 +72,7 @@ describe("Storage", async () => {
|
||||
});
|
||||
|
||||
test("events were fired", async () => {
|
||||
await storage.emgr.executeAsyncs();
|
||||
expect(events.has(StorageEvents.FileUploadedEvent.slug)).toBeTrue();
|
||||
expect(events.has(StorageEvents.FileDeletedEvent.slug)).toBeTrue();
|
||||
// @todo: file access must be tested in controllers
|
||||
|
||||
@@ -96,5 +96,10 @@ describe("media/mime-types", () => {
|
||||
`getRandomizedFilename(): ${filename} should end with ${ext}`,
|
||||
).toBe(ext);
|
||||
}
|
||||
|
||||
// make sure it keeps the extension, even if the file has a different type
|
||||
const file = new File([""], "image.jpg", { type: "text/plain" });
|
||||
const [, ext] = getRandomizedFilename(file).split(".");
|
||||
expect(ext).toBe("jpg");
|
||||
});
|
||||
});
|
||||
|
||||
+22
-21
@@ -3,7 +3,7 @@
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
"bin": "./dist/cli/index.js",
|
||||
"version": "0.10.3-rc.1",
|
||||
"version": "0.11.0-rc.2",
|
||||
"description": "Lightweight Firebase/Supabase alternative built to run anywhere — incl. Next.js, React Router, Astro, Cloudflare, Bun, Node, AWS Lambda & more.",
|
||||
"homepage": "https://bknd.io",
|
||||
"repository": {
|
||||
@@ -15,10 +15,11 @@
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"test": "bun run test:unit && bun run test:e2e",
|
||||
"test": "ALL_TESTS=1 bun test --bail",
|
||||
"test:all": "bun run test && bun run test:node",
|
||||
"test:bun": "ALL_TESTS=1 bun test --bail",
|
||||
"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",
|
||||
"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",
|
||||
@@ -31,7 +32,7 @@
|
||||
"build:types": "tsc -p tsconfig.build.json --emitDeclarationOnly && tsc-alias",
|
||||
"updater": "bun x npm-check-updates -ui",
|
||||
"cli": "LOCAL=1 bun src/cli/index.ts",
|
||||
"prepublishOnly": "bun run types && bun run test && bun run build:all && cp ../README.md ./",
|
||||
"prepublishOnly": "bun run types && bun run test && bun run test:node && bun run build:all && cp ../README.md ./",
|
||||
"postpublish": "rm -f README.md",
|
||||
"test:unit": "vitest",
|
||||
"test:unit:coverage": "vitest run --coverage",
|
||||
@@ -47,7 +48,7 @@
|
||||
"@codemirror/lang-json": "^6.0.1",
|
||||
"@codemirror/lang-liquid": "^6.2.2",
|
||||
"@hello-pangea/dnd": "^18.0.1",
|
||||
"@libsql/client": "^0.14.0",
|
||||
"@libsql/client": "^0.15.2",
|
||||
"@mantine/core": "^7.17.1",
|
||||
"@mantine/hooks": "^7.17.1",
|
||||
"@sinclair/typebox": "^0.34.30",
|
||||
@@ -136,52 +137,52 @@
|
||||
".": {
|
||||
"types": "./dist/types/index.d.ts",
|
||||
"import": "./dist/index.js",
|
||||
"require": "./dist/index.cjs"
|
||||
"require": "./dist/index.js"
|
||||
},
|
||||
"./ui": {
|
||||
"types": "./dist/types/ui/index.d.ts",
|
||||
"import": "./dist/ui/index.js",
|
||||
"require": "./dist/ui/index.cjs"
|
||||
"require": "./dist/ui/index.js"
|
||||
},
|
||||
"./elements": {
|
||||
"types": "./dist/types/ui/elements/index.d.ts",
|
||||
"import": "./dist/ui/elements/index.js",
|
||||
"require": "./dist/ui/elements/index.cjs"
|
||||
"require": "./dist/ui/elements/index.js"
|
||||
},
|
||||
"./client": {
|
||||
"types": "./dist/types/ui/client/index.d.ts",
|
||||
"import": "./dist/ui/client/index.js",
|
||||
"require": "./dist/ui/client/index.cjs"
|
||||
"require": "./dist/ui/client/index.js"
|
||||
},
|
||||
"./data": {
|
||||
"types": "./dist/types/data/index.d.ts",
|
||||
"import": "./dist/data/index.js",
|
||||
"require": "./dist/data/index.cjs"
|
||||
"require": "./dist/data/index.js"
|
||||
},
|
||||
"./core": {
|
||||
"types": "./dist/types/core/index.d.ts",
|
||||
"import": "./dist/core/index.js",
|
||||
"require": "./dist/core/index.cjs"
|
||||
"require": "./dist/core/index.js"
|
||||
},
|
||||
"./utils": {
|
||||
"types": "./dist/types/core/utils/index.d.ts",
|
||||
"import": "./dist/core/utils/index.js",
|
||||
"require": "./dist/core/utils/index.cjs"
|
||||
"require": "./dist/core/utils/index.js"
|
||||
},
|
||||
"./cli": {
|
||||
"types": "./dist/types/cli/index.d.ts",
|
||||
"import": "./dist/cli/index.js",
|
||||
"require": "./dist/cli/index.cjs"
|
||||
"require": "./dist/cli/index.js"
|
||||
},
|
||||
"./media": {
|
||||
"types": "./dist/types/media/index.d.ts",
|
||||
"import": "./dist/media/index.js",
|
||||
"require": "./dist/media/index.cjs"
|
||||
"require": "./dist/media/index.js"
|
||||
},
|
||||
"./adapter/cloudflare": {
|
||||
"types": "./dist/types/adapter/cloudflare/index.d.ts",
|
||||
"import": "./dist/adapter/cloudflare/index.js",
|
||||
"require": "./dist/adapter/cloudflare/index.cjs"
|
||||
"require": "./dist/adapter/cloudflare/index.js"
|
||||
},
|
||||
"./adapter": {
|
||||
"types": "./dist/types/adapter/index.d.ts",
|
||||
@@ -190,37 +191,37 @@
|
||||
"./adapter/vite": {
|
||||
"types": "./dist/types/adapter/vite/index.d.ts",
|
||||
"import": "./dist/adapter/vite/index.js",
|
||||
"require": "./dist/adapter/vite/index.cjs"
|
||||
"require": "./dist/adapter/vite/index.js"
|
||||
},
|
||||
"./adapter/nextjs": {
|
||||
"types": "./dist/types/adapter/nextjs/index.d.ts",
|
||||
"import": "./dist/adapter/nextjs/index.js",
|
||||
"require": "./dist/adapter/nextjs/index.cjs"
|
||||
"require": "./dist/adapter/nextjs/index.js"
|
||||
},
|
||||
"./adapter/react-router": {
|
||||
"types": "./dist/types/adapter/react-router/index.d.ts",
|
||||
"import": "./dist/adapter/react-router/index.js",
|
||||
"require": "./dist/adapter/react-router/index.cjs"
|
||||
"require": "./dist/adapter/react-router/index.js"
|
||||
},
|
||||
"./adapter/bun": {
|
||||
"types": "./dist/types/adapter/bun/index.d.ts",
|
||||
"import": "./dist/adapter/bun/index.js",
|
||||
"require": "./dist/adapter/bun/index.cjs"
|
||||
"require": "./dist/adapter/bun/index.js"
|
||||
},
|
||||
"./adapter/node": {
|
||||
"types": "./dist/types/adapter/node/index.d.ts",
|
||||
"import": "./dist/adapter/node/index.js",
|
||||
"require": "./dist/adapter/node/index.cjs"
|
||||
"require": "./dist/adapter/node/index.js"
|
||||
},
|
||||
"./adapter/astro": {
|
||||
"types": "./dist/types/adapter/astro/index.d.ts",
|
||||
"import": "./dist/adapter/astro/index.js",
|
||||
"require": "./dist/adapter/astro/index.cjs"
|
||||
"require": "./dist/adapter/astro/index.js"
|
||||
},
|
||||
"./adapter/aws": {
|
||||
"types": "./dist/types/adapter/aws/index.d.ts",
|
||||
"import": "./dist/adapter/aws/index.js",
|
||||
"require": "./dist/adapter/aws/index.cjs"
|
||||
"require": "./dist/adapter/aws/index.js"
|
||||
},
|
||||
"./dist/main.css": "./dist/ui/main.css",
|
||||
"./dist/styles.css": "./dist/ui/styles.css",
|
||||
|
||||
+71
-33
@@ -4,9 +4,10 @@ import { Event } from "core/events";
|
||||
import { Connection, type LibSqlCredentials, LibsqlConnection } from "data";
|
||||
import type { Hono } from "hono";
|
||||
import {
|
||||
ModuleManager,
|
||||
type InitialModuleConfigs,
|
||||
type ModuleBuildContext,
|
||||
ModuleManager,
|
||||
type ModuleConfigs,
|
||||
type ModuleManagerOptions,
|
||||
type Modules,
|
||||
} from "modules/ModuleManager";
|
||||
@@ -16,6 +17,7 @@ import { SystemController } from "modules/server/SystemController";
|
||||
|
||||
// biome-ignore format: must be there
|
||||
import { Api, type ApiOptions } from "Api";
|
||||
import type { ServerEnv } from "modules/Controller";
|
||||
|
||||
export type AppPlugin = (app: App) => Promise<void> | void;
|
||||
|
||||
@@ -29,12 +31,25 @@ export class AppBuiltEvent extends AppEvent {
|
||||
export class AppFirstBoot extends AppEvent {
|
||||
static override slug = "app-first-boot";
|
||||
}
|
||||
export const AppEvents = { AppConfigUpdatedEvent, AppBuiltEvent, AppFirstBoot } as const;
|
||||
export class AppRequest extends AppEvent<{ request: Request }> {
|
||||
static override slug = "app-request";
|
||||
}
|
||||
export class AppBeforeResponse extends AppEvent<{ request: Request; response: Response }> {
|
||||
static override slug = "app-before-response";
|
||||
}
|
||||
export const AppEvents = {
|
||||
AppConfigUpdatedEvent,
|
||||
AppBuiltEvent,
|
||||
AppFirstBoot,
|
||||
AppRequest,
|
||||
AppBeforeResponse,
|
||||
} as const;
|
||||
|
||||
export type AppOptions = {
|
||||
plugins?: AppPlugin[];
|
||||
seed?: (ctx: ModuleBuildContext & { app: App }) => Promise<void>;
|
||||
manager?: Omit<ModuleManagerOptions, "initial" | "onUpdated" | "seed">;
|
||||
asyncEventsMode?: "sync" | "async" | "none";
|
||||
};
|
||||
export type CreateAppConfig = {
|
||||
connection?:
|
||||
@@ -53,12 +68,14 @@ export type AppConfig = InitialModuleConfigs;
|
||||
export type LocalApiOptions = Request | ApiOptions;
|
||||
|
||||
export class App {
|
||||
modules: ModuleManager;
|
||||
static readonly Events = AppEvents;
|
||||
|
||||
modules: ModuleManager;
|
||||
adminController?: AdminController;
|
||||
_id: string = crypto.randomUUID();
|
||||
|
||||
private trigger_first_boot = false;
|
||||
private plugins: AppPlugin[];
|
||||
private _id: string = crypto.randomUUID();
|
||||
private _building: boolean = false;
|
||||
|
||||
constructor(
|
||||
@@ -70,35 +87,9 @@ export class App {
|
||||
this.modules = new ModuleManager(connection, {
|
||||
...(options?.manager ?? {}),
|
||||
initial: _initialConfig,
|
||||
onUpdated: async (key, config) => {
|
||||
// if the EventManager was disabled, we assume we shouldn't
|
||||
// respond to events, such as "onUpdated".
|
||||
// this is important if multiple changes are done, and then build() is called manually
|
||||
if (!this.emgr.enabled) {
|
||||
$console.warn("App config updated, but event manager is disabled, skip.");
|
||||
return;
|
||||
}
|
||||
|
||||
$console.log("App config updated", key);
|
||||
// @todo: potentially double syncing
|
||||
await this.build({ sync: true });
|
||||
await this.emgr.emit(new AppConfigUpdatedEvent({ app: this }));
|
||||
},
|
||||
onFirstBoot: async () => {
|
||||
$console.log("App first boot");
|
||||
this.trigger_first_boot = true;
|
||||
},
|
||||
onServerInit: async (server) => {
|
||||
server.use(async (c, next) => {
|
||||
c.set("app", this);
|
||||
await next();
|
||||
|
||||
try {
|
||||
// gracefully add the app id
|
||||
c.res.headers.set("X-bknd-id", this._id);
|
||||
} catch (e) {}
|
||||
});
|
||||
},
|
||||
onUpdated: this.onUpdated.bind(this),
|
||||
onFirstBoot: this.onFirstBoot.bind(this),
|
||||
onServerInit: this.onServerInit.bind(this),
|
||||
});
|
||||
this.modules.ctx().emgr.registerEvents(AppEvents);
|
||||
}
|
||||
@@ -213,6 +204,53 @@ export class App {
|
||||
|
||||
return new Api({ host: "http://localhost", ...(options ?? {}), fetcher });
|
||||
}
|
||||
|
||||
async onUpdated<Module extends keyof Modules>(module: Module, config: ModuleConfigs[Module]) {
|
||||
// if the EventManager was disabled, we assume we shouldn't
|
||||
// respond to events, such as "onUpdated".
|
||||
// this is important if multiple changes are done, and then build() is called manually
|
||||
if (!this.emgr.enabled) {
|
||||
$console.warn("App config updated, but event manager is disabled, skip.");
|
||||
return;
|
||||
}
|
||||
|
||||
$console.log("App config updated", module);
|
||||
// @todo: potentially double syncing
|
||||
await this.build({ sync: true });
|
||||
await this.emgr.emit(new AppConfigUpdatedEvent({ app: this }));
|
||||
}
|
||||
|
||||
async onFirstBoot() {
|
||||
$console.log("App first boot");
|
||||
this.trigger_first_boot = true;
|
||||
}
|
||||
|
||||
async onServerInit(server: Hono<ServerEnv>) {
|
||||
server.use(async (c, next) => {
|
||||
c.set("app", this);
|
||||
await this.emgr.emit(new AppRequest({ app: this, request: c.req.raw }));
|
||||
await next();
|
||||
|
||||
try {
|
||||
// gracefully add the app id
|
||||
c.res.headers.set("X-bknd-id", this._id);
|
||||
} catch (e) {}
|
||||
|
||||
await this.emgr.emit(
|
||||
new AppBeforeResponse({ app: this, request: c.req.raw, response: c.res }),
|
||||
);
|
||||
|
||||
// execute collected async events (async by default)
|
||||
switch (this.options?.asyncEventsMode ?? "async") {
|
||||
case "sync":
|
||||
await this.emgr.executeAsyncs();
|
||||
break;
|
||||
case "async":
|
||||
this.emgr.executeAsyncs();
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function createApp(config: CreateAppConfig = {}) {
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import type { TestRunner } from "core/test";
|
||||
import type { BkndConfig, DefaultArgs, FrameworkOptions, RuntimeOptions } from "./index";
|
||||
import type { App } from "App";
|
||||
|
||||
export function adapterTestSuite<
|
||||
Config extends BkndConfig = BkndConfig,
|
||||
Args extends DefaultArgs = DefaultArgs,
|
||||
>(
|
||||
testRunner: TestRunner,
|
||||
{
|
||||
makeApp,
|
||||
makeHandler,
|
||||
label = "app",
|
||||
overrides = {},
|
||||
}: {
|
||||
makeApp: (
|
||||
config: Config,
|
||||
args?: Args,
|
||||
opts?: RuntimeOptions | FrameworkOptions,
|
||||
) => Promise<App>;
|
||||
makeHandler?: (
|
||||
config?: Config,
|
||||
args?: Args,
|
||||
opts?: RuntimeOptions | FrameworkOptions,
|
||||
) => (request: Request) => Promise<Response>;
|
||||
label?: string;
|
||||
overrides?: {
|
||||
dbUrl?: string;
|
||||
};
|
||||
},
|
||||
) {
|
||||
const { test, expect, mock } = testRunner;
|
||||
const id = crypto.randomUUID();
|
||||
|
||||
test(`creates ${label}`, async () => {
|
||||
const beforeBuild = mock(async () => null) as any;
|
||||
const onBuilt = mock(async () => null) as any;
|
||||
|
||||
const config = {
|
||||
app: (env) => ({
|
||||
connection: { url: env.url },
|
||||
initialConfig: {
|
||||
server: { cors: { origin: env.origin } },
|
||||
},
|
||||
}),
|
||||
beforeBuild,
|
||||
onBuilt,
|
||||
} as const satisfies BkndConfig;
|
||||
|
||||
const app = await makeApp(
|
||||
config as any,
|
||||
{
|
||||
url: overrides.dbUrl ?? ":memory:",
|
||||
origin: "localhost",
|
||||
} as any,
|
||||
{ id },
|
||||
);
|
||||
expect(app).toBeDefined();
|
||||
expect(app.toJSON().server.cors.origin).toEqual("localhost");
|
||||
expect(beforeBuild).toHaveBeenCalledTimes(1);
|
||||
expect(onBuilt).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
if (makeHandler) {
|
||||
const getConfig = async (fetcher: (r: Request) => Promise<Response>) => {
|
||||
const res = await fetcher(new Request("http://localhost:3000/api/system/config"));
|
||||
const data = (await res.json()) as any;
|
||||
return { res, data };
|
||||
};
|
||||
|
||||
test("responds with the same app id", async () => {
|
||||
const fetcher = makeHandler(undefined, undefined, { id });
|
||||
|
||||
const { res, data } = await getConfig(fetcher);
|
||||
expect(res.ok).toBe(true);
|
||||
expect(res.status).toBe(200);
|
||||
expect(data.server.cors.origin).toEqual("localhost");
|
||||
});
|
||||
|
||||
test("creates fresh & responds to api config", async () => {
|
||||
// set the same id, but force recreate
|
||||
const fetcher = makeHandler(undefined, undefined, { id, force: true });
|
||||
|
||||
const { res, data } = await getConfig(fetcher);
|
||||
expect(res.ok).toBe(true);
|
||||
expect(res.status).toBe(200);
|
||||
expect(data.server.cors.origin).toEqual("*");
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { afterAll, beforeAll, describe } from "bun:test";
|
||||
import * as astro from "./astro.adapter";
|
||||
import { disableConsoleLog, enableConsoleLog } from "core/utils";
|
||||
import { adapterTestSuite } from "adapter/adapter-test-suite";
|
||||
import { bunTestRunner } from "adapter/bun/test";
|
||||
|
||||
beforeAll(disableConsoleLog);
|
||||
afterAll(enableConsoleLog);
|
||||
|
||||
describe("astro adapter", () => {
|
||||
adapterTestSuite(bunTestRunner, {
|
||||
makeApp: astro.getApp,
|
||||
makeHandler: (c, a, o) => (request: Request) => astro.serve(c, a, o)({ request }),
|
||||
});
|
||||
});
|
||||
@@ -1,34 +1,25 @@
|
||||
import type { App } from "bknd";
|
||||
import { type FrameworkBkndConfig, createFrameworkApp } from "bknd/adapter";
|
||||
import { Api, type ApiOptions } from "bknd/client";
|
||||
|
||||
export type AstroBkndConfig<Args = TAstro> = FrameworkBkndConfig<Args>;
|
||||
import { type FrameworkBkndConfig, createFrameworkApp, type FrameworkOptions } from "bknd/adapter";
|
||||
|
||||
type AstroEnv = NodeJS.ProcessEnv;
|
||||
type TAstro = {
|
||||
request: Request;
|
||||
};
|
||||
export type AstroBkndConfig<Env = AstroEnv> = FrameworkBkndConfig<Env>;
|
||||
|
||||
export type Options = {
|
||||
mode?: "static" | "dynamic";
|
||||
} & Omit<ApiOptions, "host"> & {
|
||||
host?: string;
|
||||
};
|
||||
|
||||
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;
|
||||
export async function getApp<Env = AstroEnv>(
|
||||
config: AstroBkndConfig<Env> = {},
|
||||
args: Env = {} as Env,
|
||||
opts: FrameworkOptions = {},
|
||||
) {
|
||||
return await createFrameworkApp(config, args ?? import.meta.env, opts);
|
||||
}
|
||||
|
||||
let app: App;
|
||||
export function serve<Context extends TAstro = TAstro>(config: AstroBkndConfig<Context> = {}) {
|
||||
return async (args: Context) => {
|
||||
if (!app) {
|
||||
app = await createFrameworkApp(config, args);
|
||||
}
|
||||
return app.fetch(args.request);
|
||||
export function serve<Env = AstroEnv>(
|
||||
config: AstroBkndConfig<Env> = {},
|
||||
args: Env = {} as Env,
|
||||
opts?: FrameworkOptions,
|
||||
) {
|
||||
return async (fnArgs: TAstro) => {
|
||||
return (await getApp(config, args, opts)).fetch(fnArgs.request);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,68 +1,76 @@
|
||||
import type { App } from "bknd";
|
||||
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 & {
|
||||
assets?:
|
||||
| {
|
||||
mode: "local";
|
||||
root: string;
|
||||
}
|
||||
| {
|
||||
mode: "url";
|
||||
url: string;
|
||||
};
|
||||
};
|
||||
type AwsLambdaEnv = object;
|
||||
export type AwsLambdaBkndConfig<Env extends AwsLambdaEnv = AwsLambdaEnv> =
|
||||
RuntimeBkndConfig<Env> & {
|
||||
assets?:
|
||||
| {
|
||||
mode: "local";
|
||||
root: string;
|
||||
}
|
||||
| {
|
||||
mode: "url";
|
||||
url: string;
|
||||
};
|
||||
};
|
||||
|
||||
let app: App;
|
||||
export async function createApp({
|
||||
adminOptions = false,
|
||||
assets,
|
||||
...config
|
||||
}: AwsLambdaBkndConfig = {}) {
|
||||
if (!app) {
|
||||
let additional: Partial<RuntimeBkndConfig> = {
|
||||
adminOptions,
|
||||
};
|
||||
export async function createApp<Env extends AwsLambdaEnv = AwsLambdaEnv>(
|
||||
{ adminOptions = false, assets, ...config }: AwsLambdaBkndConfig<Env> = {},
|
||||
args: Env = {} as Env,
|
||||
opts?: RuntimeOptions,
|
||||
): Promise<App> {
|
||||
let additional: Partial<RuntimeBkndConfig> = {
|
||||
adminOptions,
|
||||
};
|
||||
|
||||
if (assets?.mode) {
|
||||
switch (assets.mode) {
|
||||
case "local":
|
||||
// @todo: serve static outside app context
|
||||
additional = {
|
||||
adminOptions: adminOptions === false ? undefined : adminOptions,
|
||||
serveStatic: (await import("@hono/node-server/serve-static")).serveStatic({
|
||||
root: assets.root,
|
||||
onFound: (path, c) => {
|
||||
c.res.headers.set("Cache-Control", "public, max-age=31536000");
|
||||
},
|
||||
}),
|
||||
};
|
||||
break;
|
||||
case "url":
|
||||
additional.adminOptions = {
|
||||
...(typeof adminOptions === "object" ? adminOptions : {}),
|
||||
assets_path: assets.url,
|
||||
};
|
||||
break;
|
||||
default:
|
||||
throw new Error("Invalid assets mode");
|
||||
}
|
||||
if (assets?.mode) {
|
||||
switch (assets.mode) {
|
||||
case "local":
|
||||
// @todo: serve static outside app context
|
||||
additional = {
|
||||
adminOptions: adminOptions === false ? undefined : adminOptions,
|
||||
serveStatic: serveStatic({
|
||||
root: assets.root,
|
||||
onFound: (path, c) => {
|
||||
c.res.headers.set("Cache-Control", "public, max-age=31536000");
|
||||
},
|
||||
}),
|
||||
};
|
||||
break;
|
||||
case "url":
|
||||
additional.adminOptions = {
|
||||
...(typeof adminOptions === "object" ? adminOptions : {}),
|
||||
assets_path: assets.url,
|
||||
};
|
||||
break;
|
||||
default:
|
||||
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 = {}) {
|
||||
console.log("serving lambda");
|
||||
export function serve<Env extends AwsLambdaEnv = AwsLambdaEnv>(
|
||||
config: AwsLambdaBkndConfig<Env> = {},
|
||||
args: Env = {} as Env,
|
||||
opts?: RuntimeOptions,
|
||||
) {
|
||||
return async (event) => {
|
||||
const app = await createApp(config);
|
||||
const app = await createApp(config, args, opts);
|
||||
return await handle(app.server)(event);
|
||||
};
|
||||
}
|
||||
|
||||
// compatibility with old code
|
||||
export const serveLambda = serve;
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -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" />
|
||||
|
||||
import path from "node:path";
|
||||
import type { App } from "bknd";
|
||||
import { type RuntimeBkndConfig, createRuntimeApp } from "bknd/adapter";
|
||||
import { type RuntimeBkndConfig, createRuntimeApp, type RuntimeOptions } from "bknd/adapter";
|
||||
import { registerLocalMediaAdapter } from "bknd/adapter/node";
|
||||
import { config } from "bknd/core";
|
||||
import type { ServeOptions } from "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({ distPath, ...config }: RuntimeBkndConfig = {}) {
|
||||
export async function createApp<Env = BunEnv>(
|
||||
{ distPath, ...config }: BunBkndConfig<Env> = {},
|
||||
args: Env = {} as Env,
|
||||
opts?: RuntimeOptions,
|
||||
) {
|
||||
const root = path.resolve(distPath ?? "./node_modules/bknd/dist", "static");
|
||||
registerLocalMediaAdapter();
|
||||
|
||||
if (!app) {
|
||||
registerLocalMediaAdapter();
|
||||
app = await createRuntimeApp({
|
||||
return await createRuntimeApp(
|
||||
{
|
||||
...config,
|
||||
serveStatic: serveStatic({ root }),
|
||||
});
|
||||
}
|
||||
|
||||
return app;
|
||||
},
|
||||
args ?? (process.env as Env),
|
||||
opts,
|
||||
);
|
||||
}
|
||||
|
||||
export function serve({
|
||||
distPath,
|
||||
connection,
|
||||
initialConfig,
|
||||
options,
|
||||
port = config.server.default_port,
|
||||
onBuilt,
|
||||
buildConfig,
|
||||
adminOptions,
|
||||
...serveOptions
|
||||
}: BunBkndConfig = {}) {
|
||||
export function createHandler<Env = BunEnv>(
|
||||
config: BunBkndConfig<Env> = {},
|
||||
args: Env = {} as Env,
|
||||
opts?: RuntimeOptions,
|
||||
) {
|
||||
return async (req: Request) => {
|
||||
const app = await createApp(config, args ?? (process.env as Env), opts);
|
||||
return app.fetch(req);
|
||||
};
|
||||
}
|
||||
|
||||
export function serve<Env = BunEnv>(
|
||||
{
|
||||
distPath,
|
||||
connection,
|
||||
initialConfig,
|
||||
options,
|
||||
port = config.server.default_port,
|
||||
onBuilt,
|
||||
buildConfig,
|
||||
adminOptions,
|
||||
...serveOptions
|
||||
}: BunBkndConfig<Env> = {},
|
||||
args: Env = {} as Env,
|
||||
opts?: RuntimeOptions,
|
||||
) {
|
||||
Bun.serve({
|
||||
...serveOptions,
|
||||
port,
|
||||
fetch: async (request: Request) => {
|
||||
const app = await createApp({
|
||||
fetch: createHandler(
|
||||
{
|
||||
connection,
|
||||
initialConfig,
|
||||
options,
|
||||
@@ -49,9 +66,10 @@ export function serve({
|
||||
buildConfig,
|
||||
adminOptions,
|
||||
distPath,
|
||||
});
|
||||
return app.fetch(request);
|
||||
},
|
||||
},
|
||||
args,
|
||||
opts,
|
||||
),
|
||||
});
|
||||
|
||||
console.log(`Server is running on http://localhost:${port}`);
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { expect, test, mock } from "bun:test";
|
||||
|
||||
export const bunTestRunner = {
|
||||
expect,
|
||||
test,
|
||||
mock,
|
||||
};
|
||||
@@ -0,0 +1,60 @@
|
||||
import { afterAll, beforeAll, describe, expect, it } from "bun:test";
|
||||
import { makeApp } from "./modes/fresh";
|
||||
import { makeConfig } from "./config";
|
||||
import { disableConsoleLog, enableConsoleLog } from "core/utils";
|
||||
import { adapterTestSuite } from "adapter/adapter-test-suite";
|
||||
import { bunTestRunner } from "adapter/bun/test";
|
||||
import type { CloudflareBkndConfig } from "./cloudflare-workers.adapter";
|
||||
|
||||
beforeAll(disableConsoleLog);
|
||||
afterAll(enableConsoleLog);
|
||||
|
||||
describe("cf adapter", () => {
|
||||
const DB_URL = ":memory:";
|
||||
const $ctx = (env?: any, request?: Request, ctx?: ExecutionContext) => ({
|
||||
request: request ?? (null as any),
|
||||
env: env ?? { DB_URL },
|
||||
ctx: ctx ?? (null as any),
|
||||
});
|
||||
|
||||
it("makes config", async () => {
|
||||
expect(
|
||||
makeConfig(
|
||||
{
|
||||
connection: { url: DB_URL },
|
||||
},
|
||||
{},
|
||||
),
|
||||
).toEqual({ connection: { url: DB_URL } });
|
||||
|
||||
expect(
|
||||
makeConfig(
|
||||
{
|
||||
app: (env) => ({
|
||||
connection: { url: env.DB_URL },
|
||||
}),
|
||||
},
|
||||
{
|
||||
DB_URL,
|
||||
},
|
||||
),
|
||||
).toEqual({ connection: { url: DB_URL } });
|
||||
});
|
||||
|
||||
adapterTestSuite<CloudflareBkndConfig, object>(bunTestRunner, {
|
||||
makeApp,
|
||||
makeHandler: (c, a, o) => {
|
||||
return async (request: any) => {
|
||||
const app = await makeApp(
|
||||
// needs a fallback, otherwise tries to launch D1
|
||||
c ?? {
|
||||
connection: { url: DB_URL },
|
||||
},
|
||||
a,
|
||||
o,
|
||||
);
|
||||
return app.fetch(request);
|
||||
};
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -1,18 +1,16 @@
|
||||
/// <reference types="@cloudflare/workers-types" />
|
||||
|
||||
import { type FrameworkBkndConfig, makeConfig } from "bknd/adapter";
|
||||
import type { FrameworkBkndConfig } from "bknd/adapter";
|
||||
import { Hono } from "hono";
|
||||
import { serveStatic } from "hono/cloudflare-workers";
|
||||
import { D1Connection } from "./D1Connection";
|
||||
import { registerMedia } from "./StorageR2Adapter";
|
||||
import { getBinding } from "./bindings";
|
||||
import { getCached } from "./modes/cached";
|
||||
import { getDurable } from "./modes/durable";
|
||||
import { getFresh, getWarm } from "./modes/fresh";
|
||||
|
||||
export type CloudflareBkndConfig<Env = any> = FrameworkBkndConfig<Context<Env>> & {
|
||||
export type CloudflareEnv = object;
|
||||
export type CloudflareBkndConfig<Env = CloudflareEnv> = FrameworkBkndConfig<Env> & {
|
||||
mode?: "warm" | "fresh" | "cache" | "durable";
|
||||
bindings?: (args: Context<Env>) => {
|
||||
bindings?: (args: Env) => {
|
||||
kv?: KVNamespace;
|
||||
dobj?: DurableObjectNamespace;
|
||||
db?: D1Database;
|
||||
@@ -26,45 +24,15 @@ export type CloudflareBkndConfig<Env = any> = FrameworkBkndConfig<Context<Env>>
|
||||
html?: string;
|
||||
};
|
||||
|
||||
export type Context<Env = any> = {
|
||||
export type Context<Env = CloudflareEnv> = {
|
||||
request: Request;
|
||||
env: Env;
|
||||
ctx: ExecutionContext;
|
||||
};
|
||||
|
||||
let media_registered: boolean = false;
|
||||
export function makeCfConfig(config: CloudflareBkndConfig, context: Context) {
|
||||
if (!media_registered) {
|
||||
registerMedia(context.env as any);
|
||||
media_registered = true;
|
||||
}
|
||||
|
||||
const appConfig = makeConfig(config, context);
|
||||
const bindings = config.bindings?.(context);
|
||||
if (!appConfig.connection) {
|
||||
let db: D1Database | undefined;
|
||||
if (bindings?.db) {
|
||||
console.log("Using database from bindings");
|
||||
db = bindings.db;
|
||||
} else if (Object.keys(context.env ?? {}).length > 0) {
|
||||
const binding = getBinding(context.env, "D1Database");
|
||||
if (binding) {
|
||||
console.log(`Using database from env "${binding.key}"`);
|
||||
db = binding.value;
|
||||
}
|
||||
}
|
||||
|
||||
if (db) {
|
||||
appConfig.connection = new D1Connection({ binding: db });
|
||||
} else {
|
||||
throw new Error("No database connection given");
|
||||
}
|
||||
}
|
||||
|
||||
return appConfig;
|
||||
}
|
||||
|
||||
export function serve<Env = any>(config: CloudflareBkndConfig<Env> = {}) {
|
||||
export function serve<Env extends CloudflareEnv = CloudflareEnv>(
|
||||
config: CloudflareBkndConfig<Env> = {},
|
||||
) {
|
||||
return {
|
||||
async fetch(request: Request, env: Env, ctx: ExecutionContext) {
|
||||
const url = new URL(request.url);
|
||||
@@ -99,7 +67,7 @@ export function serve<Env = any>(config: CloudflareBkndConfig<Env> = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
const context = { request, env, ctx } as Context;
|
||||
const context = { request, env, ctx } as Context<Env>;
|
||||
const mode = config.mode ?? "warm";
|
||||
|
||||
switch (mode) {
|
||||
|
||||
@@ -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 { createRuntimeApp } from "bknd/adapter";
|
||||
import { type CloudflareBkndConfig, type Context, makeCfConfig } from "../index";
|
||||
import type { CloudflareBkndConfig, Context, CloudflareEnv } from "../index";
|
||||
import { makeConfig, registerAsyncsExecutionContext, constants } from "../config";
|
||||
|
||||
export async function getCached(config: CloudflareBkndConfig, { env, ctx, ...args }: Context) {
|
||||
export async function getCached<Env extends CloudflareEnv = CloudflareEnv>(
|
||||
config: CloudflareBkndConfig<Env>,
|
||||
{ env, ctx, ...args }: Context<Env>,
|
||||
) {
|
||||
const { kv } = config.bindings?.(env)!;
|
||||
if (!kv) throw new Error("kv namespace is not defined in cloudflare.bindings");
|
||||
const key = config.key ?? "app";
|
||||
@@ -16,10 +20,11 @@ export async function getCached(config: CloudflareBkndConfig, { env, ctx, ...arg
|
||||
|
||||
const app = await createRuntimeApp(
|
||||
{
|
||||
...makeCfConfig(config, { env, ctx, ...args }),
|
||||
...makeConfig(config, env),
|
||||
initialConfig,
|
||||
onBuilt: async (app) => {
|
||||
app.module.server.client.get("/__bknd/cache", async (c) => {
|
||||
registerAsyncsExecutionContext(app, ctx);
|
||||
app.module.server.client.get(constants.cache_endpoint, async (c) => {
|
||||
await kv.delete(key);
|
||||
return c.json({ message: "Cache cleared" });
|
||||
});
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import { DurableObject } from "cloudflare:workers";
|
||||
import type { App, CreateAppConfig } from "bknd";
|
||||
import { createRuntimeApp, makeConfig } from "bknd/adapter";
|
||||
import type { CloudflareBkndConfig, Context } from "../index";
|
||||
import type { CloudflareBkndConfig, Context, CloudflareEnv } from "../index";
|
||||
import { constants, registerAsyncsExecutionContext } from "../config";
|
||||
|
||||
export async function getDurable(config: CloudflareBkndConfig, ctx: Context) {
|
||||
export async function getDurable<Env extends CloudflareEnv = CloudflareEnv>(
|
||||
config: CloudflareBkndConfig<Env>,
|
||||
ctx: Context<Env>,
|
||||
) {
|
||||
const { dobj } = config.bindings?.(ctx.env)!;
|
||||
if (!dobj) throw new Error("durable object is not defined in cloudflare.bindings");
|
||||
const key = config.key ?? "app";
|
||||
@@ -17,7 +21,7 @@ export async function getDurable(config: CloudflareBkndConfig, ctx: Context) {
|
||||
const id = dobj.idFromName(key);
|
||||
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, {
|
||||
config: create_config,
|
||||
@@ -67,7 +71,8 @@ export class DurableBkndApp extends DurableObject {
|
||||
this.app = await createRuntimeApp({
|
||||
...config,
|
||||
onBuilt: async (app) => {
|
||||
app.modules.server.get("/__do", async (c) => {
|
||||
registerAsyncsExecutionContext(app, this.ctx);
|
||||
app.modules.server.get(constants.do_endpoint, async (c) => {
|
||||
// @ts-ignore
|
||||
const context: any = c.req.raw.cf ? c.req.raw.cf : c.env.cf;
|
||||
return c.json({
|
||||
@@ -92,7 +97,6 @@ export class DurableBkndApp extends DurableObject {
|
||||
this.keepAlive(options.keepAliveSeconds);
|
||||
}
|
||||
|
||||
console.log("id", this.id);
|
||||
const res = await this.app!.fetch(request);
|
||||
const headers = new Headers(res.headers);
|
||||
headers.set("X-BuildTime", buildtime.toString());
|
||||
@@ -109,16 +113,13 @@ export class DurableBkndApp extends DurableObject {
|
||||
async beforeBuild(app: App) {}
|
||||
|
||||
protected keepAlive(seconds: number) {
|
||||
console.log("keep alive for", seconds);
|
||||
if (this.interval) {
|
||||
console.log("clearing, there is a new");
|
||||
clearInterval(this.interval);
|
||||
}
|
||||
|
||||
let i = 0;
|
||||
this.interval = setInterval(() => {
|
||||
i += 1;
|
||||
//console.log("keep-alive", i);
|
||||
if (i === seconds) {
|
||||
console.log("cleared");
|
||||
clearInterval(this.interval);
|
||||
|
||||
@@ -1,27 +1,48 @@
|
||||
import type { App } from "bknd";
|
||||
import { createRuntimeApp } from "bknd/adapter";
|
||||
import { type CloudflareBkndConfig, type Context, makeCfConfig } from "../index";
|
||||
import { createRuntimeApp, type RuntimeOptions } from "bknd/adapter";
|
||||
import type { CloudflareBkndConfig, Context, CloudflareEnv } from "../index";
|
||||
import { makeConfig, registerAsyncsExecutionContext } from "../config";
|
||||
|
||||
export async function makeApp(config: CloudflareBkndConfig, ctx: Context) {
|
||||
return await createRuntimeApp(
|
||||
export async function makeApp<Env extends CloudflareEnv = CloudflareEnv>(
|
||||
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,
|
||||
},
|
||||
ctx,
|
||||
args,
|
||||
opts,
|
||||
);
|
||||
}
|
||||
|
||||
export async function getFresh(config: CloudflareBkndConfig, ctx: Context) {
|
||||
const app = await makeApp(config, ctx);
|
||||
export async function getWarm<Env extends CloudflareEnv = CloudflareEnv>(
|
||||
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);
|
||||
}
|
||||
|
||||
let warm_app: App;
|
||||
export async function getWarm(config: CloudflareBkndConfig, ctx: Context) {
|
||||
if (!warm_app) {
|
||||
warm_app = await makeApp(config, ctx);
|
||||
}
|
||||
|
||||
return warm_app.fetch(ctx.request);
|
||||
export async function getFresh<Env extends CloudflareEnv = CloudflareEnv>(
|
||||
config: CloudflareBkndConfig<Env>,
|
||||
ctx: Context<Env>,
|
||||
opts: RuntimeOptions = {},
|
||||
) {
|
||||
return await getWarm(config, ctx, {
|
||||
...opts,
|
||||
force: true,
|
||||
});
|
||||
}
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
import { createWriteStream, readFileSync } from "node:fs";
|
||||
import { test } from "node:test";
|
||||
import { Miniflare } from "miniflare";
|
||||
import { StorageR2Adapter } from "adapter/cloudflare/StorageR2Adapter";
|
||||
import { StorageR2Adapter } from "./StorageR2Adapter";
|
||||
import { adapterTestSuite } from "media";
|
||||
import { nodeTestRunner } from "adapter/node";
|
||||
import path from "node:path";
|
||||
@@ -23,7 +23,7 @@ test("StorageR2Adapter", async () => {
|
||||
const bucket = (await mf.getR2Bucket("BUCKET")) as unknown as R2Bucket;
|
||||
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 file = new File([buffer], "image.png", { type: "image/png" });
|
||||
|
||||
+2
-4
@@ -1,10 +1,8 @@
|
||||
import { registries } from "bknd";
|
||||
import { isDebug } from "bknd/core";
|
||||
import { StringEnum, Type } from "bknd/utils";
|
||||
import type { FileBody } from "media/storage/Storage";
|
||||
import { StorageAdapter } from "media/storage/StorageAdapter";
|
||||
import { guess } from "media/storage/mime-types-tiny";
|
||||
import { getBindings } from "./bindings";
|
||||
import { guessMimeType as guess, StorageAdapter, type FileBody } from "bknd/media";
|
||||
import { getBindings } from "../bindings";
|
||||
|
||||
export function makeSchema(bindings: string[] = []) {
|
||||
return Type.Object(
|
||||
+80
-43
@@ -12,76 +12,113 @@ export type BkndConfig<Args = any> = CreateAppConfig & {
|
||||
|
||||
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> & {
|
||||
distPath?: string;
|
||||
serveStatic?: MiddlewareHandler | [string, MiddlewareHandler];
|
||||
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 = {};
|
||||
if ("app" in config && config.app) {
|
||||
if (typeof config.app === "function") {
|
||||
const { app, ...rest } = config;
|
||||
if (app) {
|
||||
if (typeof app === "function") {
|
||||
if (!args) {
|
||||
throw new Error("args is required when config.app is a function");
|
||||
}
|
||||
additionalConfig = config.app(args);
|
||||
additionalConfig = app(args);
|
||||
} else {
|
||||
additionalConfig = config.app;
|
||||
additionalConfig = app;
|
||||
}
|
||||
}
|
||||
|
||||
return { ...config, ...additionalConfig };
|
||||
return { ...rest, ...additionalConfig };
|
||||
}
|
||||
|
||||
export async function createFrameworkApp<Args = any>(
|
||||
config: FrameworkBkndConfig,
|
||||
// a map that contains all apps by id
|
||||
const apps = new Map<string, App>();
|
||||
export async function createAdapterApp<Config extends BkndConfig = BkndConfig, Args = DefaultArgs>(
|
||||
config: Config = {} as Config,
|
||||
args?: Args,
|
||||
opts?: CreateAdapterAppOptions,
|
||||
): 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.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);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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 { type FrameworkBkndConfig, createFrameworkApp } from "bknd/adapter";
|
||||
import { isNode } from "core/utils";
|
||||
import { createFrameworkApp, type FrameworkBkndConfig, type FrameworkOptions } from "bknd/adapter";
|
||||
import { isNode } from "bknd/utils";
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
export type NextjsBkndConfig = FrameworkBkndConfig & {
|
||||
type NextjsEnv = NextApiRequest["env"];
|
||||
|
||||
export type NextjsBkndConfig<Env = NextjsEnv> = FrameworkBkndConfig<Env> & {
|
||||
cleanRequest?: { searchParams?: string[] };
|
||||
};
|
||||
|
||||
type NextjsContext = {
|
||||
env: Record<string, string | undefined>;
|
||||
};
|
||||
|
||||
let app: App;
|
||||
let building: boolean = false;
|
||||
|
||||
export async function getApp<Args extends NextjsContext = NextjsContext>(
|
||||
config: NextjsBkndConfig,
|
||||
args?: Args,
|
||||
export async function getApp<Env = NextjsEnv>(
|
||||
config: NextjsBkndConfig<Env>,
|
||||
args: Env = {} as Env,
|
||||
opts?: FrameworkOptions,
|
||||
) {
|
||||
if (building) {
|
||||
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;
|
||||
return await createFrameworkApp(config, args ?? (process.env as Env), opts);
|
||||
}
|
||||
|
||||
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) => {
|
||||
if (!app) {
|
||||
app = await getApp(config, { env: process.env ?? {} });
|
||||
}
|
||||
const app = await getApp(config, args, opts);
|
||||
const request = getCleanRequest(req, cleanRequest);
|
||||
return app.fetch(request);
|
||||
};
|
||||
|
||||
@@ -5,6 +5,15 @@ export * from "./node.adapter";
|
||||
export { StorageLocalAdapter, type LocalAdapterConfig };
|
||||
export { nodeTestRunner } from "./test";
|
||||
|
||||
let registered = false;
|
||||
export function registerLocalMediaAdapter() {
|
||||
registries.media.register("local", StorageLocalAdapter);
|
||||
if (!registered) {
|
||||
registries.media.register("local", StorageLocalAdapter);
|
||||
registered = true;
|
||||
}
|
||||
|
||||
return (config: Partial<LocalAdapterConfig> = {}) => {
|
||||
const adapter = new StorageLocalAdapter(config);
|
||||
return adapter.toJSON(true);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { describe, before, after } from "node:test";
|
||||
import * as node from "./node.adapter";
|
||||
import { adapterTestSuite } from "adapter/adapter-test-suite";
|
||||
import { nodeTestRunner } from "adapter/node";
|
||||
import { disableConsoleLog, enableConsoleLog } from "core/utils";
|
||||
|
||||
before(() => disableConsoleLog());
|
||||
after(enableConsoleLog);
|
||||
|
||||
describe("node adapter", () => {
|
||||
adapterTestSuite(nodeTestRunner, {
|
||||
makeApp: node.createApp,
|
||||
makeHandler: node.createHandler,
|
||||
});
|
||||
});
|
||||
@@ -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 { serveStatic } from "@hono/node-server/serve-static";
|
||||
import { registerLocalMediaAdapter } from "adapter/node/index";
|
||||
import type { App } from "bknd";
|
||||
import { type RuntimeBkndConfig, createRuntimeApp } from "bknd/adapter";
|
||||
import { type RuntimeBkndConfig, createRuntimeApp, type RuntimeOptions } from "bknd/adapter";
|
||||
import { config as $config } from "bknd/core";
|
||||
|
||||
export type NodeBkndConfig = RuntimeBkndConfig & {
|
||||
type NodeEnv = NodeJS.ProcessEnv;
|
||||
export type NodeBkndConfig<Env = NodeEnv> = RuntimeBkndConfig<Env> & {
|
||||
port?: number;
|
||||
hostname?: string;
|
||||
listener?: Parameters<typeof honoServe>[1];
|
||||
@@ -14,14 +14,11 @@ export type NodeBkndConfig = RuntimeBkndConfig & {
|
||||
relativeDistPath?: string;
|
||||
};
|
||||
|
||||
export function serve({
|
||||
distPath,
|
||||
relativeDistPath,
|
||||
port = $config.server.default_port,
|
||||
hostname,
|
||||
listener,
|
||||
...config
|
||||
}: NodeBkndConfig = {}) {
|
||||
export async function createApp<Env = NodeEnv>(
|
||||
{ distPath, relativeDistPath, ...config }: NodeBkndConfig<Env> = {},
|
||||
args: Env = {} as Env,
|
||||
opts?: RuntimeOptions,
|
||||
) {
|
||||
const root = path.relative(
|
||||
process.cwd(),
|
||||
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");
|
||||
}
|
||||
|
||||
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(
|
||||
{
|
||||
port,
|
||||
hostname,
|
||||
fetch: async (req: Request) => {
|
||||
if (!app) {
|
||||
registerLocalMediaAdapter();
|
||||
app = await createRuntimeApp({
|
||||
...config,
|
||||
serveStatic: serveStatic({ root }),
|
||||
});
|
||||
}
|
||||
|
||||
return app.fetch(req);
|
||||
},
|
||||
fetch: createHandler(config, args, opts),
|
||||
},
|
||||
(connInfo) => {
|
||||
console.log(`Server is running on http://localhost:${connInfo.port}`);
|
||||
|
||||
@@ -3,6 +3,7 @@ import { StorageLocalAdapter } from "./StorageLocalAdapter";
|
||||
// @ts-ignore
|
||||
import { assetsPath, assetsTmpPath } from "../../../../__test__/helper";
|
||||
import { adapterTestSuite } from "media/storage/adapters/adapter-test-suite";
|
||||
import { bunTestRunner } from "adapter/bun/test";
|
||||
|
||||
describe("StorageLocalAdapter (bun)", async () => {
|
||||
const adapter = new StorageLocalAdapter({
|
||||
@@ -10,5 +11,5 @@ describe("StorageLocalAdapter (bun)", async () => {
|
||||
});
|
||||
|
||||
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: "./" }),
|
||||
},
|
||||
{ title: "Local", description: "Local file system storage" },
|
||||
{ title: "Local", description: "Local file system storage", additionalProperties: false },
|
||||
);
|
||||
export type LocalAdapterConfig = Static<typeof localAdapterConfig>;
|
||||
|
||||
export class StorageLocalAdapter extends StorageAdapter {
|
||||
private config: LocalAdapterConfig;
|
||||
|
||||
constructor(config: any) {
|
||||
constructor(config: Partial<LocalAdapterConfig> = {}) {
|
||||
super();
|
||||
this.config = parse(localAdapterConfig, config);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,17 @@ import nodeAssert from "node:assert/strict";
|
||||
import { test } from "node: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) =>
|
||||
({
|
||||
toEqual: (expected: T, failMsg = parentFailMsg) => {
|
||||
@@ -23,6 +34,18 @@ const nodeTestMatcher = <T = unknown>(actual: T, parentFailMsg?: string) =>
|
||||
const e = Array.isArray(expected) ? expected : [expected];
|
||||
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>;
|
||||
|
||||
const nodeTestResolverProxy = <T = unknown>(
|
||||
@@ -63,6 +86,7 @@ nodeTest.skipIf = (condition: boolean): Test => {
|
||||
|
||||
export const nodeTestRunner: TestRunner = {
|
||||
test: nodeTest,
|
||||
mock: createMockFunction,
|
||||
expect: <T = unknown>(actual?: T, failMsg?: string) => ({
|
||||
...nodeTestMatcher(actual, failMsg),
|
||||
resolves: nodeTestResolverProxy(actual as Promise<T>, {
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { afterAll, beforeAll, describe } from "bun:test";
|
||||
import * as rr from "./react-router.adapter";
|
||||
import { disableConsoleLog, enableConsoleLog } from "core/utils";
|
||||
import { adapterTestSuite } from "adapter/adapter-test-suite";
|
||||
import { bunTestRunner } from "adapter/bun/test";
|
||||
|
||||
beforeAll(disableConsoleLog);
|
||||
afterAll(enableConsoleLog);
|
||||
|
||||
describe("react-router adapter", () => {
|
||||
adapterTestSuite(bunTestRunner, {
|
||||
makeApp: rr.getApp,
|
||||
makeHandler: (c, a, o) => (request: Request) => rr.serve(c, a?.env, o)({ request }),
|
||||
});
|
||||
});
|
||||
@@ -1,39 +1,26 @@
|
||||
import type { App } from "bknd";
|
||||
import { type FrameworkBkndConfig, createFrameworkApp } from "bknd/adapter";
|
||||
import type { FrameworkOptions } from "adapter";
|
||||
|
||||
type ReactRouterContext = {
|
||||
type ReactRouterEnv = NodeJS.ProcessEnv;
|
||||
type ReactRouterFunctionArgs = {
|
||||
request: Request;
|
||||
};
|
||||
export type ReactRouterBkndConfig<Args = ReactRouterContext> = FrameworkBkndConfig<Args>;
|
||||
export type ReactRouterBkndConfig<Env = ReactRouterEnv> = FrameworkBkndConfig<Env>;
|
||||
|
||||
let app: App;
|
||||
let building: boolean = false;
|
||||
|
||||
export async function getApp<Args extends ReactRouterContext = ReactRouterContext>(
|
||||
config: ReactRouterBkndConfig<Args>,
|
||||
args?: Args,
|
||||
export async function getApp<Env = ReactRouterEnv>(
|
||||
config: ReactRouterBkndConfig<Env>,
|
||||
args: Env = {} as Env,
|
||||
opts?: FrameworkOptions,
|
||||
) {
|
||||
if (building) {
|
||||
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;
|
||||
return await createFrameworkApp(config, args ?? process.env, opts);
|
||||
}
|
||||
|
||||
export function serve<Args extends ReactRouterContext = ReactRouterContext>(
|
||||
config: ReactRouterBkndConfig<Args> = {},
|
||||
export function serve<Env = ReactRouterEnv>(
|
||||
config: ReactRouterBkndConfig<Env> = {},
|
||||
args: Env = {} as Env,
|
||||
opts?: FrameworkOptions,
|
||||
) {
|
||||
return async (args: Args) => {
|
||||
app = await getApp(config, args);
|
||||
return app.fetch(args.request);
|
||||
return async (fnArgs: ReactRouterFunctionArgs) => {
|
||||
return (await getApp(config, args, opts)).fetch(fnArgs.request);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Permission } from "core";
|
||||
import { $console, type Permission } from "core";
|
||||
import { patternMatch } from "core/utils";
|
||||
import type { Context } from "hono";
|
||||
import { createMiddleware } from "hono/factory";
|
||||
@@ -49,7 +49,7 @@ export const auth = (options?: {
|
||||
// make sure to only register once
|
||||
if (authCtx.registered) {
|
||||
skipped = true;
|
||||
console.warn(`auth middleware already registered for ${getPath(c)}`);
|
||||
$console.warn(`auth middleware already registered for ${getPath(c)}`);
|
||||
} else {
|
||||
authCtx.registered = true;
|
||||
|
||||
@@ -93,7 +93,7 @@ export const permission = (
|
||||
if (app?.module.auth.enabled) {
|
||||
throw new Error(msg);
|
||||
} else {
|
||||
console.warn(msg);
|
||||
$console.warn(msg);
|
||||
}
|
||||
} else if (!authCtx.skip) {
|
||||
const guard = app.modules.ctx().guard;
|
||||
|
||||
@@ -24,6 +24,32 @@ export async function overrideJson<File extends object = object>(
|
||||
await writeFile(pkgPath, JSON.stringify(newPkg, null, opts?.indent || 2));
|
||||
}
|
||||
|
||||
export async function upsertEnvFile(
|
||||
kv: Record<string, string | number>,
|
||||
opts?: { dir?: string; file?: string },
|
||||
) {
|
||||
const file = opts?.file ?? ".env";
|
||||
const envPath = path.resolve(opts?.dir ?? process.cwd(), file);
|
||||
const current: Record<string, string | number> = {};
|
||||
|
||||
try {
|
||||
const values = await readFile(envPath, "utf-8");
|
||||
const lines = values.split("\n");
|
||||
for (const line of lines) {
|
||||
const [key, value] = line.split("=");
|
||||
if (key && value) {
|
||||
current[key] = value;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
await writeFile(envPath, "");
|
||||
}
|
||||
|
||||
const newEnv = { ...current, ...kv };
|
||||
const lines = Object.entries(newEnv).map(([key, value]) => `${key}=${value}`);
|
||||
await writeFile(envPath, lines.join("\n"));
|
||||
}
|
||||
|
||||
export async function overridePackageJson(
|
||||
fn: (pkg: TPackageJson) => Promise<TPackageJson> | TPackageJson,
|
||||
opts?: { dir?: string },
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import * as $p from "@clack/prompts";
|
||||
import { upsertEnvFile } from "cli/commands/create/npm";
|
||||
import { typewriter } from "cli/utils/cli";
|
||||
import c from "picocolors";
|
||||
import type { Template } from ".";
|
||||
import open from "open";
|
||||
|
||||
export const aws = {
|
||||
key: "aws",
|
||||
title: "AWS Lambda Basic",
|
||||
integration: "aws",
|
||||
description: "A basic bknd AWS Lambda starter",
|
||||
path: "gh:bknd-io/bknd/examples/aws-lambda",
|
||||
ref: true,
|
||||
setup: async (ctx) => {
|
||||
await $p.stream.info(
|
||||
(async function* () {
|
||||
yield* typewriter("You need a running LibSQL instance for this adapter to work.");
|
||||
})(),
|
||||
);
|
||||
|
||||
const choice = await $p.select({
|
||||
message: "How do you want to proceed?",
|
||||
options: [
|
||||
{ label: "Enter instance details", value: "enter" },
|
||||
{ label: "Create a new instance", value: "new" },
|
||||
],
|
||||
});
|
||||
if ($p.isCancel(choice)) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (choice === "new") {
|
||||
await $p.stream.info(
|
||||
(async function* () {
|
||||
yield* typewriter(c.dim("Proceed on turso.tech to create your instance."));
|
||||
})(),
|
||||
);
|
||||
|
||||
await open("https://sqlite.new");
|
||||
}
|
||||
|
||||
const url = await $p.text({
|
||||
message: "Enter database URL",
|
||||
placeholder: "libsql://<instance>.turso.io",
|
||||
validate: (v) => {
|
||||
if (!v) {
|
||||
return "Invalid URL";
|
||||
}
|
||||
return;
|
||||
},
|
||||
});
|
||||
if ($p.isCancel(url)) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const token = await $p.text({
|
||||
message: "Enter database token",
|
||||
placeholder: "eyJhbGciOiJIUzI1NiIsInR...",
|
||||
validate: (v) => {
|
||||
if (!v) {
|
||||
return "";
|
||||
}
|
||||
return;
|
||||
},
|
||||
});
|
||||
if ($p.isCancel(token)) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
await upsertEnvFile(
|
||||
{
|
||||
DB_URL: url,
|
||||
DB_TOKEN: token ?? "",
|
||||
},
|
||||
{ dir: ctx.dir },
|
||||
);
|
||||
|
||||
await $p.stream.info(
|
||||
(async function* () {
|
||||
yield* typewriter(`Connection details written to ${c.cyan(".env")}`);
|
||||
})(),
|
||||
);
|
||||
},
|
||||
} as const satisfies Template;
|
||||
@@ -78,6 +78,12 @@ async function createD1(ctx: TemplateSetupCtx) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
await $p.stream.info(
|
||||
(async function* () {
|
||||
yield* typewriter("Now running wrangler to create a D1 database...");
|
||||
})(),
|
||||
);
|
||||
|
||||
exec(`npx wrangler d1 create ${name}`);
|
||||
await $p.stream.info(
|
||||
(async function* () {
|
||||
|
||||
@@ -72,7 +72,8 @@ export async function getConfigPath(filePath?: string) {
|
||||
}
|
||||
}
|
||||
|
||||
const paths = ["./bknd.config", "./bknd.config.ts", "./bknd.config.js"];
|
||||
const exts = ["", ".js", ".ts", ".mjs", ".cjs", ".json"];
|
||||
const paths = exts.map((e) => `bknd.config${e}`);
|
||||
for (const p of paths) {
|
||||
const _p = path.resolve(process.cwd(), p);
|
||||
if (await fileExists(_p)) {
|
||||
|
||||
@@ -7,6 +7,7 @@ import { colorizeConsole, config } from "core";
|
||||
import dotenv from "dotenv";
|
||||
import { registries } from "modules/registries";
|
||||
import c from "picocolors";
|
||||
import path from "node:path";
|
||||
import {
|
||||
PLATFORMS,
|
||||
type Platform,
|
||||
@@ -15,8 +16,12 @@ import {
|
||||
getConnectionCredentialsFromEnv,
|
||||
startServer,
|
||||
} from "./platform";
|
||||
import { makeConfig } from "adapter";
|
||||
|
||||
dotenv.config();
|
||||
const env_files = [".env", ".dev.vars"];
|
||||
dotenv.config({
|
||||
path: env_files.map((file) => path.resolve(process.cwd(), file)),
|
||||
});
|
||||
const isBun = typeof Bun !== "undefined";
|
||||
|
||||
export const run: CliCommand = (program) => {
|
||||
@@ -85,24 +90,12 @@ async function makeApp(config: MakeAppConfig) {
|
||||
return app;
|
||||
}
|
||||
|
||||
export async function makeConfigApp(config: CliBkndConfig, platform?: Platform) {
|
||||
const appConfig = typeof config.app === "function" ? config.app(process.env) : config.app;
|
||||
const app = App.create(appConfig);
|
||||
|
||||
app.emgr.onEvent(
|
||||
App.Events.AppBuiltEvent,
|
||||
async () => {
|
||||
await attachServeStatic(app, platform ?? "node");
|
||||
app.registerAdminController();
|
||||
|
||||
await config.onBuilt?.(app);
|
||||
},
|
||||
"sync",
|
||||
);
|
||||
|
||||
await config.beforeBuild?.(app);
|
||||
await app.build(config.buildConfig);
|
||||
return app;
|
||||
export async function makeConfigApp(_config: CliBkndConfig, platform?: Platform) {
|
||||
const config = makeConfig(_config, { env: process.env });
|
||||
return makeApp({
|
||||
...config,
|
||||
server: { platform },
|
||||
});
|
||||
}
|
||||
|
||||
async function action(options: {
|
||||
@@ -118,19 +111,31 @@ async function action(options: {
|
||||
const configFilePath = await getConfigPath(options.config);
|
||||
|
||||
let app: App | undefined = undefined;
|
||||
// first start from arguments if given
|
||||
if (options.dbUrl) {
|
||||
console.info("Using connection from", c.cyan("--db-url"));
|
||||
const connection = options.dbUrl
|
||||
? { url: options.dbUrl, authToken: options.dbToken }
|
||||
: undefined;
|
||||
app = await makeApp({ connection, server: { platform: options.server } });
|
||||
|
||||
// check configuration file to be present
|
||||
} else if (configFilePath) {
|
||||
console.info("Using config from", c.cyan(configFilePath));
|
||||
const config = (await import(configFilePath).then((m) => m.default)) as CliBkndConfig;
|
||||
app = await makeConfigApp(config, options.server);
|
||||
try {
|
||||
const config = (await import(configFilePath).then((m) => m.default)) as CliBkndConfig;
|
||||
app = await makeConfigApp(config, options.server);
|
||||
} catch (e) {
|
||||
console.error("Failed to load config:", e);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// try to use an in-memory connection
|
||||
} else if (options.memory) {
|
||||
console.info("Using", c.cyan("in-memory"), "connection");
|
||||
app = await makeApp({ server: { platform: options.server } });
|
||||
|
||||
// finally try to use env variables
|
||||
} else {
|
||||
const credentials = getConnectionCredentialsFromEnv();
|
||||
if (credentials) {
|
||||
@@ -139,6 +144,7 @@ async function action(options: {
|
||||
}
|
||||
}
|
||||
|
||||
// if nothing helps, create a file based app
|
||||
if (!app) {
|
||||
const connection = { url: "file:data.db" } as Config;
|
||||
console.info("Using connection", c.cyan(connection.url));
|
||||
|
||||
Vendored
+2
-5
@@ -1,12 +1,9 @@
|
||||
import type { CreateAppConfig } from "App";
|
||||
import type { FrameworkBkndConfig } from "adapter";
|
||||
import type { BkndConfig } from "adapter";
|
||||
import type { Command } from "commander";
|
||||
|
||||
export type CliCommand = (program: Command) => void;
|
||||
|
||||
export type CliBkndConfig<Env = any> = FrameworkBkndConfig & {
|
||||
app: CreateAppConfig | ((env: Env) => CreateAppConfig);
|
||||
setAdminHtml?: boolean;
|
||||
export type CliBkndConfig<Env = any> = BkndConfig & {
|
||||
server?: {
|
||||
port?: number;
|
||||
platform?: "node" | "bun";
|
||||
|
||||
+41
-15
@@ -65,27 +65,53 @@ function __tty(_type: any, args: any[]) {
|
||||
}
|
||||
|
||||
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);
|
||||
export const $console = new Proxy(
|
||||
{},
|
||||
{
|
||||
get: (_, prop) => {
|
||||
if (prop === "original") {
|
||||
export const $console = new Proxy(config as any, {
|
||||
get: (_, prop) => {
|
||||
switch (prop) {
|
||||
case "original":
|
||||
return console;
|
||||
}
|
||||
case "setLevel":
|
||||
return (l: TConsoleSeverity) => {
|
||||
config.level = l;
|
||||
};
|
||||
case "resetLevel":
|
||||
return () => {
|
||||
config.level = defaultLevel;
|
||||
};
|
||||
}
|
||||
|
||||
const current = keys.indexOf(level as string);
|
||||
const requested = keys.indexOf(prop as string);
|
||||
if (prop in __consoles && requested <= current) {
|
||||
return (...args: any[]) => __tty(prop, args);
|
||||
}
|
||||
return () => null;
|
||||
},
|
||||
const current = keys.indexOf(config.level);
|
||||
const requested = keys.indexOf(prop as string);
|
||||
|
||||
if (prop in __consoles && requested <= current) {
|
||||
return (...args: any[]) => __tty(prop, args);
|
||||
}
|
||||
return () => null;
|
||||
},
|
||||
) as typeof console & {
|
||||
}) as typeof console & {
|
||||
original: typeof console;
|
||||
} & {
|
||||
setLevel: (l: TConsoleSeverity) => void;
|
||||
resetLevel: () => void;
|
||||
};
|
||||
|
||||
export function colorizeConsole(con: typeof console) {
|
||||
|
||||
@@ -22,6 +22,7 @@ export class EventManager<
|
||||
protected events: EventClass[] = [];
|
||||
protected listeners: EventListener[] = [];
|
||||
enabled: boolean = true;
|
||||
protected asyncs: (() => Promise<void>)[] = [];
|
||||
|
||||
constructor(
|
||||
events?: RegisteredEvents,
|
||||
@@ -29,7 +30,6 @@ export class EventManager<
|
||||
listeners?: EventListener[];
|
||||
onError?: (event: Event, e: unknown) => void;
|
||||
onInvalidReturn?: (event: Event, e: InvalidEventReturn) => void;
|
||||
asyncExecutor?: typeof Promise.all;
|
||||
},
|
||||
) {
|
||||
if (events) {
|
||||
@@ -176,9 +176,15 @@ export class EventManager<
|
||||
this.events.forEach((event) => this.onEvent(event, handler, config));
|
||||
}
|
||||
|
||||
protected executeAsyncs(promises: (() => Promise<void>)[]) {
|
||||
const executor = this.options?.asyncExecutor ?? ((e) => Promise.all(e));
|
||||
executor(promises.map((p) => p())).then(() => void 0);
|
||||
protected collectAsyncs(promises: (() => Promise<void>)[]) {
|
||||
this.asyncs.push(...promises);
|
||||
}
|
||||
|
||||
async executeAsyncs(executor: typeof Promise.all = (e) => Promise.all(e)): Promise<void> {
|
||||
if (this.asyncs.length === 0) return;
|
||||
const asyncs = [...this.asyncs];
|
||||
this.asyncs = [];
|
||||
await executor(asyncs.map((p) => p()));
|
||||
}
|
||||
|
||||
async emit<Actual extends Event<any, any>>(event: Actual): Promise<Actual> {
|
||||
@@ -209,8 +215,8 @@ export class EventManager<
|
||||
return !listener.once;
|
||||
});
|
||||
|
||||
// execute asyncs
|
||||
this.executeAsyncs(asyncs);
|
||||
// collect asyncs
|
||||
this.collectAsyncs(asyncs);
|
||||
|
||||
// execute syncs
|
||||
let _event: Actual = event;
|
||||
|
||||
@@ -5,6 +5,8 @@ export type Matcher<T = unknown> = {
|
||||
toBeString: (failMsg?: string) => void;
|
||||
toBeOneOf: (expected: T | Array<T> | Iterable<T>, 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 interface Test {
|
||||
@@ -15,6 +17,7 @@ export interface Test {
|
||||
}
|
||||
export type TestRunner = {
|
||||
test: Test;
|
||||
mock: <T extends (...args: any[]) => any>(fn: T) => T | any;
|
||||
expect: <T = unknown>(
|
||||
actual?: T,
|
||||
failMsg?: string,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { $console } from "core";
|
||||
|
||||
type ConsoleSeverity = "log" | "warn" | "error";
|
||||
const _oldConsoles = {
|
||||
log: console.log,
|
||||
@@ -34,21 +36,14 @@ export function disableConsoleLog(severities: ConsoleSeverity[] = ["log", "warn"
|
||||
severities.forEach((severity) => {
|
||||
console[severity] = () => null;
|
||||
});
|
||||
return enableConsoleLog;
|
||||
$console.setLevel("error");
|
||||
}
|
||||
|
||||
export function enableConsoleLog() {
|
||||
Object.entries(_oldConsoles).forEach(([severity, fn]) => {
|
||||
console[severity as ConsoleSeverity] = fn;
|
||||
});
|
||||
}
|
||||
|
||||
export function tryit(fn: () => void, fallback?: any) {
|
||||
try {
|
||||
return fn();
|
||||
} catch (e) {
|
||||
return fallback || e;
|
||||
}
|
||||
$console.resetLevel();
|
||||
}
|
||||
|
||||
export function formatMemoryUsage() {
|
||||
|
||||
@@ -6,6 +6,7 @@ import { type DatabaseIntrospector, Kysely, ParseJSONResultsPlugin } from "kysel
|
||||
import type { QB } from "../Connection";
|
||||
import { SqliteConnection } from "./SqliteConnection";
|
||||
import { SqliteIntrospector } from "./SqliteIntrospector";
|
||||
import { $console } from "core";
|
||||
|
||||
export const LIBSQL_PROTOCOLS = ["wss", "https", "libsql"] as const;
|
||||
export type LibSqlCredentials = Config & {
|
||||
@@ -33,6 +34,7 @@ export class LibsqlConnection extends SqliteConnection {
|
||||
constructor(credentials: LibSqlCredentials);
|
||||
constructor(clientOrCredentials: Client | LibSqlCredentials) {
|
||||
let client: Client;
|
||||
let batching_enabled = true;
|
||||
if (clientOrCredentials && "url" in clientOrCredentials) {
|
||||
let { url, authToken, protocol } = clientOrCredentials;
|
||||
if (protocol && LIBSQL_PROTOCOLS.includes(protocol)) {
|
||||
@@ -42,6 +44,13 @@ export class LibsqlConnection extends SqliteConnection {
|
||||
}
|
||||
|
||||
client = createClient({ url, authToken });
|
||||
|
||||
// currently there is an issue in limbo implementation
|
||||
// that prevents batching from working correctly
|
||||
if (/\.aws.*turso\.io$/.test(url)) {
|
||||
$console.warn("Using an Turso AWS endpoint currently disables batching support");
|
||||
batching_enabled = false;
|
||||
}
|
||||
} else {
|
||||
client = clientOrCredentials;
|
||||
}
|
||||
@@ -54,6 +63,7 @@ export class LibsqlConnection extends SqliteConnection {
|
||||
|
||||
super(kysely, {}, plugins);
|
||||
this.client = client;
|
||||
this.supported.batching = batching_enabled;
|
||||
}
|
||||
|
||||
getClient(): Client {
|
||||
|
||||
@@ -27,9 +27,9 @@ export type RepositoryResponse<T = EntityData[]> = RepositoryRawResponse & {
|
||||
entity: Entity;
|
||||
data: T;
|
||||
meta: {
|
||||
total: number;
|
||||
count: number;
|
||||
items: number;
|
||||
total?: number;
|
||||
count?: number;
|
||||
time?: number;
|
||||
query?: {
|
||||
sql: string;
|
||||
@@ -47,6 +47,7 @@ export type RepositoryExistsResponse = RepositoryRawResponse & {
|
||||
|
||||
export type RepositoryOptions = {
|
||||
silent?: boolean;
|
||||
includeCounts?: boolean;
|
||||
emgr?: EventManager<any>;
|
||||
};
|
||||
|
||||
@@ -59,13 +60,17 @@ export class Repository<TBD extends object = DefaultDB, TB extends keyof TBD = a
|
||||
constructor(
|
||||
public em: EntityManager<TBD>,
|
||||
public entity: Entity,
|
||||
protected options?: RepositoryOptions,
|
||||
protected options: RepositoryOptions = {},
|
||||
) {
|
||||
this.emgr = options?.emgr ?? new EventManager(MutatorEvents);
|
||||
}
|
||||
|
||||
private cloneFor(entity: Entity) {
|
||||
return new Repository(this.em, this.em.entity(entity), { emgr: this.emgr });
|
||||
private cloneFor(entity: Entity, opts: Partial<RepositoryOptions> = {}) {
|
||||
return new Repository(this.em, this.em.entity(entity), {
|
||||
...this.options,
|
||||
...opts,
|
||||
emgr: this.emgr,
|
||||
});
|
||||
}
|
||||
|
||||
private get conn() {
|
||||
@@ -172,28 +177,39 @@ export class Repository<TBD extends object = DefaultDB, TB extends keyof TBD = a
|
||||
if (options.limit) validated.limit = options.limit;
|
||||
if (options.offset) validated.offset = options.offset;
|
||||
|
||||
//$console.debug("Repository: options", { entity: entity.name, options, validated });
|
||||
|
||||
return validated;
|
||||
}
|
||||
|
||||
protected async performQuery(qb: RepositoryQB): Promise<RepositoryResponse> {
|
||||
const entity = this.entity;
|
||||
protected async executeQb(qb: RepositoryQB) {
|
||||
const compiled = qb.compile();
|
||||
if (this.options?.silent !== true) {
|
||||
$console.debug(`Repository: query\n${compiled.sql}\n`, compiled.parameters);
|
||||
}
|
||||
|
||||
const start = performance.now();
|
||||
const selector = (as = "count") => this.conn.fn.countAll<number>().as(as);
|
||||
const countQuery = qb
|
||||
.clearSelect()
|
||||
.select(selector())
|
||||
.clearLimit()
|
||||
.clearOffset()
|
||||
.clearGroupBy()
|
||||
.clearOrderBy();
|
||||
const totalQuery = this.conn.selectFrom(entity.name).select(selector("total"));
|
||||
let result: any;
|
||||
try {
|
||||
result = await qb.execute();
|
||||
} catch (e) {
|
||||
if (this.options?.silent !== true) {
|
||||
if (e instanceof Error) {
|
||||
$console.error("[ERROR] Repository.executeQb", e.message);
|
||||
}
|
||||
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
result,
|
||||
sql: compiled.sql,
|
||||
parameters: [...compiled.parameters],
|
||||
};
|
||||
}
|
||||
|
||||
protected async performQuery(qb: RepositoryQB): Promise<RepositoryResponse> {
|
||||
const entity = this.entity;
|
||||
const compiled = qb.compile();
|
||||
|
||||
const payload = {
|
||||
entity,
|
||||
sql: compiled.sql,
|
||||
@@ -209,13 +225,53 @@ export class Repository<TBD extends object = DefaultDB, TB extends keyof TBD = a
|
||||
},
|
||||
};
|
||||
|
||||
// don't batch (add counts) if `includeCounts` is set to false
|
||||
// or when explicitly set to true and batching is not supported
|
||||
if (
|
||||
this.options?.includeCounts === false ||
|
||||
(this.options?.includeCounts === true && !this.em.connection.supports("batching"))
|
||||
) {
|
||||
const start = performance.now();
|
||||
const res = await this.executeQb(qb);
|
||||
const time = Number.parseFloat((performance.now() - start).toFixed(2));
|
||||
const result = res.result ?? [];
|
||||
const data = this.em.hydrate(entity.name, result);
|
||||
|
||||
return {
|
||||
...payload,
|
||||
result,
|
||||
data,
|
||||
meta: {
|
||||
...payload.meta,
|
||||
total: undefined,
|
||||
count: undefined,
|
||||
items: data.length,
|
||||
time,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (this.options?.silent !== true) {
|
||||
$console.debug(`Repository: query\n${compiled.sql}\n`, compiled.parameters);
|
||||
}
|
||||
|
||||
const selector = (as = "count") => this.conn.fn.countAll<number>().as(as);
|
||||
const countQuery = qb
|
||||
.clearSelect()
|
||||
.select(selector())
|
||||
.clearLimit()
|
||||
.clearOffset()
|
||||
.clearGroupBy()
|
||||
.clearOrderBy();
|
||||
const totalQuery = this.conn.selectFrom(entity.name).select(selector());
|
||||
|
||||
try {
|
||||
const start = performance.now();
|
||||
const [_count, _total, result] = await this.em.connection.batchQuery([
|
||||
countQuery,
|
||||
totalQuery,
|
||||
qb,
|
||||
]);
|
||||
//$console.log("result", { _count, _total });
|
||||
|
||||
const time = Number.parseFloat((performance.now() - start).toFixed(2));
|
||||
const data = this.em.hydrate(entity.name, result);
|
||||
@@ -227,7 +283,7 @@ export class Repository<TBD extends object = DefaultDB, TB extends keyof TBD = a
|
||||
meta: {
|
||||
...payload.meta,
|
||||
// parsing is important since pg returns string
|
||||
total: ensureInt(_total[0]?.total),
|
||||
total: ensureInt(_total[0]?.count),
|
||||
count: ensureInt(_count[0]?.count),
|
||||
items: result.length,
|
||||
time,
|
||||
@@ -435,8 +491,7 @@ export class Repository<TBD extends object = DefaultDB, TB extends keyof TBD = a
|
||||
qb = WhereBuilder.addClause(qb, options.where);
|
||||
}
|
||||
|
||||
const compiled = qb.compile();
|
||||
const result = await qb.execute();
|
||||
const { result, ...compiled } = await this.executeQb(qb);
|
||||
|
||||
return {
|
||||
sql: compiled.sql,
|
||||
@@ -454,15 +509,9 @@ export class Repository<TBD extends object = DefaultDB, TB extends keyof TBD = a
|
||||
let qb = this.conn.selectFrom(entity.name).select(selector);
|
||||
|
||||
// add mandatory where
|
||||
qb = WhereBuilder.addClause(qb, options.where);
|
||||
qb = WhereBuilder.addClause(qb, options.where).limit(1);
|
||||
|
||||
// we only need 1
|
||||
qb = qb.limit(1);
|
||||
|
||||
const compiled = qb.compile();
|
||||
//$console.log("exists query", compiled.sql, compiled.parameters);
|
||||
const result = await qb.execute();
|
||||
//$console.log("result", result);
|
||||
const { result, ...compiled } = await this.executeQb(qb);
|
||||
|
||||
return {
|
||||
sql: compiled.sql,
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { ModuleConfigs } from "modules";
|
||||
import {
|
||||
BooleanField,
|
||||
type BooleanFieldConfig,
|
||||
type Connection,
|
||||
DateField,
|
||||
type DateFieldConfig,
|
||||
Entity,
|
||||
@@ -171,8 +172,6 @@ export class FieldPrototype {
|
||||
}
|
||||
}
|
||||
|
||||
//type Entity<Fields extends Record<string, Field<any, any>> = {}> = { name: string; fields: Fields };
|
||||
|
||||
export function entity<
|
||||
EntityName extends string,
|
||||
Fields extends Record<string, Field<any, any, any>>,
|
||||
@@ -270,6 +269,10 @@ class EntityManagerPrototype<Entities extends Record<string, Entity>> extends En
|
||||
) {
|
||||
super(Object.values(__entities), new DummyConnection(), relations, indices);
|
||||
}
|
||||
|
||||
withConnection(connection: Connection): EntityManager<Schema<Entities>> {
|
||||
return new EntityManager(this.entities, connection, this.relations.all, this.indices);
|
||||
}
|
||||
}
|
||||
|
||||
type Chained<R extends Record<string, (...args: any[]) => any>> = {
|
||||
@@ -326,6 +329,7 @@ export function em<Entities extends Record<string, Entity>>(
|
||||
entities: e.__entities,
|
||||
relations,
|
||||
indices,
|
||||
proto: e,
|
||||
toJSON: () =>
|
||||
e.toJSON() as unknown as Pick<ModuleConfigs["data"], "entities" | "relations" | "indices">,
|
||||
};
|
||||
|
||||
@@ -4,6 +4,7 @@ import { config } from "dotenv";
|
||||
// @ts-ignore
|
||||
import { assetsPath, assetsTmpPath } from "../../../../../__test__/helper";
|
||||
import { adapterTestSuite } from "media/storage/adapters/adapter-test-suite";
|
||||
import { bunTestRunner } from "adapter/bun/test";
|
||||
|
||||
const dotenvOutput = config({ path: `${import.meta.dir}/.env` });
|
||||
const {
|
||||
@@ -43,7 +44,7 @@ describe.skipIf(ALL_TESTS)("StorageCloudinaryAdapter", async () => {
|
||||
});
|
||||
});
|
||||
|
||||
await adapterTestSuite({ test, expect }, adapter, file, {
|
||||
await adapterTestSuite(bunTestRunner, adapter, file, {
|
||||
// eventual consistency
|
||||
retries: 20,
|
||||
retryTimeout: 1000,
|
||||
|
||||
@@ -4,6 +4,7 @@ import { StorageS3Adapter } from "./StorageS3Adapter";
|
||||
import { config } from "dotenv";
|
||||
import { adapterTestSuite } from "media";
|
||||
import { assetsPath } from "../../../../../__test__/helper";
|
||||
import { bunTestRunner } from "adapter/bun/test";
|
||||
//import { enableFetchLogging } from "../../helper";
|
||||
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 } =
|
||||
@@ -45,6 +46,6 @@ describe.skipIf(ALL_TESTS)("StorageS3Adapter", async () => {
|
||||
const file = Bun.file(`${assetsPath}/image.png`) as unknown as File;
|
||||
|
||||
describe.each(versions)("%s", async (_name, adapter) => {
|
||||
await adapterTestSuite({ test, expect }, adapter, file);
|
||||
await adapterTestSuite(bunTestRunner, adapter, file);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,7 +19,7 @@ export function getRandomizedFilename(file: File | string, length = 16): string
|
||||
}
|
||||
|
||||
let ext = getExtensionFromName(filename);
|
||||
if (isFile(file) && file.type) {
|
||||
if (!ext && isFile(file) && file.type) {
|
||||
const _ext = extension(file.type);
|
||||
if (_ext.length > 0) ext = _ext;
|
||||
}
|
||||
|
||||
@@ -222,6 +222,8 @@ export class ModuleManager {
|
||||
return this.__em.repo(__bknd, {
|
||||
// to prevent exceptions when table doesn't exist
|
||||
silent: true,
|
||||
// disable counts for performance and compatibility
|
||||
includeCounts: false,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -66,15 +66,15 @@ const Skeleton = ({ theme }: { theme?: any }) => {
|
||||
<Logo theme={actualTheme} />
|
||||
</div>
|
||||
<nav className="hidden md:flex flex-row gap-2.5 pl-0 p-2.5 items-center">
|
||||
{[...new Array(5)].map((item, key) => (
|
||||
{[...new Array(4)].map((item, key) => (
|
||||
<AppShell.NavLink key={key} as="span" className="active h-full opacity-50">
|
||||
<div className="w-10 h-3" />
|
||||
<div className="w-18 h-3" />
|
||||
</AppShell.NavLink>
|
||||
))}
|
||||
</nav>
|
||||
<nav className="flex md:hidden flex-row items-center">
|
||||
<nav className="flex md:hidden flex-row gap-2.5 pl-0 p-2.5 items-center">
|
||||
<AppShell.NavLink as="span" className="active h-full opacity-50">
|
||||
<div className="w-10 h-3" />
|
||||
<div className="w-20 h-3" />
|
||||
</AppShell.NavLink>
|
||||
</nav>
|
||||
<div className="flex flex-grow" />
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Form } from "json-schema-form-react";
|
||||
import { transform } from "lodash-es";
|
||||
import type { ComponentPropsWithoutRef } from "react";
|
||||
import { Button } from "ui/components/buttons/Button";
|
||||
import { Group, Input, Label } from "ui/components/form/Formy/components";
|
||||
import { Group, Input, Password, Label } from "ui/components/form/Formy/components";
|
||||
import { SocialLink } from "./SocialLink";
|
||||
import type { ValueError } from "@sinclair/typebox/value";
|
||||
import { type TSchema, Value } from "core/utils";
|
||||
@@ -99,7 +99,7 @@ export function AuthForm({
|
||||
</Group>
|
||||
<Group>
|
||||
<Label htmlFor="password">Password</Label>
|
||||
<Input type="password" name="password" />
|
||||
<Password name="password" />
|
||||
</Group>
|
||||
|
||||
<Button
|
||||
|
||||
@@ -44,8 +44,7 @@ export function MediaInfoModal({
|
||||
return (
|
||||
<div className="flex flex-col md:flex-row">
|
||||
<div className="flex w-full md:w-[calc(100%-300px)] justify-center items-center bg-lightest min-w-0">
|
||||
{/* @ts-ignore */}
|
||||
<Media.Preview file={file} className="max-h-[70dvh]" controls muted />
|
||||
<FilePreview file={file} />
|
||||
</div>
|
||||
<div className="w-full md:!w-[300px] flex flex-col">
|
||||
<Item title="ID" value={data?.id} copyValue={origin + mediaUrl} first>
|
||||
@@ -157,6 +156,19 @@ const Item = ({
|
||||
);
|
||||
};
|
||||
|
||||
const FilePreview = ({ file }: { file: FileState }) => {
|
||||
if (file.type.startsWith("image/") || file.type.startsWith("video/")) {
|
||||
// @ts-ignore
|
||||
return <Media.Preview file={file} className="max-h-[70dvh]" controls muted />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-w-96 min-h-48 flex justify-center items-center h-full max-h-[70dvh]">
|
||||
<span className="opacity-50 font-mono">No Preview Available</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
MediaInfoModal.defaultTitle = undefined;
|
||||
MediaInfoModal.modalProps = {
|
||||
withCloseButton: false,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"include": ["./src/**/*.ts", "./src/**/*.tsx"]
|
||||
"include": ["./src/**/*.ts", "./src/**/*.tsx"],
|
||||
"exclude": ["./node_modules", "./__test__"]
|
||||
}
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ import { readFile } from "node:fs/promises";
|
||||
import { serveStatic } from "@hono/node-server/serve-static";
|
||||
import { showRoutes } from "hono/dev";
|
||||
import { App, registries } from "./src";
|
||||
import { StorageLocalAdapter } from "./src/adapter/node/storage/StorageLocalAdapter";
|
||||
import { StorageLocalAdapter } from "./src/adapter/node";
|
||||
import { EntityManager, LibsqlConnection } from "data";
|
||||
import { __bknd } from "modules/ModuleManager";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user