Files
userbase/docs/content/docs/(documentation)/extending/plugins.mdx
T

390 lines
12 KiB
Plaintext

---
title: Plugins
tags: ["documentation"]
---
import { TypeTable } from 'fumadocs-ui/components/type-table';
bknd allows you to extend its functionality by creating plugins. These allows to hook into the app lifecycle and to provide a data structure that is guaranteed to be merged. A plugin is a function that takes in an instance of `App` and returns the following structure:
<AutoTypeTable path="../app/src/App.ts" name="AppPluginConfig" />
## Creating a simple plugin
To create a simple plugin which guarantees an entity `pages` to be available and an additioanl endpoint to render a html list of pages, you can create it as follows:
```tsx title="myPagesPlugin.tsx"
/** @jsxImportSource hono/jsx */
import { type App, type AppPlugin, em, entity, text } from "bknd";
export const myPagesPlugin: AppPlugin = (app) => ({
name: "my-pages-plugin",
// define the schema of the plugin
// this will always be merged into the app's schema
schema: () => em({
pages: entity("pages", {
title: text(),
content: text(),
}),
}),
// execute code after the app is built
onBuilt: () => {
// register a new endpoint, make sure that you choose an endpoint that is reachable for bknd
app.server.get("/my-pages", async (c) => {
const { data: pages } = await app.em.repo("pages").findMany({});
return c.html(
<body>
<h1>Pages: {pages.length}</h1>
<ul>
{pages.map((page: any) => (
<li key={page.id}>{page.title}</li>
))}
</ul>
</body>,
);
});
},
});
```
And then register it in your `bknd.config.ts` file:
```typescript
import type { BkndConfig } from "bknd/adapter";
import { myPagesPlugin } from "./myPagesPlugin";
export default {
options: {
plugins: [myPagesPlugin],
}
} satisfies BkndConfig;
```
The schema returned from the plugin will be merged into the schema of the app.
## Built-in plugins
bknd comes with a few built-in plugins that you can use.
### `syncTypes`
A simple plugin that writes down the TypeScript types of the data schema on boot and each build. The output is equivalent to running `npx bknd types`.
```typescript title="bknd.config.ts"
import { syncTypes } from "bknd/plugins";
import { writeFile } from "node:fs/promises";
export default {
options: {
plugins: [
syncTypes({
// whether to enable the plugin, make sure to disable in production
enabled: true,
// your writing function (required)
write: async (et) => {
await writeFile("bknd-types.d.ts", et.toString(), "utf-8");
}
}),
]
},
} satisfies BkndConfig;
```
### `syncConfig`
A simple plugin that writes down the app configuration on boot and each build.
```typescript title="bknd.config.ts"
import { syncConfig } from "bknd/plugins";
import { writeFile } from "node:fs/promises";
export default {
options: {
plugins: [
syncConfig({
// whether to enable the plugin, make sure to disable in production
enabled: true,
// your writing function (required)
write: async (config) => {
await writeFile("config.json", JSON.stringify(config, null, 2), "utf-8");
},
}),
]
},
} satisfies BkndConfig;
```
### `syncSecrets`
A simple plugin that writes down the app secrets on boot and each build.
```typescript title="bknd.config.ts"
import { syncSecrets } from "bknd/plugins";
import { writeFile } from "node:fs/promises";
export default {
options: {
plugins: [
syncSecrets({
// whether to enable the plugin, make sure to disable in production
enabled: true,
// your writing function (required)
write: async (secrets) => {
// apply your writing logic, e.g. writing a template file in an env format
await writeFile(
".env.example",
Object.entries(secrets)
.map(([key]) => `${key}=`)
.join("\n")
);
},
}),
]
},
} satisfies BkndConfig;
```
### `showRoutes`
A simple plugin that logs the routes of your app in the console.
```typescript title="bknd.config.ts"
import { showRoutes } from "bknd/plugins";
export default {
options: {
plugins: [
showRoutes({
// whether to show the routes only once (on first build)
once: true
})
],
},
} satisfies BkndConfig;
```
### `cloudflareImageOptimization`
<Callout type="info">
This plugin doesn't work on the development server, or on workers deployed with a `workers.dev` subdomain. It requires [Cloudflare Image transformations to be enabled](https://developers.cloudflare.com/images/get-started/#enable-transformations-on-your-zone) on your zone.
</Callout>
A plugin that add Cloudflare Image Optimization to your app's media storage.
```typescript title="bknd.config.ts"
import { cloudflareImageOptimization } from "bknd/plugins";
export default {
options: {
plugins: [
cloudflareImageOptimization({
// the url to access the image optimization plugin
accessUrl: "/api/plugin/image/optimize",
// the path to resolve the image from, defaults to `/api/media/file`
resolvePath: "/api/media/file",
// for example, you may want to have default option to limit to a width of 1000px
defaultOptions: {
width: 1000,
}
})
],
},
} satisfies BkndConfig;
```
Here is a break down of all configuration options:
<AutoTypeTable path="../app/src/plugins/cloudflare/image-optimization.plugin.ts" name="CloudflareImageOptimizationOptions" />
When enabled, you can now access your images at your configured `accessUrl`. For example, if you have a media file at `/api/media/file/image.jpg`, you can access the optimized image at `/api/plugin/image/optimize/image.jpg` for optimization.
Now you can add query parameters for the transformations, e.g. `?width=1000&height=1000`.
<TypeTable
type={{
dpr: {
description:
'The device pixel ratio to use',
type: 'number',
default: 1,
},
fit: {
description: 'The fit mode to use',
type: '"scale-down" | "contain" | "cover" | "crop" | "pad"',
},
format: {
description: 'The format to use',
type: '"auto" | "avif" | "webp" | "jpeg" | "baseline-jpeg" | "json"'
},
height: {
description: 'The height to use',
type: 'number',
},
width: {
description: 'The width to use',
type: 'number',
},
metadata: {
description: 'The metadata to use',
type: '"copyright" | "keep" | "none"',
default: 'copyright'
},
quality: {
description: 'The quality to use',
type: 'number',
default: 85
},
}}
/>
### `timestamps`
A plugin that adds `created_at` and `updated_at` fields to the specified entities.
```typescript title="bknd.config.ts"
import { timestamps } from "bknd/plugins";
export default {
options: {
plugins: [
timestamps({
// the entities to add timestamps to
entities: ["pages"],
// whether to set the `updated_at` field on create, defaults to true
setUpdatedOnCreate: true,
})
],
},
} satisfies BkndConfig;
```
### `emailOTP`
<Callout type="warning">
Make sure to setup proper permissions to restrict reading from the OTP entity. Also, this plugin requires the `email` driver to be registered.
</Callout>
A plugin that adds email OTP functionality to your app. It will add two endpoints to your app:
- `POST /api/auth/otp/login` to login a user with an OTP code
- `POST /api/auth/otp/register` to register a user with an OTP code
Both endpoints accept a JSON body with `email` (required) and `code` (optional). If `code` is provided, the OTP code will be validated and the user will be logged in or registered. If `code` is not provided, a new OTP code will be generated and sent to the user's email.
For example, to login an existing user with an OTP code, two requests are needed. The first one only with the email to generate and send the OTP code, and the second to send the users' email along with the OTP code. The last request will authenticate the user.
```http title="Generate OTP code to login"
POST /api/auth/otp/login
Content-Type: application/json
{
"email": "test@example.com"
}
```
If the user exists, an email will be sent with the OTP code, and the response will be a `201 Created`.
```http title="Login with OTP code"
POST /api/auth/otp/login
Content-Type: application/json
{
"email": "test@example.com",
"code": "123456"
}
```
If the code is valid, the user will be authenticated by sending a `Set-Cookie` header and a body property `token` with the JWT token (equally to the login endpoint).
```typescript title="bknd.config.ts"
import { emailOTP } from "bknd/plugins";
import { resendEmail } from "bknd";
export default {
options: {
drivers: {
// an email driver is required
email: resendEmail({ /* ... */}),
},
plugins: [
// all options are optional
emailOTP({
// the base path for the API endpoints
apiBasePath: "/api/auth/otp",
// the TTL for the OTP tokens in seconds
ttl: 600,
// the name of the OTP entity
entity: "users_otp",
// customize the email content
generateEmail: (otp) => ({
subject: "OTP Code",
body: `Your OTP code is: ${otp.code}`,
}),
// customize the code generation
generateCode: (user) => {
return Math.floor(100000 + Math.random() * 900000).toString();
},
})
],
},
} satisfies BkndConfig;
```
<AutoTypeTable path="../app/src/plugins/auth/email-otp.plugin.ts" name="EmailOTPPluginOptions" />
### `sort`
A plugin that maintains sequential numeric positions for one or more entities. Positions are updated automatically when items are inserted or moved. You can optionally configure a scope so that positions are maintained independently within groups, such as tasks within a project.
```typescript title="bknd.config.ts"
import { sort } from "bknd/plugins";
export default {
options: {
plugins: [
sort({
entities: {
tasks: {
field: "position",
scope: "project_id",
},
},
}),
],
},
} satisfies BkndConfig;
```
The configured `field` is used for the numeric position. If the field does not exist, the plugin adds it as a number field with a default value of `0`; if it already exists, it must be a number field. The plugin also adds an index for the field when one is not already present. The optional `scope` field limits reordering to records with the same scope value.
The plugin registers the following endpoints. The default base path is `/api/sort` and can be changed with `apiBasePath`:
- `POST /api/sort/:entity/reorder` moves an item to a position. The request body must include the item `id` and its new numeric `position`.
- `POST /api/sort/:entity/recalculate` recalculates positions in ascending order of the configured field. To recalculate only one scope, pass its value in the optional `scope` property.
```http title="Move a task"
POST /api/sort/tasks/reorder
Content-Type: application/json
{
"id": "task_123",
"position": 2
}
```
```http title="Recalculate a project's task positions"
POST /api/sort/tasks/recalculate
Content-Type: application/json
{
"scope": "project_456"
}
```
<AutoTypeTable path="../app/src/plugins/data/sort.plugin.ts" name="SortPluginOptions" />