mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-15 20:17:22 +00:00
added fallback route to server, created extensive setup instructions in docs
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
"bin": "./dist/cli/index.js",
|
||||
"version": "0.4.0-rc2",
|
||||
"version": "0.4.0-rc3",
|
||||
"scripts": {
|
||||
"build:all": "NODE_ENV=production bun run build.ts --minify --types --clean && bun run build:cli",
|
||||
"dev": "vite",
|
||||
|
||||
@@ -10,7 +10,7 @@ import * as SystemPermissions from "modules/permissions";
|
||||
import { AdminController, type AdminControllerOptions } from "modules/server/AdminController";
|
||||
import { SystemController } from "modules/server/SystemController";
|
||||
|
||||
export type AppPlugin = (app: App) => void;
|
||||
export type AppPlugin = (app: App) => Promise<void> | void;
|
||||
|
||||
abstract class AppEvent<A = {}> extends Event<{ app: App } & A> {}
|
||||
export class AppConfigUpdatedEvent extends AppEvent {
|
||||
@@ -93,7 +93,7 @@ export class App {
|
||||
|
||||
// load plugins
|
||||
if (this.plugins.length > 0) {
|
||||
this.plugins.forEach((plugin) => plugin(this));
|
||||
await Promise.all(this.plugins.map((plugin) => plugin(this)));
|
||||
}
|
||||
|
||||
//console.log("emitting built", options);
|
||||
|
||||
@@ -7,7 +7,6 @@ export async function getCached(config: CloudflareBkndConfig, { env, ctx }: Cont
|
||||
if (!kv) throw new Error("kv namespace is not defined in cloudflare.bindings");
|
||||
const key = config.key ?? "app";
|
||||
|
||||
const create_config = typeof config.app === "function" ? config.app(env) : config.app;
|
||||
const cachedConfig = await kv.get(key);
|
||||
const initialConfig = cachedConfig ? JSON.parse(cachedConfig) : undefined;
|
||||
|
||||
@@ -15,8 +14,9 @@ export async function getCached(config: CloudflareBkndConfig, { env, ctx }: Cont
|
||||
ctx.waitUntil(kv!.put(key, JSON.stringify(__config)));
|
||||
}
|
||||
|
||||
const app = await createRuntimeApp({
|
||||
...create_config,
|
||||
const app = await createRuntimeApp(
|
||||
{
|
||||
...config,
|
||||
initialConfig,
|
||||
onBuilt: async (app) => {
|
||||
app.module.server.client.get("/__bknd/cache", async (c) => {
|
||||
@@ -36,7 +36,9 @@ export async function getCached(config: CloudflareBkndConfig, { env, ctx }: Cont
|
||||
await config.beforeBuild?.(app);
|
||||
},
|
||||
adminOptions: { html: config.html }
|
||||
});
|
||||
},
|
||||
env
|
||||
);
|
||||
|
||||
if (!cachedConfig) {
|
||||
saveConfig(app.toJSON(true));
|
||||
|
||||
@@ -3,12 +3,13 @@ import type { App } from "bknd";
|
||||
import type { CloudflareBkndConfig, Context } from "../index";
|
||||
|
||||
export async function makeApp(config: CloudflareBkndConfig, { env }: Context) {
|
||||
const create_config = typeof config.app === "function" ? config.app(env) : config.app;
|
||||
return await createRuntimeApp({
|
||||
return await createRuntimeApp(
|
||||
{
|
||||
...config,
|
||||
...create_config,
|
||||
adminOptions: config.html ? { html: config.html } : undefined
|
||||
});
|
||||
},
|
||||
env
|
||||
);
|
||||
}
|
||||
|
||||
export async function getFresh(config: CloudflareBkndConfig, ctx: Context) {
|
||||
|
||||
@@ -4,15 +4,16 @@ import type { MiddlewareHandler } from "hono";
|
||||
import { StorageLocalAdapter } from "media/storage/adapters/StorageLocalAdapter";
|
||||
import type { AdminControllerOptions } from "modules/server/AdminController";
|
||||
|
||||
type BaseExternalBkndConfig = CreateAppConfig & {
|
||||
export type BkndConfig<Env = any> = CreateAppConfig & {
|
||||
app?: CreateAppConfig | ((env: Env) => CreateAppConfig);
|
||||
onBuilt?: (app: App) => Promise<void>;
|
||||
beforeBuild?: (app: App) => Promise<void>;
|
||||
buildConfig?: Parameters<App["build"]>[0];
|
||||
};
|
||||
|
||||
export type FrameworkBkndConfig = BaseExternalBkndConfig;
|
||||
export type FrameworkBkndConfig<Env = any> = BkndConfig<Env>;
|
||||
|
||||
export type RuntimeBkndConfig = BaseExternalBkndConfig & {
|
||||
export type RuntimeBkndConfig<Env = any> = BkndConfig<Env> & {
|
||||
distPath?: string;
|
||||
};
|
||||
|
||||
@@ -44,8 +45,27 @@ export function registerLocalMediaAdapter() {
|
||||
registries.media.register("local", StorageLocalAdapter);
|
||||
}
|
||||
|
||||
export async function createFrameworkApp(config: FrameworkBkndConfig): Promise<App> {
|
||||
const app = App.create(config);
|
||||
export function makeConfig<Env = any>(config: BkndConfig<Env>, env?: Env): CreateAppConfig {
|
||||
let additionalConfig: CreateAppConfig = {};
|
||||
if ("app" in config && config.app) {
|
||||
if (typeof config.app === "function") {
|
||||
if (!env) {
|
||||
throw new Error("env is required when config.app is a function");
|
||||
}
|
||||
additionalConfig = config.app(env);
|
||||
} else {
|
||||
additionalConfig = config.app;
|
||||
}
|
||||
}
|
||||
|
||||
return { ...config, ...additionalConfig };
|
||||
}
|
||||
|
||||
export async function createFrameworkApp<Env = any>(
|
||||
config: FrameworkBkndConfig,
|
||||
env?: Env
|
||||
): Promise<App> {
|
||||
const app = App.create(makeConfig(config, env));
|
||||
|
||||
if (config.onBuilt) {
|
||||
app.emgr.onEvent(
|
||||
@@ -63,21 +83,24 @@ export async function createFrameworkApp(config: FrameworkBkndConfig): Promise<A
|
||||
return app;
|
||||
}
|
||||
|
||||
export async function createRuntimeApp({
|
||||
export async function createRuntimeApp<Env = any>(
|
||||
{
|
||||
serveStatic,
|
||||
registerLocalMedia,
|
||||
adminOptions,
|
||||
...config
|
||||
}: RuntimeBkndConfig & {
|
||||
}: RuntimeBkndConfig & {
|
||||
serveStatic?: MiddlewareHandler | [string, MiddlewareHandler];
|
||||
registerLocalMedia?: boolean;
|
||||
adminOptions?: AdminControllerOptions | false;
|
||||
}): Promise<App> {
|
||||
},
|
||||
env?: Env
|
||||
): Promise<App> {
|
||||
if (registerLocalMedia) {
|
||||
registerLocalMediaAdapter();
|
||||
}
|
||||
|
||||
const app = App.create(config);
|
||||
const app = App.create(makeConfig(config, env));
|
||||
|
||||
app.emgr.onEvent(
|
||||
App.Events.AppBuiltEvent,
|
||||
|
||||
@@ -1,24 +1,40 @@
|
||||
import { serveStatic } from "@hono/node-server/serve-static";
|
||||
import { type RuntimeBkndConfig, createRuntimeApp } from "adapter";
|
||||
import type { CreateAppConfig } from "bknd";
|
||||
import type { App } from "bknd";
|
||||
|
||||
export type ViteBkndConfig<Env = any> = RuntimeBkndConfig & {
|
||||
app: CreateAppConfig | ((env: Env) => CreateAppConfig);
|
||||
export type ViteBkndConfig<Env = any> = RuntimeBkndConfig<Env> & {
|
||||
setAdminHtml?: boolean;
|
||||
forceDev?: boolean;
|
||||
html?: string;
|
||||
};
|
||||
|
||||
async function createApp(config: ViteBkndConfig, env: any) {
|
||||
const create_config = typeof config.app === "function" ? config.app(env) : config.app;
|
||||
return await createRuntimeApp({
|
||||
...create_config,
|
||||
export function addViteScript(html: string, addBkndContext: boolean = true) {
|
||||
return html.replace(
|
||||
"</head>",
|
||||
`<script type="module">
|
||||
import RefreshRuntime from "/@react-refresh"
|
||||
RefreshRuntime.injectIntoGlobalHook(window)
|
||||
window.$RefreshReg$ = () => {}
|
||||
window.$RefreshSig$ = () => (type) => type
|
||||
window.__vite_plugin_react_preamble_installed__ = true
|
||||
</script>
|
||||
<script type="module" src="/@vite/client"></script>
|
||||
${addBkndContext ? "<!-- BKND_CONTEXT -->" : ""}
|
||||
</head>`
|
||||
);
|
||||
}
|
||||
|
||||
async function createApp(config: ViteBkndConfig, env?: any) {
|
||||
return await createRuntimeApp(
|
||||
{
|
||||
...config,
|
||||
adminOptions: config.setAdminHtml
|
||||
? { html: config.html, forceDev: config.forceDev }
|
||||
: undefined,
|
||||
serveStatic: ["/assets/*", serveStatic({ root: config.distPath ?? "./" })]
|
||||
});
|
||||
},
|
||||
env
|
||||
);
|
||||
}
|
||||
|
||||
export async function serveFresh(config: ViteBkndConfig) {
|
||||
|
||||
@@ -74,6 +74,21 @@ export class AppServer extends Module<typeof serverConfigSchema> {
|
||||
})
|
||||
);
|
||||
|
||||
// add an initial fallback route
|
||||
this.client.use("/", async (c, next) => {
|
||||
await next();
|
||||
// if not finalized or giving a 404
|
||||
if (!c.finalized || c.res.status === 404) {
|
||||
// double check it's root
|
||||
if (new URL(c.req.url).pathname === "/") {
|
||||
c.res = undefined;
|
||||
c.res = Response.json({
|
||||
bknd: "hello world!"
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.client.onError((err, c) => {
|
||||
//throw err;
|
||||
console.error(err);
|
||||
@@ -82,21 +97,6 @@ export class AppServer extends Module<typeof serverConfigSchema> {
|
||||
return err;
|
||||
}
|
||||
|
||||
/*if (isDebug()) {
|
||||
console.log("accept", c.req.header("Accept"));
|
||||
if (c.req.header("Accept") === "application/json") {
|
||||
const stack = err.stack;
|
||||
|
||||
if ("toJSON" in err && typeof err.toJSON === "function") {
|
||||
return c.json({ ...err.toJSON(), stack }, 500);
|
||||
}
|
||||
|
||||
return c.json({ message: String(err), stack }, 500);
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}*/
|
||||
|
||||
if (err instanceof Exception) {
|
||||
console.log("---is exception", err.code);
|
||||
return c.json(err.toJSON(), err.code as any);
|
||||
@@ -107,32 +107,6 @@ export class AppServer extends Module<typeof serverConfigSchema> {
|
||||
this.setBuilt();
|
||||
}
|
||||
|
||||
/*setAdminHtml(html: string) {
|
||||
this.admin_html = html;
|
||||
const basepath = (String(this.config.admin.basepath) + "/").replace(/\/+$/, "/");
|
||||
|
||||
const allowed_prefix = basepath + "auth";
|
||||
const login_path = basepath + "auth/login";
|
||||
|
||||
this.client.get(basepath + "*", async (c, next) => {
|
||||
const path = new URL(c.req.url).pathname;
|
||||
if (!path.startsWith(allowed_prefix)) {
|
||||
console.log("guard check permissions");
|
||||
try {
|
||||
this.ctx.guard.throwUnlessGranted(SystemPermissions.admin);
|
||||
} catch (e) {
|
||||
return c.redirect(login_path);
|
||||
}
|
||||
}
|
||||
|
||||
return c.html(this.admin_html!);
|
||||
});
|
||||
}
|
||||
|
||||
getAdminHtml() {
|
||||
return this.admin_html;
|
||||
}*/
|
||||
|
||||
override toJSON(secrets?: boolean) {
|
||||
return this.config;
|
||||
}
|
||||
|
||||
@@ -45,12 +45,14 @@ export const ALL = serve({
|
||||
connection: {
|
||||
type: "libsql",
|
||||
config: {
|
||||
url: "file:data.db"
|
||||
// location of your local Astro DB
|
||||
// make sure to use a remote URL in production
|
||||
url: "file:.astro/content.db"
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
For more information about the connection object, refer to the [Setup](/setup) guide. In the
|
||||
For more information about the connection object, refer to the [Setup](/setup/introduction) guide. In the
|
||||
special case of astro, you may also use your Astro DB credentials since it's also using LibSQL
|
||||
under the hood. Refer to the [Astro DB documentation](https://docs.astro.build/en/guides/astro-db/) for more information.
|
||||
|
||||
@@ -73,7 +75,11 @@ export const prerender = false;
|
||||
<body>
|
||||
<Admin
|
||||
withProvider={{ user }}
|
||||
config={{ basepath: "/admin", color_scheme: "dark" }}
|
||||
config={{
|
||||
basepath: "/admin",
|
||||
color_scheme: "dark",
|
||||
logo_return_path: "/../"
|
||||
}}
|
||||
client:only
|
||||
/>
|
||||
</body>
|
||||
|
||||
@@ -27,7 +27,7 @@ serve({
|
||||
}
|
||||
});
|
||||
```
|
||||
For more information about the connection object, refer to the [Setup](/setup) guide.
|
||||
For more information about the connection object, refer to the [Setup](/setup/introduction) guide.
|
||||
|
||||
Run the application using Bun by executing:
|
||||
```bash
|
||||
|
||||
@@ -31,7 +31,7 @@ export default serve({
|
||||
})
|
||||
});
|
||||
```
|
||||
For more information about the connection object, refer to the [Setup](/setup) guide.
|
||||
For more information about the connection object, refer to the [Setup](/setup/introduction) guide.
|
||||
|
||||
Now run the worker:
|
||||
```bash
|
||||
@@ -64,8 +64,9 @@ export default serve({
|
||||
}
|
||||
}
|
||||
}),
|
||||
manifest,
|
||||
setAdminHtml: true
|
||||
}, manifest);
|
||||
});
|
||||
```
|
||||
|
||||
## Adding custom routes
|
||||
@@ -87,8 +88,9 @@ export default serve({
|
||||
onBuilt: async (app) => {
|
||||
app.modules.server.get("/hello", (c) => c.json({ hello: "world" }));
|
||||
},
|
||||
manifest,
|
||||
setAdminHtml: true
|
||||
}, manifest);
|
||||
});
|
||||
```
|
||||
|
||||
## Using a different mode
|
||||
@@ -109,11 +111,8 @@ mode`, like so:
|
||||
import { serve } from "bknd/adapter/cloudflare";
|
||||
|
||||
export default serve({
|
||||
app: (env: Env) => ({ /* ... */ }),
|
||||
cloudflare: {
|
||||
mode: "fresh"
|
||||
// mode: "warm"
|
||||
}
|
||||
/* ... */,
|
||||
mode: "fresh" // mode: "fresh" | "warm" | "cache" | "durable"
|
||||
});
|
||||
```
|
||||
|
||||
@@ -124,11 +123,9 @@ For the cache mode to work, you also need to specify the KV to be used. For this
|
||||
import { serve } from "bknd/adapter/cloudflare";
|
||||
|
||||
export default serve({
|
||||
app: (env: Env) => ({ /* ... */ }),
|
||||
cloudflare: {
|
||||
/* ... */,
|
||||
mode: "cache",
|
||||
bindings: (env: Env) => ({ kv: env.KV })
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
@@ -140,12 +137,10 @@ import { serve, DurableBkndApp } from "bknd/adapter/cloudflare";
|
||||
|
||||
export { DurableBkndApp };
|
||||
export default serve({
|
||||
app: (env: Env) => ({ /* ... */ }),
|
||||
cloudflare: {
|
||||
/* ... */,
|
||||
mode: "durable",
|
||||
bindings: (env: Env) => ({ dobj: env.DOBJ }),
|
||||
keepAliveSeconds: 60 // optional
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
@@ -169,12 +164,10 @@ import type { App } from "bknd";
|
||||
import { serve, DurableBkndApp } from "bknd/adapter/cloudflare";
|
||||
|
||||
export default serve({
|
||||
app: (env: Env) => ({ /* ... */ }),
|
||||
cloudflare: {
|
||||
/* ... */,
|
||||
mode: "durable",
|
||||
bindings: (env: Env) => ({ dobj: env.DOBJ }),
|
||||
keepAliveSeconds: 60 // optional
|
||||
}
|
||||
});
|
||||
|
||||
export class CustomDurableBkndApp extends DurableBkndApp {
|
||||
|
||||
@@ -14,7 +14,7 @@ Install bknd as a dependency:
|
||||
import { serve } from "bknd/adapter/nextjs";
|
||||
|
||||
export const config = {
|
||||
runtime: "experimental-edge",
|
||||
runtime: "experimental-edge", // or "edge", depending on your nextjs version
|
||||
unstable_allowDynamic: ["**/*.js"]
|
||||
};
|
||||
|
||||
@@ -28,12 +28,13 @@ export default serve({
|
||||
}
|
||||
});
|
||||
```
|
||||
For more information about the connection object, refer to the [Setup](/setup) guide.
|
||||
For more information about the connection object, refer to the [Setup](/setup/introduction) guide.
|
||||
|
||||
## Enabling the Admin UI
|
||||
Create a file `[[...admin]].tsx` inside the `pages/admin` folder:
|
||||
```tsx
|
||||
// pages/admin/[[...admin]].tsx
|
||||
import type { InferGetServerSidePropsType as InferProps } from "next";
|
||||
import { withApi } from "bknd/adapter/nextjs";
|
||||
import dynamic from "next/dynamic";
|
||||
import "bknd/dist/styles.css";
|
||||
@@ -50,9 +51,12 @@ export const getServerSideProps = withApi(async (context) => {
|
||||
};
|
||||
});
|
||||
|
||||
export default function AdminPage() {
|
||||
export default function AdminPage({ user }: InferProps<typeof getServerSideProps>) {
|
||||
if (typeof document === "undefined") return null;
|
||||
return <Admin withProvider config={{ basepath: "/admin" }} />;
|
||||
return <Admin
|
||||
withProvider={{ user }}
|
||||
config={{ basepath: "/admin", logo_return_path: "/../" }}
|
||||
/>;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ const config = {
|
||||
|
||||
serve(config);
|
||||
```
|
||||
For more information about the connection object, refer to the [Setup](/setup) guide.
|
||||
For more information about the connection object, refer to the [Setup](/setup/introduction) guide.
|
||||
|
||||
Run the application using node by executing:
|
||||
```bash
|
||||
|
||||
@@ -26,7 +26,7 @@ const handler = serve({
|
||||
export const loader = handler;
|
||||
export const action = handler;
|
||||
```
|
||||
For more information about the connection object, refer to the [Setup](/setup) guide.
|
||||
For more information about the connection object, refer to the [Setup](/setup/introduction) guide.
|
||||
|
||||
Now make sure that you wrap your root layout with the `ClientProvider` so that all components
|
||||
share the same context:
|
||||
|
||||
@@ -61,7 +61,11 @@
|
||||
"navigation": [
|
||||
{
|
||||
"group": "Getting Started",
|
||||
"pages": ["introduction", "setup", "sdk", "react", "cli"]
|
||||
"pages": ["introduction", "sdk", "react", "cli"]
|
||||
},
|
||||
{
|
||||
"group": "Setup",
|
||||
"pages": ["setup/introduction", "setup/database"]
|
||||
},
|
||||
{
|
||||
"group": "Modules",
|
||||
@@ -74,39 +78,17 @@
|
||||
"modules/flows"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Configuration",
|
||||
"pages": [
|
||||
"config/overview",
|
||||
"config/migration",
|
||||
{
|
||||
"group": "Modules",
|
||||
"pages": [
|
||||
"config/modules/overview",
|
||||
"config/modules/server",
|
||||
"config/modules/data",
|
||||
"config/modules/auth",
|
||||
"config/modules/flows",
|
||||
"config/modules/media"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Integration",
|
||||
"pages": [
|
||||
"integration/extending",
|
||||
"integration/hono",
|
||||
"integration/nextjs",
|
||||
"integration/remix",
|
||||
"integration/cloudflare",
|
||||
"integration/bun",
|
||||
"integration/vite",
|
||||
"integration/express",
|
||||
"integration/astro",
|
||||
"integration/node",
|
||||
"integration/deno",
|
||||
"integration/browser",
|
||||
"integration/docker"
|
||||
]
|
||||
},
|
||||
|
||||
183
docs/setup/database.mdx
Normal file
183
docs/setup/database.mdx
Normal file
@@ -0,0 +1,183 @@
|
||||
---
|
||||
title: 'Database'
|
||||
description: 'Choosing the right database configuration'
|
||||
---
|
||||
|
||||
In order to use **bknd**, you need to prepare access information to your database and install
|
||||
the dependencies.
|
||||
|
||||
<Note>
|
||||
Connections to the database are managed using Kysely. Therefore, all its dialects are
|
||||
theoretically supported. However, only the `SQLite` dialect is implemented as of now.
|
||||
</Note>
|
||||
|
||||
## Database
|
||||
### SQLite as file
|
||||
The easiest to get started is using SQLite as a file. When serving the API in the "Integrations",
|
||||
the function accepts an object with connection details. To use a file, use the following:
|
||||
```json
|
||||
{
|
||||
"type": "libsql",
|
||||
"config": {
|
||||
"url": "file:<path/to/your/database.db>"
|
||||
}
|
||||
}
|
||||
```
|
||||
Please note that using SQLite as a file is only supported in server environments.
|
||||
|
||||
### SQLite using LibSQL
|
||||
Turso offers a SQLite-fork called LibSQL that runs a server around your SQLite database. To
|
||||
point **bknd** to a local instance of LibSQL, [install Turso's CLI](https://docs.turso.tech/cli/introduction) and run the following command:
|
||||
```bash
|
||||
turso dev
|
||||
```
|
||||
|
||||
The command will yield a URL. Use it in the connection object:
|
||||
```json
|
||||
{
|
||||
"type": "libsql",
|
||||
"config": {
|
||||
"url": "http://localhost:8080"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### SQLite using LibSQL on Turso
|
||||
If you want to use LibSQL on Turso, [sign up for a free account](https://turso.tech/), create a database and point your
|
||||
connection object to your new database:
|
||||
```json
|
||||
{
|
||||
"type": "libsql",
|
||||
"config": {
|
||||
"url": "libsql://your-database-url.turso.io",
|
||||
"authToken": "your-auth-token"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Connection
|
||||
<Note>
|
||||
Follow the progress of custom connections on its [Github Issue](https://github.com/bknd-io/bknd/issues/24).
|
||||
If you're interested, make sure to upvote so it can be prioritized.
|
||||
</Note>
|
||||
|
||||
Any bknd app instantiation accepts as connection either `undefined`, a connection object like
|
||||
described above, or an class instance that extends from `Connection`:
|
||||
|
||||
```ts
|
||||
import { createApp } from "bknd";
|
||||
import { Connection } from "bknd/data";
|
||||
|
||||
class CustomConnection extends Connection {
|
||||
constructor() {
|
||||
const kysely = new Kysely(/* ... */);
|
||||
super(kysely);
|
||||
}
|
||||
}
|
||||
|
||||
const connection = new CustomConnection();
|
||||
|
||||
// e.g. and then, create an instance
|
||||
const app = createApp({ connection })
|
||||
```
|
||||
|
||||
## Initial Structure
|
||||
To provide an initial database structure, you can pass `initialConfig` to the creation of an app. This will only be used if there isn't an existing configuration found in the database given. Here is a quick example:
|
||||
|
||||
```ts
|
||||
import { em, entity, text, number } from "bknd/data";
|
||||
|
||||
const schema = em({
|
||||
posts: entity("posts", {
|
||||
// "id" is automatically added
|
||||
title: text().required(),
|
||||
slug: text().required(),
|
||||
content: text(),
|
||||
views: number()
|
||||
}),
|
||||
comments: entity("comments", {
|
||||
content: text()
|
||||
})
|
||||
|
||||
// relations and indices are defined separately.
|
||||
// the first argument are the helper functions, the second the entities.
|
||||
}, ({ relation, index }, { posts, comments }) => {
|
||||
relation(comments).manyToOne(posts);
|
||||
// relation as well as index can be chained!
|
||||
index(posts).on(["title"]).on(["slug"], true);
|
||||
});
|
||||
|
||||
// to get a type from your schema, use:
|
||||
type Database = (typeof schema)["DB"];
|
||||
// type Database = {
|
||||
// posts: {
|
||||
// id: number;
|
||||
// title: string;
|
||||
// content: string;
|
||||
// views: number;
|
||||
// },
|
||||
// comments: {
|
||||
// id: number;
|
||||
// content: string;
|
||||
// }
|
||||
// }
|
||||
|
||||
// pass the schema to the app
|
||||
const app = createApp({
|
||||
connection: { /* ... */ },
|
||||
initialConfig: {
|
||||
data: schema.toJSON()
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
Note that we didn't add relational fields directly to the entity, but instead defined them afterwards. That is because the relations are managed outside the entity scope to have an unified expierence for all kinds of relations (e.g. many-to-many).
|
||||
|
||||
<Note>
|
||||
Defined relations are currently not part of the produced types for the structure. We're working on that, but in the meantime, you can define them manually.
|
||||
</Note>
|
||||
|
||||
### Type completion
|
||||
All entity related functions use the types defined in `DB` from `bknd/core`. To get type completion, you can extend that interface with your own schema:
|
||||
|
||||
```ts
|
||||
import { em } from "bknd/data";
|
||||
import { Api } from "bknd";
|
||||
|
||||
// const schema = em({ ... });
|
||||
|
||||
type Database = (typeof schema)["DB"];
|
||||
declare module "bknd/core" {
|
||||
interface DB extends Database {}
|
||||
}
|
||||
|
||||
const api = new Api({ /* ... */ });
|
||||
const { data: posts } = await api.data.readMany("posts", {})
|
||||
// `posts` is now typed as Database["posts"]
|
||||
```
|
||||
|
||||
The type completion is available for the API as well as all provided [React hooks](/react).
|
||||
|
||||
### Seeding the database
|
||||
To seed your database with initial data, you can pass a `seed` function to the configuration. It
|
||||
provides the `ModuleBuildContext` ([reference](/setup/introduction#modulebuildcontext)) as the first argument.
|
||||
|
||||
Note that the seed function will only be executed on app's first boot. If a configuration
|
||||
already exists in the database, it will not be executed.
|
||||
|
||||
```ts
|
||||
import { createApp, type ModuleBuildContext } from "bknd";
|
||||
|
||||
const app = createApp({
|
||||
connection: { /* ... */ },
|
||||
initialConfig: { /* ... */ },
|
||||
options: {
|
||||
seed: async (ctx: ModuleBuildContext) => {
|
||||
await ctx.em.mutator("posts").insertMany([
|
||||
{ title: "First post", slug: "first-post", content: "..." },
|
||||
{ title: "Second post", slug: "second-post" }
|
||||
]);
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
205
docs/setup/introduction.mdx
Normal file
205
docs/setup/introduction.mdx
Normal file
@@ -0,0 +1,205 @@
|
||||
---
|
||||
title: 'Introduction'
|
||||
description: 'Setting up bknd'
|
||||
---
|
||||
|
||||
There are several methods to get **bknd** up and running. You can choose between these options:
|
||||
1. [Run it using the CLI](/cli): That's the easiest and fastest way to get started.
|
||||
2. Use a runtime like [Node](/integration/node), [Bun](/integration/bun) or
|
||||
[Cloudflare](/integration/cloudflare) (workerd). This will run the API and UI in the runtime's
|
||||
native server and serves the UI assets statically from `node_modules`.
|
||||
3. Run it inside your React framework of choice like [Next.js](/integration/nextjs),
|
||||
[Astro](/integration/astro) or [Remix](/integration/remix).
|
||||
|
||||
There is also a fourth option, which is running it inside a
|
||||
[Docker container](/integration/docker). This is essentially a wrapper around the CLI.
|
||||
|
||||
## Basic setup
|
||||
Regardless of the method you choose, at the end all adapters come down to the actual
|
||||
instantiation of the `App`, which in raw looks like this:
|
||||
|
||||
```ts
|
||||
import { createApp, type CreateAppConfig } from "bknd";
|
||||
|
||||
// create the app
|
||||
const config = { /* ... */ } satisfies CreateAppConfig;
|
||||
const app = createApp(config);
|
||||
|
||||
// build the app
|
||||
await app.build();
|
||||
|
||||
// export for Web API compliant envs
|
||||
export default app;
|
||||
```
|
||||
|
||||
In Web API compliant environments, all you have to do is to default exporting the app, as it
|
||||
implements the `Fetch` API.
|
||||
|
||||
## Configuration (`CreateAppConfig`)
|
||||
The `CreateAppConfig` type is the main configuration object for the `createApp` function. It has
|
||||
the following properties:
|
||||
```ts
|
||||
import type { Connection } from "bknd/data";
|
||||
import type { Config } from "@libsql/client";
|
||||
|
||||
type AppPlugin = (app: App) => Promise<void> | void;
|
||||
type LibSqlCredentials = Config;
|
||||
|
||||
type CreateAppConfig = {
|
||||
connection?:
|
||||
| Connection
|
||||
| {
|
||||
type: "libsql";
|
||||
config: LibSqlCredentials;
|
||||
};
|
||||
initialConfig?: InitialModuleConfigs;
|
||||
plugins?: AppPlugin[];
|
||||
options?: {
|
||||
basePath?: string;
|
||||
trustFetched?: boolean;
|
||||
onFirstBoot?: () => Promise<void>;
|
||||
seed?: (ctx: ModuleBuildContext) => Promise<void>;
|
||||
};
|
||||
};
|
||||
```
|
||||
### `connection`
|
||||
The `connection` property is the main connection object to the database. It can be either an
|
||||
object with a type specifier (only `libsql` is supported at the moment) and the actual
|
||||
`Connection` class. The `libsql` connection object looks like this:
|
||||
|
||||
```ts
|
||||
const connection = {
|
||||
type: "libsql",
|
||||
config: {
|
||||
url: string;
|
||||
authToken?: string;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
Alternatively, you can pass an instance of a `Connection` class directly,
|
||||
see [Custom Connection](/setup/database#custom-connection) as a reference.
|
||||
|
||||
If the connection object is omitted, the app will try to use an in-memory database.
|
||||
|
||||
### `initialConfig`
|
||||
As initial configuration, you can either pass a partial configuration object or a complete one
|
||||
with a version number. The version number is used to automatically migrate the configuration up
|
||||
to the latest version upon boot. The default configuration looks like this:
|
||||
|
||||
```json
|
||||
{
|
||||
"server": {
|
||||
"admin": {
|
||||
"basepath": "",
|
||||
"color_scheme": "light",
|
||||
"logo_return_path": "/"
|
||||
},
|
||||
"cors": {
|
||||
"origin": "*",
|
||||
"allow_methods": ["GET", "POST", "PATCH", "PUT", "DELETE" ],
|
||||
"allow_headers": ["Content-Type", "Content-Length", "Authorization", "Accept"]
|
||||
}
|
||||
},
|
||||
"data": {
|
||||
"basepath": "/api/data",
|
||||
"entities": {},
|
||||
"relations": {},
|
||||
"indices": {}
|
||||
},
|
||||
"auth": {
|
||||
"enabled": false,
|
||||
"basepath": "/api/auth",
|
||||
"entity_name": "users",
|
||||
"allow_register": true,
|
||||
"jwt": {
|
||||
"secret": "",
|
||||
"alg": "HS256",
|
||||
"fields": ["id", "email", "role"]
|
||||
},
|
||||
"cookie": {
|
||||
"path": "/",
|
||||
"sameSite": "lax",
|
||||
"secure": true,
|
||||
"httpOnly": true,
|
||||
"expires": 604800,
|
||||
"renew": true,
|
||||
"pathSuccess": "/",
|
||||
"pathLoggedOut": "/"
|
||||
},
|
||||
"strategies": {
|
||||
"password": {
|
||||
"type": "password",
|
||||
"config": {
|
||||
"hashing": "sha256"
|
||||
}
|
||||
}
|
||||
},
|
||||
"roles": {}
|
||||
},
|
||||
"media": {
|
||||
"enabled": false,
|
||||
"basepath": "/api/media",
|
||||
"entity_name": "media",
|
||||
"storage": {}
|
||||
},
|
||||
"flows": {
|
||||
"basepath": "/api/flows",
|
||||
"flows": {}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
You can use the CLI to get the default configuration:
|
||||
```sh
|
||||
npx bknd config --pretty
|
||||
```
|
||||
|
||||
To validate your configuration against a JSON schema, you can also dump the schema using the CLI:
|
||||
```sh
|
||||
npx bknd schema
|
||||
```
|
||||
|
||||
To create an initial data structure, you can use helpers [described here](/setup/database#initial-structure).
|
||||
|
||||
### `plugins`
|
||||
The `plugins` property is an array of functions that are called after the app has been built,
|
||||
but before its event is emitted. This is useful for adding custom routes or other functionality.
|
||||
A simple plugin that adds a custom route looks like this:
|
||||
|
||||
```ts
|
||||
export const myPlugin: AppPlugin = (app) => {
|
||||
app.server.get("/hello", (c) => c.json({ hello: "world" }));
|
||||
};
|
||||
```
|
||||
|
||||
Since each plugin has full access to the `app` instance, it can add routes, modify the database
|
||||
structure, add custom middlewares, respond to or add events, etc. Plugins are very powerful, so
|
||||
make sure to only run trusted ones.
|
||||
|
||||
### `options`
|
||||
This object is passed to the `ModuleManager` which is responsible for:
|
||||
- validating and maintaining configuration of all modules
|
||||
- building all modules (data, auth, media, flows)
|
||||
- maintaining the `ModuleBuildContext` used by the modules
|
||||
|
||||
The `options` object has the following properties:
|
||||
- `basePath` (`string`): The base path for the Hono instance. This is used to prefix all routes.
|
||||
- `trustFetched` (`boolean`): If set to `true`, the app will not perform any validity checks for
|
||||
the given or fetched configuration.
|
||||
- `onFirstBoot` (`() => Promise<void>`): A function that is called when the app is booted for
|
||||
the first time.
|
||||
- `seed` (`(ctx: ModuleBuildContext) => Promise<void>`): A function that is called when the app is
|
||||
booted for the first time and an initial partial configuration is provided.
|
||||
|
||||
|
||||
## `ModuleBuildContext`
|
||||
```ts
|
||||
type ModuleBuildContext = {
|
||||
connection: Connection;
|
||||
server: Hono;
|
||||
em: EntityManager;
|
||||
emgr: EventManager;
|
||||
guard: Guard;
|
||||
};
|
||||
```
|
||||
@@ -1,11 +1,11 @@
|
||||
// @ts-ignore somehow causes types:build issues on app
|
||||
import { type BunAdapterOptions, serve } from "bknd/adapter/bun";
|
||||
import { type BunBkndConfig, serve } from "bknd/adapter/bun";
|
||||
|
||||
// Actually, all it takes is the following line:
|
||||
// serve();
|
||||
|
||||
// this is optional, if omitted, it uses an in-memory database
|
||||
const config: BunAdapterOptions = {
|
||||
const config: BunBkndConfig = {
|
||||
connection: {
|
||||
type: "libsql",
|
||||
config: {
|
||||
|
||||
6
examples/bun/minimal.ts
Normal file
6
examples/bun/minimal.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { createApp } from "bknd";
|
||||
|
||||
const app = createApp();
|
||||
await app.build();
|
||||
|
||||
export default app;
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { InferGetServerSidePropsType } from "next";
|
||||
import type { InferGetServerSidePropsType as InferProps } from "next";
|
||||
import dynamic from "next/dynamic";
|
||||
|
||||
import { withApi } from "bknd/adapter/nextjs";
|
||||
@@ -16,9 +16,7 @@ export const getServerSideProps = withApi(async (context) => {
|
||||
};
|
||||
});
|
||||
|
||||
export default function AdminPage({
|
||||
user
|
||||
}: InferGetServerSidePropsType<typeof getServerSideProps>) {
|
||||
export default function AdminPage({ user }: InferProps<typeof getServerSideProps>) {
|
||||
if (typeof document === "undefined") return null;
|
||||
return (
|
||||
<Admin withProvider={{ user }} config={{ basepath: "/admin", logo_return_path: "/../" }} />
|
||||
|
||||
@@ -1,25 +1,22 @@
|
||||
// pages/admin/[[...admin]].tsx
|
||||
import { withApi } from "bknd/adapter/nextjs";
|
||||
// pages/admin/[[...admin]].tsx
|
||||
import type { InferGetServerSidePropsType as InferProps } from "next";
|
||||
import dynamic from "next/dynamic";
|
||||
import "bknd/dist/styles.css";
|
||||
|
||||
/*export const config = {
|
||||
runtime: "experimental-edge"
|
||||
}*/
|
||||
|
||||
const Admin = dynamic(() => import("bknd/ui").then((mod) => mod.Admin), {
|
||||
ssr: false,
|
||||
ssr: false
|
||||
});
|
||||
|
||||
export const getServerSideProps = withApi(async (context) => {
|
||||
return {
|
||||
props: {
|
||||
user: context.api.getUser(),
|
||||
},
|
||||
user: context.api.getUser()
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
export default function AdminPage() {
|
||||
export default function AdminPage({ user }: InferProps<typeof getServerSideProps>) {
|
||||
if (typeof document === "undefined") return null;
|
||||
return <Admin withProvider config={{ basepath: "/admin" }} />;
|
||||
return <Admin withProvider={{ user }} config={{ basepath: "/admin" }} />;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user