Compare commits

...

9 Commits

Author SHA1 Message Date
dswbx 9ecfcb3e05 Merge pull request #91 from bknd-io/release/0.8.1
Fix Release 0.8.1
2025-02-21 08:18:32 +01:00
dswbx fb3dbd343b bump fix release version to 0.8.1 2025-02-21 08:18:13 +01:00
dswbx 060df210f9 fix Api import in App 2025-02-21 08:14:22 +01:00
dswbx fb725be0ed dev: only report env once 2025-02-21 07:44:07 +01:00
dswbx 1a95b4c8bb Merge remote-tracking branch 'origin/release/0.8.1' into release/0.8.1
# Conflicts:
#	app/src/ui/elements/media/Dropzone.tsx
2025-02-21 07:41:45 +01:00
dswbx cfa950aa46 fix media upload without name, add data result to uploaded payload 2025-02-21 07:40:43 +01:00
dswbx 5771298bfa Merge pull request #90 from bknd-io/fix/media-mime-guessing-on-entity
fix media mime guessing when uploading to entity
2025-02-20 11:34:36 +01:00
dswbx 94c40de011 fix media mime guessing when uploading to entity 2025-02-20 11:32:24 +01:00
dswbx eaa7276173 update docs on new cli starters 2025-02-19 21:27:04 +01:00
26 changed files with 264 additions and 102 deletions
+4 -4
View File
@@ -18,10 +18,10 @@ bknd simplifies app development by providing a fully functional backend for data
> and therefore full backward compatibility is not guaranteed before reaching v1.0.0. > and therefore full backward compatibility is not guaranteed before reaching v1.0.0.
## Size ## Size
![gzipped size of bknd](https://img.badgesize.io/https://unpkg.com/bknd@0.8.0/dist/index.js?compression=gzip&label=bknd) ![gzipped size of bknd](https://img.badgesize.io/https://unpkg.com/bknd@0.7.1/dist/index.js?compression=gzip&label=bknd)
![gzipped size of bknd/client](https://img.badgesize.io/https://unpkg.com/bknd@0.8.0/dist/ui/client/index.js?compression=gzip&label=bknd/client) ![gzipped size of bknd/client](https://img.badgesize.io/https://unpkg.com/bknd@0.7.1/dist/ui/client/index.js?compression=gzip&label=bknd/client)
![gzipped size of bknd/elements](https://img.badgesize.io/https://unpkg.com/bknd@0.8.0/dist/ui/elements/index.js?compression=gzip&label=bknd/elements) ![gzipped size of bknd/elements](https://img.badgesize.io/https://unpkg.com/bknd@0.7.1/dist/ui/elements/index.js?compression=gzip&label=bknd/elements)
![gzipped size of bknd/ui](https://img.badgesize.io/https://unpkg.com/bknd@0.8.0/dist/ui/index.js?compression=gzip&label=bknd/ui) ![gzipped size of bknd/ui](https://img.badgesize.io/https://unpkg.com/bknd@0.7.1/dist/ui/index.js?compression=gzip&label=bknd/ui)
The size on npm is misleading, as the `bknd` package includes the backend, the ui components as well as the whole backend bundled into the cli including static assets. The size on npm is misleading, as the `bknd` package includes the backend, the ui components as well as the whole backend bundled into the cli including static assets.
+1
View File
@@ -111,6 +111,7 @@ describe("EventManager", async () => {
emgr.onEvent(ReturnEvent, async () => "1", "sync"); emgr.onEvent(ReturnEvent, async () => "1", "sync");
emgr.onEvent(ReturnEvent, async () => "0", "sync"); emgr.onEvent(ReturnEvent, async () => "0", "sync");
// @todo: fix this
// @ts-expect-error must be string // @ts-expect-error must be string
emgr.onEvent(ReturnEvent, async () => 0, "sync"); emgr.onEvent(ReturnEvent, async () => 0, "sync");
+17
View File
@@ -34,4 +34,21 @@ describe("media/mime-types", () => {
} }
} }
}); });
test("isMimeType", () => {
expect(tiny.isMimeType("image/jpeg")).toBe(true);
expect(tiny.isMimeType("image/jpeg", ["image/png"])).toBe(true);
expect(tiny.isMimeType("image/png", ["image/png"])).toBe(false);
expect(tiny.isMimeType("image/png")).toBe(true);
expect(tiny.isMimeType("whatever")).toBe(false);
expect(tiny.isMimeType("text/tab-separated-values")).toBe(true);
});
test("extension", () => {
expect(tiny.extension("image/png")).toBe("png");
expect(tiny.extension("image/jpeg")).toBe("jpeg");
expect(tiny.extension("application/zip")).toBe("zip");
expect(tiny.extension("text/tab-separated-values")).toBe("tsv");
expect(tiny.extension("application/zip")).toBe("zip");
});
}); });
+3 -3
View File
@@ -3,7 +3,7 @@
"type": "module", "type": "module",
"sideEffects": false, "sideEffects": false,
"bin": "./dist/cli/index.js", "bin": "./dist/cli/index.js",
"version": "0.8.0", "version": "0.8.1",
"description": "Lightweight Firebase/Supabase alternative built to run anywhere — incl. Next.js, Remix, Astro, Cloudflare, Bun, Node, AWS Lambda & more.", "description": "Lightweight Firebase/Supabase alternative built to run anywhere — incl. Next.js, Remix, Astro, Cloudflare, Bun, Node, AWS Lambda & more.",
"homepage": "https://bknd.io", "homepage": "https://bknd.io",
"repository": { "repository": {
@@ -22,9 +22,9 @@
"build:cli": "bun build src/cli/index.ts --target node --outdir dist/cli --minify", "build:cli": "bun build src/cli/index.ts --target node --outdir dist/cli --minify",
"build:static": "vite build", "build:static": "vite build",
"watch": "bun run build.ts --types --watch", "watch": "bun run build.ts --types --watch",
"types": "bun tsc --noEmit", "types": "bun tsc -p tsconfig.build.json --noEmit",
"clean:types": "find ./dist -name '*.d.ts' -delete && rm -f ./dist/tsconfig.tsbuildinfo", "clean:types": "find ./dist -name '*.d.ts' -delete && rm -f ./dist/tsconfig.tsbuildinfo",
"build:types": "tsc --emitDeclarationOnly && tsc-alias", "build:types": "tsc -p tsconfig.build.json --emitDeclarationOnly && tsc-alias",
"updater": "bun x npm-check-updates -ui", "updater": "bun x npm-check-updates -ui",
"cli": "LOCAL=1 bun src/cli/index.ts", "cli": "LOCAL=1 bun src/cli/index.ts",
"prepublishOnly": "bun run types && bun run test && bun run build:all && cp ../README.md ./", "prepublishOnly": "bun run types && bun run test && bun run build:all && cp ../README.md ./",
+1 -1
View File
@@ -1,5 +1,5 @@
import { Api, type ApiOptions } from "Api";
import type { CreateUserPayload } from "auth/AppAuth"; import type { CreateUserPayload } from "auth/AppAuth";
import { Api, type ApiOptions } from "bknd/client";
import { $console } from "core"; import { $console } from "core";
import { Event } from "core/events"; import { Event } from "core/events";
import { Connection, type LibSqlCredentials, LibsqlConnection } from "data"; import { Connection, type LibSqlCredentials, LibsqlConnection } from "data";
+1 -1
View File
@@ -220,7 +220,7 @@ export class DataController extends Controller {
return c.notFound(); return c.notFound();
} }
const where = c.req.json() as any; const where = (await c.req.json()) as any;
const result = await this.em.repository(entity).count(where); const result = await this.em.repository(entity).count(where);
return c.json({ entity, count: result.count }); return c.json({ entity, count: result.count });
} }
+2 -1
View File
@@ -124,8 +124,9 @@ export class AppMedia extends Module<typeof mediaConfigSchema> {
const mutator = em.mutator(media); const mutator = em.mutator(media);
mutator.__unstable_toggleSystemEntityCreation(false); mutator.__unstable_toggleSystemEntityCreation(false);
const payload = this.uploadedEventDataToMediaPayload(e.params); const payload = this.uploadedEventDataToMediaPayload(e.params);
await mutator.insertOne(payload); const { data } = await mutator.insertOne(payload);
mutator.__unstable_toggleSystemEntityCreation(true); mutator.__unstable_toggleSystemEntityCreation(true);
return { data };
}, },
{ mode: "sync", id: "add-data-media" } { mode: "sync", id: "add-data-media" }
); );
+2 -1
View File
@@ -44,7 +44,8 @@ export class MediaApi extends ModuleApi<MediaApiOptions> {
return (await res.blob()) as File; return (await res.blob()) as File;
} }
getFileUploadUrl(file: FileWithPath): string { getFileUploadUrl(file?: FileWithPath): string {
if (!file) return this.getUrl("/upload");
return this.getUrl(`/upload/${file.path}`); return this.getUrl(`/upload/${file.path}`);
} }
+6 -7
View File
@@ -72,11 +72,8 @@ export class MediaController extends Controller {
// upload file // upload file
// @todo: add required type for "upload endpoints" // @todo: add required type for "upload endpoints"
hono.post("/upload/:filename", async (c) => { hono.post("/upload/:filename?", async (c) => {
const { filename } = c.req.param(); const reqname = c.req.param("filename");
if (!filename) {
throw new Error("No file name provided");
}
const body = await getFileFromContext(c); const body = await getFileFromContext(c);
if (!body) { if (!body) {
@@ -89,7 +86,9 @@ export class MediaController extends Controller {
); );
} }
const filename = reqname ?? getRandomizedFilename(body as File);
const res = await this.getStorage().uploadFile(body, filename); const res = await this.getStorage().uploadFile(body, filename);
return c.json(res, HttpStatus.CREATED); return c.json(res, HttpStatus.CREATED);
}); });
@@ -191,8 +190,8 @@ export class MediaController extends Controller {
); );
} }
const file_name = getRandomizedFilename(file as File); const filename = getRandomizedFilename(file as File);
const info = await this.getStorage().uploadFile(file, file_name, true); const info = await this.getStorage().uploadFile(file, filename, true);
const mutator = this.media.em.mutator(media_entity); const mutator = this.media.em.mutator(media_entity);
mutator.__unstable_toggleSystemEntityCreation(false); mutator.__unstable_toggleSystemEntityCreation(false);
+4 -1
View File
@@ -119,7 +119,10 @@ export class Storage implements EmitsEvents {
} }
}; };
if (!noEmit) { if (!noEmit) {
await this.emgr.emit(new StorageEvents.FileUploadedEvent(eventData)); const result = await this.emgr.emit(new StorageEvents.FileUploadedEvent(eventData));
if (result.returned) {
return result.params;
}
} }
return eventData; return eventData;
+14 -2
View File
@@ -1,11 +1,23 @@
import { Event } from "core/events"; import { Event, InvalidEventReturn } from "core/events";
import type { FileBody, FileUploadPayload } from "../Storage"; import type { FileBody, FileUploadPayload } from "../Storage";
export type FileUploadedEventData = FileUploadPayload & { export type FileUploadedEventData = FileUploadPayload & {
file: FileBody; file: FileBody;
}; };
export class FileUploadedEvent extends Event<FileUploadedEventData> { export class FileUploadedEvent extends Event<FileUploadedEventData, object> {
static override slug = "file-uploaded"; static override slug = "file-uploaded";
override validate(data: object) {
if (typeof data !== "object") {
throw new InvalidEventReturn("object", typeof data);
}
return this.clone({
// prepending result, so original is always kept
...data,
...this.params
});
}
} }
export class FileDeletedEvent extends Event<{ name: string }> { export class FileDeletedEvent extends Event<{ name: string }> {
+19
View File
@@ -77,6 +77,17 @@ export function guess(f: string): string {
} }
export function isMimeType(mime: any, exclude: string[] = []) { export function isMimeType(mime: any, exclude: string[] = []) {
if (exclude.includes(mime)) return false;
// try quick first
if (
Object.entries(Q)
.flatMap(([t, e]) => e.map((x) => `${t}/${x}`))
.includes(mime)
) {
return true;
}
for (const [k, v] of M.entries()) { for (const [k, v] of M.entries()) {
if (v === mime && !exclude.includes(k)) { if (v === mime && !exclude.includes(k)) {
return true; return true;
@@ -86,6 +97,14 @@ export function isMimeType(mime: any, exclude: string[] = []) {
} }
export function extension(mime: string) { export function extension(mime: string) {
for (const [t, e] of Object.entries(Q)) {
for (const _e of e) {
if (mime === `${t}/${_e}`) {
return _e;
}
}
}
for (const [k, v] of M.entries()) { for (const [k, v] of M.entries()) {
if (v === mime) { if (v === mime) {
return k; return k;
+10 -3
View File
@@ -1,6 +1,7 @@
import { randomString } from "core/utils"; import { isFile, randomString } from "core/utils";
import { extension } from "media/storage/mime-types-tiny";
export function getExtension(filename: string): string | undefined { export function getExtensionFromName(filename: string): string | undefined {
if (!filename.includes(".")) return; if (!filename.includes(".")) return;
const parts = filename.split("."); const parts = filename.split(".");
@@ -17,6 +18,12 @@ export function getRandomizedFilename(file: File | string, length = 16): string
throw new Error("Invalid file name"); throw new Error("Invalid file name");
} }
let ext = getExtensionFromName(filename);
if (isFile(file) && file.type) {
const _ext = extension(file.type);
if (_ext.length > 0) ext = _ext;
}
// @todo: use uuid instead? // @todo: use uuid instead?
return [randomString(length), getExtension(filename)].filter(Boolean).join("."); return [randomString(length), ext].filter(Boolean).join(".");
} }
+1
View File
@@ -10,4 +10,5 @@ export * from "./api/use-api";
export * from "./api/use-entity"; export * from "./api/use-entity";
export { useAuth } from "./schema/auth/use-auth"; export { useAuth } from "./schema/auth/use-auth";
export { Api, type TApiUser, type AuthState, type ApiOptions } from "../../Api"; export { Api, type TApiUser, type AuthState, type ApiOptions } from "../../Api";
export { FetchPromise } from "modules/ModuleApi";
export type { RepoQueryIn } from "data"; export type { RepoQueryIn } from "data";
+19 -13
View File
@@ -1,3 +1,4 @@
import type { DB } from "core";
import { import {
type ComponentPropsWithRef, type ComponentPropsWithRef,
type ComponentPropsWithoutRef, type ComponentPropsWithoutRef,
@@ -23,6 +24,8 @@ export type FileState = {
progress: number; progress: number;
}; };
export type FileStateWithData = FileState & { data: DB["media"] };
export type DropzoneRenderProps = { export type DropzoneRenderProps = {
wrapperRef: RefObject<HTMLDivElement>; wrapperRef: RefObject<HTMLDivElement>;
inputProps: ComponentPropsWithRef<"input">; inputProps: ComponentPropsWithRef<"input">;
@@ -50,7 +53,7 @@ export type DropzoneProps = {
autoUpload?: boolean; autoUpload?: boolean;
onRejected?: (files: FileWithPath[]) => void; onRejected?: (files: FileWithPath[]) => void;
onDeleted?: (file: FileState) => void; onDeleted?: (file: FileState) => void;
onUploaded?: (files: FileState[]) => void; onUploaded?: (files: FileStateWithData[]) => void;
placeholder?: { placeholder?: {
show?: boolean; show?: boolean;
text?: string; text?: string;
@@ -172,15 +175,16 @@ export function Dropzone({
setUploading(false); setUploading(false);
return; return;
} else { } else {
const uploaded: FileStateWithData[] = [];
for (const file of pendingFiles) { for (const file of pendingFiles) {
try { try {
await uploadFileProgress(file); uploaded.push(await uploadFileProgress(file));
} catch (e) { } catch (e) {
handleUploadError(e); handleUploadError(e);
} }
} }
setUploading(false); setUploading(false);
onUploaded?.(files); onUploaded?.(uploaded);
} }
})(); })();
} }
@@ -220,8 +224,8 @@ export function Dropzone({
setFiles((prev) => prev.filter((f) => f.path !== path)); setFiles((prev) => prev.filter((f) => f.path !== path));
} }
function uploadFileProgress(file: FileState) { function uploadFileProgress(file: FileState): Promise<FileStateWithData> {
return new Promise<void>((resolve, reject) => { return new Promise((resolve, reject) => {
if (!file.body) { if (!file.body) {
console.error("File has no body"); console.error("File has no body");
reject(); reject();
@@ -279,17 +283,19 @@ export function Dropzone({
const response = JSON.parse(xhr.responseText); const response = JSON.parse(xhr.responseText);
console.log("Response:", file, response); console.log("Response:", file, response);
console.log("New state", response.state); const newState = {
replaceFileState(file.path, {
...response.state, ...response.state,
progress: 1, progress: 1,
state: "uploaded" state: "uploaded"
}); };
replaceFileState(file.path, newState);
resolve({ ...response, ...file, ...newState });
} catch (e) { } catch (e) {
setFileState(file.path, "uploaded", 1); setFileState(file.path, "uploaded", 1);
console.error("Error parsing response", e); console.error("Error parsing response", e);
reject(e);
} }
resolve();
} else { } else {
setFileState(file.path, "failed", 1); setFileState(file.path, "failed", 1);
console.error("Upload failed with status: ", xhr.status, xhr.statusText); console.error("Upload failed with status: ", xhr.status, xhr.statusText);
@@ -327,8 +333,8 @@ export function Dropzone({
} }
async function uploadFile(file: FileState) { async function uploadFile(file: FileState) {
await uploadFileProgress(file); const result = await uploadFileProgress(file);
onUploaded?.([file]); onUploaded?.([result]);
} }
const openFileInput = () => inputRef.current?.click(); const openFileInput = () => inputRef.current?.click();
@@ -496,9 +502,9 @@ const Preview: React.FC<PreviewProps> = ({ file, handleUpload, handleDelete }) =
/> />
</div> </div>
<div className="flex flex-col px-1.5 py-1"> <div className="flex flex-col px-1.5 py-1">
<p className="truncate">{file.name}</p> <p className="truncate select-text">{file.name}</p>
<div className="flex flex-row justify-between text-sm font-mono opacity-50 text-nowrap gap-2"> <div className="flex flex-row justify-between text-sm font-mono opacity-50 text-nowrap gap-2">
<span className="truncate">{file.type}</span> <span className="truncate select-text">{file.type}</span>
<span>{(file.size / 1024).toFixed(1)} KB</span> <span>{(file.size / 1024).toFixed(1)} KB</span>
</div> </div>
</div> </div>
@@ -10,7 +10,7 @@ import { mediaItemsToFileStates } from "./helper";
export type DropzoneContainerProps = { export type DropzoneContainerProps = {
children?: ReactNode; children?: ReactNode;
initialItems?: MediaFieldSchema[]; initialItems?: MediaFieldSchema[] | false;
entity?: { entity?: {
name: string; name: string;
id: number; id: number;
@@ -18,6 +18,7 @@ export type DropzoneContainerProps = {
}; };
media?: Pick<TAppMediaConfig, "entity_name" | "storage">; media?: Pick<TAppMediaConfig, "entity_name" | "storage">;
query?: RepoQueryIn; query?: RepoQueryIn;
randomFilename?: boolean;
} & Omit<Partial<DropzoneProps>, "children" | "initialItems">; } & Omit<Partial<DropzoneProps>, "children" | "initialItems">;
const DropzoneContainerContext = createContext<DropzoneRenderProps>(undefined!); const DropzoneContainerContext = createContext<DropzoneRenderProps>(undefined!);
@@ -28,6 +29,7 @@ export function DropzoneContainer({
entity, entity,
query, query,
children, children,
randomFilename,
...props ...props
}: DropzoneContainerProps) { }: DropzoneContainerProps) {
const id = useId(); const id = useId();
@@ -57,12 +59,12 @@ export function DropzoneContainer({
...query ...query
}); });
const $q = useApiQuery(selectApi, { enabled: !initialItems }); const $q = useApiQuery(selectApi, { enabled: initialItems !== false && !initialItems });
const getUploadInfo = useEvent((file) => { const getUploadInfo = useEvent((file) => {
const url = entity const url = entity
? api.media.getEntityUploadUrl(entity.name, entity.id, entity.field) ? api.media.getEntityUploadUrl(entity.name, entity.id, entity.field)
: api.media.getFileUploadUrl(file); : api.media.getFileUploadUrl(randomFilename ? undefined : file);
return { return {
url, url,
@@ -79,7 +81,7 @@ export function DropzoneContainer({
return api.media.deleteFile(file.path); return api.media.deleteFile(file.path);
}); });
const actualItems = initialItems ?? (($q.data || []) as MediaFieldSchema[]); const actualItems = (initialItems || $q.data || []) as MediaFieldSchema[];
const _initialItems = mediaItemsToFileStates(actualItems, { baseUrl }); const _initialItems = mediaItemsToFileStates(actualItems, { baseUrl });
const key = id + JSON.stringify(_initialItems); const key = id + JSON.stringify(_initialItems);
+1
View File
@@ -12,6 +12,7 @@ export { useDropzone as useMediaDropzone };
export type { export type {
PreviewComponentProps, PreviewComponentProps,
FileState, FileState,
FileStateWithData,
DropzoneProps, DropzoneProps,
DropzoneRenderProps DropzoneRenderProps
} from "./Dropzone"; } from "./Dropzone";
+4
View File
@@ -0,0 +1,4 @@
{
"extends": "./tsconfig.json",
"include": ["./src/**/*.ts", "./src/**/*.tsx"]
}
+1 -1
View File
@@ -33,6 +33,6 @@
"bknd": ["./src/*"] "bknd": ["./src/*"]
} }
}, },
"include": ["./src/**/*.ts", "./src/**/*.tsx"], "include": ["./src/**/*.ts", "./src/**/*.tsx", "vite.dev.ts", "build.ts"],
"exclude": ["node_modules", "dist", "dist/types", "**/*.d.ts"] "exclude": ["node_modules", "dist", "dist/types", "**/*.d.ts"]
} }
+4 -3
View File
@@ -29,7 +29,7 @@ if (example) {
let app: App; let app: App;
const recreate = import.meta.env.VITE_APP_DISABLE_FRESH !== "1"; const recreate = import.meta.env.VITE_APP_DISABLE_FRESH !== "1";
let routesShown = false; let firstStart = true;
export default { export default {
async fetch(request: Request) { async fetch(request: Request) {
if (!app || recreate) { if (!app || recreate) {
@@ -48,8 +48,9 @@ export default {
await app.build(); await app.build();
// log routes // log routes
if (!routesShown) { if (firstStart) {
routesShown = true; console.log("[DB]", credentials);
firstStart = false;
console.log("\n\n[APP ROUTES]"); console.log("\n\n[APP ROUTES]");
showRoutes(app.server); showRoutes(app.server);
console.log("-------\n\n"); console.log("-------\n\n");
+37 -23
View File
@@ -5,34 +5,48 @@ description: 'Run bknd inside Astro'
import InstallBknd from '/snippets/install-bknd.mdx'; import InstallBknd from '/snippets/install-bknd.mdx';
## Installation ## Installation
Install bknd as a dependency: To get started with Astro and bknd you can either install the package manually, and follow the descriptions below, or use the CLI starter:
<InstallBknd />
<Note>The guide below assumes you're using Astro v4. We've experienced issues with Astro DB <Tabs>
using v5, see [this issue](https://github.com/withastro/astro/issues/12474).</Note> <Tab title="CLI Starter">
Create a new Astro CLI starter project by running the following command:
For the Astro integration to work, you also need to [add the react integration](https://docs.astro.build/en/guides/integrations-guide/react/): ```sh
```bash npx bknd create -i astro
npx astro add react ```
``` </Tab>
<Tab title="Manual">
Create a new Astro project by following the [official guide](https://docs.astro.build/en/install-and-setup/), and then install bknd as a dependency:
You also need to make sure to set the output to `hybrid` in your Astro config: <InstallBknd />
```js {6}
// astro.config.mjs
import { defineConfig } from "astro/config";
import react from "@astrojs/react";
export default defineConfig({ <Note>The guide below assumes you're using Astro v4. We've experienced issues with Astro DB
output: "hybrid", using v5, see [this issue](https://github.com/withastro/astro/issues/12474).</Note>
integrations: [react()]
});
```
<Note> For the Astro integration to work, you also need to [add the react integration](https://docs.astro.build/en/guides/integrations-guide/react/):
If you don't want to use React with Astro, there is also an option to serve the bknd Admin UI ```bash
statically using Astro's middleware. In case you're interested in this, feel free to reach npx astro add react
out in [Discord](https://discord.gg/952SFk8Tb8) or open an [issue on GitHub](https://github.com/bknd-io/bknd/issues/new). ```
</Note>
You also need to make sure to set the output to `hybrid` in your Astro config:
```js {6}
// astro.config.mjs
import { defineConfig } from "astro/config";
import react from "@astrojs/react";
export default defineConfig({
output: "hybrid",
integrations: [react()]
});
```
<Note>
If you don't want to use React with Astro, there is also an option to serve the bknd Admin UI
statically using Astro's middleware. In case you're interested in this, feel free to reach
out in [Discord](https://discord.gg/952SFk8Tb8) or open an [issue on GitHub](https://github.com/bknd-io/bknd/issues/new).
</Note>
</Tab>
</Tabs>
## Serve the API ## Serve the API
Create a new catch-all route at `src/pages/api/[...api].ts`: Create a new catch-all route at `src/pages/api/[...api].ts`:
+17 -2
View File
@@ -5,8 +5,23 @@ description: 'Run bknd inside Bun'
import InstallBknd from '/snippets/install-bknd.mdx'; import InstallBknd from '/snippets/install-bknd.mdx';
## Installation ## Installation
Install bknd as a dependency: To get started with Bun and bknd you can either install the package manually, and follow the descriptions below, or use the CLI starter:
<InstallBknd />
<Tabs>
<Tab title="CLI Starter">
Create a new Bun CLI starter project by running the following command:
```sh
npx bknd create -i bun
```
</Tab>
<Tab title="Manual">
Create a new Bun project and then install bknd as a dependency:
<InstallBknd />
</Tab>
</Tabs>
## Serve the API & static files ## Serve the API & static files
The `serve` function of the Bun adapter makes sure to also serve the static files required for The `serve` function of the Bun adapter makes sure to also serve the static files required for
+39 -25
View File
@@ -5,30 +5,51 @@ description: 'Run bknd inside Cloudflare Worker'
import InstallBknd from '/snippets/install-bknd.mdx'; import InstallBknd from '/snippets/install-bknd.mdx';
## Installation ## Installation
Create a new cloudflare worker project by following the To get started with Cloudflare Workers and bknd you can either install the package manually, and follow the descriptions below, or use the CLI starter:
[official guide](https://developers.cloudflare.com/workers/get-started/guide/),
and then install bknd as a dependency:
<InstallBknd /> <Tabs>
<Tab title="CLI Starter">
Create a new Cloudflare CLI starter project by running the following command:
```sh
npx bknd create -i cloudflare
```
</Tab>
<Tab title="Manual">
Create a new cloudflare worker project by following the [official guide](https://developers.cloudflare.com/workers/get-started/guide/), and then install bknd as a dependency:
<InstallBknd />
</Tab>
</Tabs>
## Serve the API ## Serve the API
If you don't choose anything specific, the following code will use the `warm` mode. See the If you don't choose anything specific, the following code will use the `warm` mode and uses the first D1 binding it finds. See the
chapter [Using a different mode](#using-a-different-mode) for available modes. chapter [Using a different mode](#using-a-different-mode) for available modes.
```ts src/index.ts ```ts src/index.ts
import { serve } from "bknd/adapter/cloudflare"; import { serve, d1 } from "bknd/adapter/cloudflare";
// scans your environment for the first D1 binding it finds
export default serve();
// manually specifying a D1 binding:
export default serve<Env>({ export default serve<Env>({
app: ({ env }) => ({ app: ({ env }) => d1({ binding: env.D1_BINDING })
connection: { });
url: env.DB_URL,
authToken: env.DB_TOKEN // or specify binding using `bindings`
} export default serve<Env>({
}) bindings: ({ env }) => ({ db: env.D1_BINDING })
});
// or use LibSQL
export default serve<Env>({
app: ({ env }) => ({ url: env.DB_URL })
}); });
``` ```
For more information about the connection object, refer to the [Database](/usage/database) guide.
For more information about the connection object when using LibSQL, refer to the [Database](/usage/database) guide.
Now run the worker: Now run the worker:
```bash ```bash
@@ -73,24 +94,17 @@ Now in order to also server the static admin files, you have to modify the `wran
## Adding custom routes ## Adding custom routes
You can also add custom routes by defining them after the app has been built, like so: You can also add custom routes by defining them after the app has been built, like so:
```ts {14-16} ```ts {5-7}
import { serve } from "bknd/adapter/cloudflare"; import { serve } from "bknd/adapter/cloudflare";
import manifest from "__STATIC_CONTENT_MANIFEST";
export default serve<Env>({ export default serve<Env>({
app: ({ env }) => ({ // ...
connection: {
url: env.DB_URL,
authToken: env.DB_TOKEN
}
}),
onBuilt: async (app) => { onBuilt: async (app) => {
app.modules.server.get("/hello", (c) => c.json({ hello: "world" })); app.server.get("/hello", (c) => c.json({ hello: "world" }));
}, }
manifest,
setAdminHtml: true
}); });
``` ```
The property `app.server` is a [Hono](https://hono.dev/) instance, you can literally anything you can do with Hono.
## Using a different mode ## Using a different mode
With the Cloudflare Workers adapter, you're being offered to 4 modes to choose from (default: With the Cloudflare Workers adapter, you're being offered to 4 modes to choose from (default:
+16 -2
View File
@@ -5,8 +5,22 @@ description: 'Run bknd inside Next.js'
import InstallBknd from '/snippets/install-bknd.mdx'; import InstallBknd from '/snippets/install-bknd.mdx';
## Installation ## Installation
Install bknd as a dependency: To get started with Next.js and bknd you can either install the package manually, and follow the descriptions below, or use the CLI starter:
<InstallBknd />
<Tabs>
<Tab title="CLI Starter">
Create a new Next.js CLI starter project by running the following command:
```sh
npx bknd create -i nextjs
```
</Tab>
<Tab title="Manual">
Create a new Next.js project by following the [official guide](https://nextjs.org/docs/pages/api-reference/cli/create-next-app), and then install bknd as a dependency:
<InstallBknd />
</Tab>
</Tabs>
## Serve the API ## Serve the API
``` tsx ``` tsx
+18 -3
View File
@@ -4,9 +4,24 @@ description: 'Run bknd inside Node'
--- ---
import InstallBknd from '/snippets/install-bknd.mdx'; import InstallBknd from '/snippets/install-bknd.mdx';
## Installation ## Installation
Install bknd as a dependency: To get started with Node and bknd you can either install the package manually, and follow the descriptions below, or use the CLI starter:
<InstallBknd />
<Tabs>
<Tab title="CLI Starter">
Create a new Node CLI starter project by running the following command:
```sh
npx bknd create -i node
```
</Tab>
<Tab title="Manual">
Create a new Node project and then install bknd as a dependency:
<InstallBknd />
</Tab>
</Tabs>
## Serve the API & static files ## Serve the API & static files
The `serve` function of the Node adapter makes sure to also serve the static files required for The `serve` function of the Node adapter makes sure to also serve the static files required for
@@ -20,7 +35,7 @@ import { serve } from "bknd/adapter/node";
/** @type {import("bknd/adapter/node").NodeAdapterOptions} */ /** @type {import("bknd/adapter/node").NodeAdapterOptions} */
const config = { const config = {
connection: { connection: {
url: ":memory:" url: "file:data.db"
} }
}; };
+17 -2
View File
@@ -5,8 +5,23 @@ description: 'Run bknd inside Remix'
import InstallBknd from '/snippets/install-bknd.mdx'; import InstallBknd from '/snippets/install-bknd.mdx';
## Installation ## Installation
Install bknd as a dependency: To get started with Remix and bknd you can either install the package manually, and follow the descriptions below, or use the CLI starter:
<InstallBknd />
<Tabs>
<Tab title="CLI Starter">
Create a new Remix CLI starter project by running the following command:
```sh
npx bknd create -i remix
```
</Tab>
<Tab title="Manual">
Create a new Remix project by following the [official guide](https://remix.run/docs/en/main/other-api/create-remix), and then install bknd as a dependency:
<InstallBknd />
</Tab>
</Tabs>
## Serve the API ## Serve the API
Since Remix doesn't support middleware yet, we need a helper file to initialize the App to import from. Create a new file at `app/bknd.ts`: Since Remix doesn't support middleware yet, we need a helper file to initialize the App to import from. Create a new file at `app/bknd.ts`: