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.
## 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/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/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/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](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.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.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.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.
+1
View File
@@ -111,6 +111,7 @@ describe("EventManager", async () => {
emgr.onEvent(ReturnEvent, async () => "1", "sync");
emgr.onEvent(ReturnEvent, async () => "0", "sync");
// @todo: fix this
// @ts-expect-error must be string
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",
"sideEffects": false,
"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.",
"homepage": "https://bknd.io",
"repository": {
@@ -22,9 +22,9 @@
"build:cli": "bun build src/cli/index.ts --target node --outdir dist/cli --minify",
"build:static": "vite build",
"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",
"build:types": "tsc --emitDeclarationOnly && tsc-alias",
"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 ./",
+1 -1
View File
@@ -1,5 +1,5 @@
import { Api, type ApiOptions } from "Api";
import type { CreateUserPayload } from "auth/AppAuth";
import { Api, type ApiOptions } from "bknd/client";
import { $console } from "core";
import { Event } from "core/events";
import { Connection, type LibSqlCredentials, LibsqlConnection } from "data";
+1 -1
View File
@@ -220,7 +220,7 @@ export class DataController extends Controller {
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);
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);
mutator.__unstable_toggleSystemEntityCreation(false);
const payload = this.uploadedEventDataToMediaPayload(e.params);
await mutator.insertOne(payload);
const { data } = await mutator.insertOne(payload);
mutator.__unstable_toggleSystemEntityCreation(true);
return { data };
},
{ 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;
}
getFileUploadUrl(file: FileWithPath): string {
getFileUploadUrl(file?: FileWithPath): string {
if (!file) return this.getUrl("/upload");
return this.getUrl(`/upload/${file.path}`);
}
+6 -7
View File
@@ -72,11 +72,8 @@ export class MediaController extends Controller {
// upload file
// @todo: add required type for "upload endpoints"
hono.post("/upload/:filename", async (c) => {
const { filename } = c.req.param();
if (!filename) {
throw new Error("No file name provided");
}
hono.post("/upload/:filename?", async (c) => {
const reqname = c.req.param("filename");
const body = await getFileFromContext(c);
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);
return c.json(res, HttpStatus.CREATED);
});
@@ -191,8 +190,8 @@ export class MediaController extends Controller {
);
}
const file_name = getRandomizedFilename(file as File);
const info = await this.getStorage().uploadFile(file, file_name, true);
const filename = getRandomizedFilename(file as File);
const info = await this.getStorage().uploadFile(file, filename, true);
const mutator = this.media.em.mutator(media_entity);
mutator.__unstable_toggleSystemEntityCreation(false);
+4 -1
View File
@@ -119,7 +119,10 @@ export class Storage implements EmitsEvents {
}
};
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;
+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";
export type FileUploadedEventData = FileUploadPayload & {
file: FileBody;
};
export class FileUploadedEvent extends Event<FileUploadedEventData> {
export class FileUploadedEvent extends Event<FileUploadedEventData, object> {
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 }> {
+19
View File
@@ -77,6 +77,17 @@ export function guess(f: string): 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()) {
if (v === mime && !exclude.includes(k)) {
return true;
@@ -86,6 +97,14 @@ export function isMimeType(mime: any, exclude: 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()) {
if (v === mime) {
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;
const parts = filename.split(".");
@@ -17,6 +18,12 @@ export function getRandomizedFilename(file: File | string, length = 16): string
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?
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 { useAuth } from "./schema/auth/use-auth";
export { Api, type TApiUser, type AuthState, type ApiOptions } from "../../Api";
export { FetchPromise } from "modules/ModuleApi";
export type { RepoQueryIn } from "data";
+19 -13
View File
@@ -1,3 +1,4 @@
import type { DB } from "core";
import {
type ComponentPropsWithRef,
type ComponentPropsWithoutRef,
@@ -23,6 +24,8 @@ export type FileState = {
progress: number;
};
export type FileStateWithData = FileState & { data: DB["media"] };
export type DropzoneRenderProps = {
wrapperRef: RefObject<HTMLDivElement>;
inputProps: ComponentPropsWithRef<"input">;
@@ -50,7 +53,7 @@ export type DropzoneProps = {
autoUpload?: boolean;
onRejected?: (files: FileWithPath[]) => void;
onDeleted?: (file: FileState) => void;
onUploaded?: (files: FileState[]) => void;
onUploaded?: (files: FileStateWithData[]) => void;
placeholder?: {
show?: boolean;
text?: string;
@@ -172,15 +175,16 @@ export function Dropzone({
setUploading(false);
return;
} else {
const uploaded: FileStateWithData[] = [];
for (const file of pendingFiles) {
try {
await uploadFileProgress(file);
uploaded.push(await uploadFileProgress(file));
} catch (e) {
handleUploadError(e);
}
}
setUploading(false);
onUploaded?.(files);
onUploaded?.(uploaded);
}
})();
}
@@ -220,8 +224,8 @@ export function Dropzone({
setFiles((prev) => prev.filter((f) => f.path !== path));
}
function uploadFileProgress(file: FileState) {
return new Promise<void>((resolve, reject) => {
function uploadFileProgress(file: FileState): Promise<FileStateWithData> {
return new Promise((resolve, reject) => {
if (!file.body) {
console.error("File has no body");
reject();
@@ -279,17 +283,19 @@ export function Dropzone({
const response = JSON.parse(xhr.responseText);
console.log("Response:", file, response);
console.log("New state", response.state);
replaceFileState(file.path, {
const newState = {
...response.state,
progress: 1,
state: "uploaded"
});
};
replaceFileState(file.path, newState);
resolve({ ...response, ...file, ...newState });
} catch (e) {
setFileState(file.path, "uploaded", 1);
console.error("Error parsing response", e);
reject(e);
}
resolve();
} else {
setFileState(file.path, "failed", 1);
console.error("Upload failed with status: ", xhr.status, xhr.statusText);
@@ -327,8 +333,8 @@ export function Dropzone({
}
async function uploadFile(file: FileState) {
await uploadFileProgress(file);
onUploaded?.([file]);
const result = await uploadFileProgress(file);
onUploaded?.([result]);
}
const openFileInput = () => inputRef.current?.click();
@@ -496,9 +502,9 @@ const Preview: React.FC<PreviewProps> = ({ file, handleUpload, handleDelete }) =
/>
</div>
<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">
<span className="truncate">{file.type}</span>
<span className="truncate select-text">{file.type}</span>
<span>{(file.size / 1024).toFixed(1)} KB</span>
</div>
</div>
@@ -10,7 +10,7 @@ import { mediaItemsToFileStates } from "./helper";
export type DropzoneContainerProps = {
children?: ReactNode;
initialItems?: MediaFieldSchema[];
initialItems?: MediaFieldSchema[] | false;
entity?: {
name: string;
id: number;
@@ -18,6 +18,7 @@ export type DropzoneContainerProps = {
};
media?: Pick<TAppMediaConfig, "entity_name" | "storage">;
query?: RepoQueryIn;
randomFilename?: boolean;
} & Omit<Partial<DropzoneProps>, "children" | "initialItems">;
const DropzoneContainerContext = createContext<DropzoneRenderProps>(undefined!);
@@ -28,6 +29,7 @@ export function DropzoneContainer({
entity,
query,
children,
randomFilename,
...props
}: DropzoneContainerProps) {
const id = useId();
@@ -57,12 +59,12 @@ export function DropzoneContainer({
...query
});
const $q = useApiQuery(selectApi, { enabled: !initialItems });
const $q = useApiQuery(selectApi, { enabled: initialItems !== false && !initialItems });
const getUploadInfo = useEvent((file) => {
const url = entity
? api.media.getEntityUploadUrl(entity.name, entity.id, entity.field)
: api.media.getFileUploadUrl(file);
: api.media.getFileUploadUrl(randomFilename ? undefined : file);
return {
url,
@@ -79,7 +81,7 @@ export function DropzoneContainer({
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 key = id + JSON.stringify(_initialItems);
+1
View File
@@ -12,6 +12,7 @@ export { useDropzone as useMediaDropzone };
export type {
PreviewComponentProps,
FileState,
FileStateWithData,
DropzoneProps,
DropzoneRenderProps
} 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/*"]
}
},
"include": ["./src/**/*.ts", "./src/**/*.tsx"],
"include": ["./src/**/*.ts", "./src/**/*.tsx", "vite.dev.ts", "build.ts"],
"exclude": ["node_modules", "dist", "dist/types", "**/*.d.ts"]
}
+4 -3
View File
@@ -29,7 +29,7 @@ if (example) {
let app: App;
const recreate = import.meta.env.VITE_APP_DISABLE_FRESH !== "1";
let routesShown = false;
let firstStart = true;
export default {
async fetch(request: Request) {
if (!app || recreate) {
@@ -48,8 +48,9 @@ export default {
await app.build();
// log routes
if (!routesShown) {
routesShown = true;
if (firstStart) {
console.log("[DB]", credentials);
firstStart = false;
console.log("\n\n[APP ROUTES]");
showRoutes(app.server);
console.log("-------\n\n");
+37 -23
View File
@@ -5,34 +5,48 @@ description: 'Run bknd inside Astro'
import InstallBknd from '/snippets/install-bknd.mdx';
## Installation
Install bknd as a dependency:
<InstallBknd />
To get started with Astro and bknd you can either install the package manually, and follow the descriptions below, or use the CLI starter:
<Note>The guide below assumes you're using Astro v4. We've experienced issues with Astro DB
using v5, see [this issue](https://github.com/withastro/astro/issues/12474).</Note>
<Tabs>
<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/):
```bash
npx astro add react
```
```sh
npx bknd create -i astro
```
</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:
```js {6}
// astro.config.mjs
import { defineConfig } from "astro/config";
import react from "@astrojs/react";
<InstallBknd />
export default defineConfig({
output: "hybrid",
integrations: [react()]
});
```
<Note>The guide below assumes you're using Astro v4. We've experienced issues with Astro DB
using v5, see [this issue](https://github.com/withastro/astro/issues/12474).</Note>
<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>
For the Astro integration to work, you also need to [add the react integration](https://docs.astro.build/en/guides/integrations-guide/react/):
```bash
npx astro add react
```
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
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';
## Installation
Install bknd as a dependency:
<InstallBknd />
To get started with Bun and bknd you can either install the package manually, and follow the descriptions below, or use the CLI starter:
<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
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';
## Installation
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:
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:
<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
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.
```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>({
app: ({ env }) => ({
connection: {
url: env.DB_URL,
authToken: env.DB_TOKEN
}
})
app: ({ env }) => d1({ binding: env.D1_BINDING })
});
// 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:
```bash
@@ -73,24 +94,17 @@ Now in order to also server the static admin files, you have to modify the `wran
## Adding custom routes
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 manifest from "__STATIC_CONTENT_MANIFEST";
export default serve<Env>({
app: ({ env }) => ({
connection: {
url: env.DB_URL,
authToken: env.DB_TOKEN
}
}),
// ...
onBuilt: async (app) => {
app.modules.server.get("/hello", (c) => c.json({ hello: "world" }));
},
manifest,
setAdminHtml: true
app.server.get("/hello", (c) => c.json({ hello: "world" }));
}
});
```
The property `app.server` is a [Hono](https://hono.dev/) instance, you can literally anything you can do with Hono.
## Using a different mode
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';
## Installation
Install bknd as a dependency:
<InstallBknd />
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:
<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
``` tsx
+18 -3
View File
@@ -4,9 +4,24 @@ description: 'Run bknd inside Node'
---
import InstallBknd from '/snippets/install-bknd.mdx';
## Installation
Install bknd as a dependency:
<InstallBknd />
To get started with Node and bknd you can either install the package manually, and follow the descriptions below, or use the CLI starter:
<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
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} */
const config = {
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';
## Installation
Install bknd as a dependency:
<InstallBknd />
To get started with Remix and bknd you can either install the package manually, and follow the descriptions below, or use the CLI starter:
<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
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`: