mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-16 04:27:21 +00:00
docs: plugins, cloudflare, sdk, elements, database (#240)
* docs: added plugins docs, updated cloudflare docs * updated cli help text * added `systemEntity` and added docs on how to work with system entities * docs: added defaults to cloudflare image plugin * docs: updated sdk and elements
This commit is contained in:
@@ -23,13 +23,34 @@ import type { IEmailDriver, ICacheDriver } from "core/drivers";
|
|||||||
import { Api, type ApiOptions } from "Api";
|
import { Api, type ApiOptions } from "Api";
|
||||||
|
|
||||||
export type AppPluginConfig = {
|
export type AppPluginConfig = {
|
||||||
|
/**
|
||||||
|
* The name of the plugin.
|
||||||
|
*/
|
||||||
name: string;
|
name: string;
|
||||||
|
/**
|
||||||
|
* The schema of the plugin.
|
||||||
|
*/
|
||||||
schema?: () => MaybePromise<ReturnType<typeof prototypeEm> | void>;
|
schema?: () => MaybePromise<ReturnType<typeof prototypeEm> | void>;
|
||||||
|
/**
|
||||||
|
* Called before the app is built.
|
||||||
|
*/
|
||||||
beforeBuild?: () => MaybePromise<void>;
|
beforeBuild?: () => MaybePromise<void>;
|
||||||
|
/**
|
||||||
|
* Called after the app is built.
|
||||||
|
*/
|
||||||
onBuilt?: () => MaybePromise<void>;
|
onBuilt?: () => MaybePromise<void>;
|
||||||
|
/**
|
||||||
|
* Called when the server is initialized.
|
||||||
|
*/
|
||||||
onServerInit?: (server: Hono<ServerEnv>) => MaybePromise<void>;
|
onServerInit?: (server: Hono<ServerEnv>) => MaybePromise<void>;
|
||||||
onFirstBoot?: () => MaybePromise<void>;
|
/**
|
||||||
|
* Called when the app is booted.
|
||||||
|
*/
|
||||||
onBoot?: () => MaybePromise<void>;
|
onBoot?: () => MaybePromise<void>;
|
||||||
|
/**
|
||||||
|
* Called when the app is first booted.
|
||||||
|
*/
|
||||||
|
onFirstBoot?: () => MaybePromise<void>;
|
||||||
};
|
};
|
||||||
export type AppPlugin = (app: App) => AppPluginConfig;
|
export type AppPlugin = (app: App) => AppPluginConfig;
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ type BunEnv = Bun.Env;
|
|||||||
export type BunBkndConfig<Env = BunEnv> = RuntimeBkndConfig<Env> & Omit<ServeOptions, "fetch">;
|
export type BunBkndConfig<Env = BunEnv> = RuntimeBkndConfig<Env> & Omit<ServeOptions, "fetch">;
|
||||||
|
|
||||||
export async function createApp<Env = BunEnv>(
|
export async function createApp<Env = BunEnv>(
|
||||||
{ distPath, ...config }: BunBkndConfig<Env> = {},
|
{ distPath, serveStatic: _serveStatic, ...config }: BunBkndConfig<Env> = {},
|
||||||
args: Env = {} as Env,
|
args: Env = {} as Env,
|
||||||
opts?: RuntimeOptions,
|
opts?: RuntimeOptions,
|
||||||
) {
|
) {
|
||||||
@@ -20,7 +20,11 @@ export async function createApp<Env = BunEnv>(
|
|||||||
|
|
||||||
return await createRuntimeApp(
|
return await createRuntimeApp(
|
||||||
{
|
{
|
||||||
serveStatic: serveStatic({ root }),
|
serveStatic:
|
||||||
|
_serveStatic ??
|
||||||
|
serveStatic({
|
||||||
|
root,
|
||||||
|
}),
|
||||||
...config,
|
...config,
|
||||||
},
|
},
|
||||||
args ?? (process.env as Env),
|
args ?? (process.env as Env),
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { resolve } from "node:path";
|
|||||||
* Vite plugin that provides Node.js filesystem access during development
|
* Vite plugin that provides Node.js filesystem access during development
|
||||||
* by injecting a polyfill into the SSR environment
|
* by injecting a polyfill into the SSR environment
|
||||||
*/
|
*/
|
||||||
export function devFsPlugin({
|
export function devFsVitePlugin({
|
||||||
verbose = false,
|
verbose = false,
|
||||||
configFile = "bknd.config.ts",
|
configFile = "bknd.config.ts",
|
||||||
}: {
|
}: {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { usersFields } from "./auth-entities";
|
|||||||
import { Authenticator } from "./authenticate/Authenticator";
|
import { Authenticator } from "./authenticate/Authenticator";
|
||||||
import { Role } from "./authorize/Role";
|
import { Role } from "./authorize/Role";
|
||||||
|
|
||||||
|
export type UsersFields = typeof AppAuth.usersFields;
|
||||||
export type UserFieldSchema = FieldSchema<typeof AppAuth.usersFields>;
|
export type UserFieldSchema = FieldSchema<typeof AppAuth.usersFields>;
|
||||||
declare module "bknd" {
|
declare module "bknd" {
|
||||||
interface Users extends AppEntity, UserFieldSchema {}
|
interface Users extends AppEntity, UserFieldSchema {}
|
||||||
|
|||||||
@@ -39,6 +39,9 @@ import {
|
|||||||
type PolymorphicRelationConfig,
|
type PolymorphicRelationConfig,
|
||||||
} from "data/relations";
|
} from "data/relations";
|
||||||
|
|
||||||
|
import type { MediaFields } from "media/AppMedia";
|
||||||
|
import type { UsersFields } from "auth/AppAuth";
|
||||||
|
|
||||||
type Options<Config = any> = {
|
type Options<Config = any> = {
|
||||||
entity: { name: string; fields: Record<string, Field<any, any, any>> };
|
entity: { name: string; fields: Record<string, Field<any, any, any>> };
|
||||||
field_name: string;
|
field_name: string;
|
||||||
@@ -199,6 +202,18 @@ export function entity<
|
|||||||
return new Entity(name, _fields, config, type);
|
return new Entity(name, _fields, config, type);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SystemEntities = {
|
||||||
|
users: UsersFields;
|
||||||
|
media: MediaFields;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function systemEntity<
|
||||||
|
E extends keyof SystemEntities,
|
||||||
|
Fields extends Record<string, Field<any, any, any>>,
|
||||||
|
>(name: E, fields: Fields) {
|
||||||
|
return entity<E, SystemEntities[E] & Fields>(name, fields as any);
|
||||||
|
}
|
||||||
|
|
||||||
export function relation<Local extends Entity>(local: Local) {
|
export function relation<Local extends Entity>(local: Local) {
|
||||||
return {
|
return {
|
||||||
manyToOne: <Foreign extends Entity>(foreign: Foreign, config?: ManyToOneRelationConfig) => {
|
manyToOne: <Foreign extends Entity>(foreign: Foreign, config?: ManyToOneRelationConfig) => {
|
||||||
|
|||||||
@@ -157,6 +157,7 @@ export {
|
|||||||
medium,
|
medium,
|
||||||
make,
|
make,
|
||||||
entity,
|
entity,
|
||||||
|
systemEntity,
|
||||||
relation,
|
relation,
|
||||||
index,
|
index,
|
||||||
em,
|
em,
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { buildMediaSchema, registry, type TAppMediaConfig } from "./media-schema
|
|||||||
import { mediaFields } from "./media-entities";
|
import { mediaFields } from "./media-entities";
|
||||||
import * as MediaPermissions from "media/media-permissions";
|
import * as MediaPermissions from "media/media-permissions";
|
||||||
|
|
||||||
|
export type MediaFields = typeof AppMedia.mediaFields;
|
||||||
export type MediaFieldSchema = FieldSchema<typeof AppMedia.mediaFields>;
|
export type MediaFieldSchema = FieldSchema<typeof AppMedia.mediaFields>;
|
||||||
declare module "bknd" {
|
declare module "bknd" {
|
||||||
interface Media extends AppEntity, MediaFieldSchema {}
|
interface Media extends AppEntity, MediaFieldSchema {}
|
||||||
|
|||||||
@@ -19,22 +19,46 @@ const schema = s.partialObject({
|
|||||||
type ImageOptimizationSchema = s.Static<typeof schema>;
|
type ImageOptimizationSchema = s.Static<typeof schema>;
|
||||||
|
|
||||||
export type CloudflareImageOptimizationOptions = {
|
export type CloudflareImageOptimizationOptions = {
|
||||||
|
/**
|
||||||
|
* The url to access the image optimization plugin
|
||||||
|
* @default /api/plugin/image/optimize
|
||||||
|
*/
|
||||||
accessUrl?: string;
|
accessUrl?: string;
|
||||||
|
/**
|
||||||
|
* The path to resolve the image from
|
||||||
|
* @default /api/media/file
|
||||||
|
*/
|
||||||
resolvePath?: string;
|
resolvePath?: string;
|
||||||
|
/**
|
||||||
|
* Whether to explain the image optimization schema
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
explain?: boolean;
|
explain?: boolean;
|
||||||
|
/**
|
||||||
|
* The default options to use
|
||||||
|
* @default {}
|
||||||
|
*/
|
||||||
defaultOptions?: ImageOptimizationSchema;
|
defaultOptions?: ImageOptimizationSchema;
|
||||||
|
/**
|
||||||
|
* The fixed options to use
|
||||||
|
* @default {}
|
||||||
|
*/
|
||||||
fixedOptions?: ImageOptimizationSchema;
|
fixedOptions?: ImageOptimizationSchema;
|
||||||
|
/**
|
||||||
|
* The cache control to use
|
||||||
|
* @default public, max-age=31536000, immutable
|
||||||
|
*/
|
||||||
cacheControl?: string;
|
cacheControl?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function cloudflareImageOptimization({
|
export function cloudflareImageOptimization({
|
||||||
accessUrl = "/_plugin/image/optimize",
|
accessUrl = "/api/plugin/image/optimize",
|
||||||
resolvePath = "/api/media/file",
|
resolvePath = "/api/media/file",
|
||||||
explain = false,
|
explain = false,
|
||||||
defaultOptions = {},
|
defaultOptions = {},
|
||||||
fixedOptions = {},
|
fixedOptions = {},
|
||||||
}: CloudflareImageOptimizationOptions = {}): AppPlugin {
|
}: CloudflareImageOptimizationOptions = {}): AppPlugin {
|
||||||
const disallowedAccessUrls = ["/api", "/admin", "/_optimize"];
|
const disallowedAccessUrls = ["/api", "/admin", "/api/plugin"];
|
||||||
if (disallowedAccessUrls.includes(accessUrl) || accessUrl.length < 2) {
|
if (disallowedAccessUrls.includes(accessUrl) || accessUrl.length < 2) {
|
||||||
throw new Error(`Disallowed accessUrl: ${accessUrl}`);
|
throw new Error(`Disallowed accessUrl: ${accessUrl}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,24 +46,73 @@ export type DropzoneRenderProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type DropzoneProps = {
|
export type DropzoneProps = {
|
||||||
|
/**
|
||||||
|
* Get the upload info for a file
|
||||||
|
*/
|
||||||
getUploadInfo: (file: { path: string }) => { url: string; headers?: Headers; method?: string };
|
getUploadInfo: (file: { path: string }) => { url: string; headers?: Headers; method?: string };
|
||||||
|
/**
|
||||||
|
* Handle the deletion of a file
|
||||||
|
*/
|
||||||
handleDelete: (file: { path: string }) => Promise<boolean>;
|
handleDelete: (file: { path: string }) => Promise<boolean>;
|
||||||
|
/**
|
||||||
|
* The initial items to display
|
||||||
|
*/
|
||||||
initialItems?: FileState[];
|
initialItems?: FileState[];
|
||||||
flow?: "start" | "end";
|
/**
|
||||||
|
* Maximum number of media items that can be uploaded
|
||||||
|
*/
|
||||||
maxItems?: number;
|
maxItems?: number;
|
||||||
|
/**
|
||||||
|
* The allowed mime types
|
||||||
|
*/
|
||||||
allowedMimeTypes?: string[];
|
allowedMimeTypes?: string[];
|
||||||
|
/**
|
||||||
|
* If true, the media item will be overwritten on entity media uploads if limit was reached
|
||||||
|
*/
|
||||||
overwrite?: boolean;
|
overwrite?: boolean;
|
||||||
|
/**
|
||||||
|
* If true, the media items will be uploaded automatically
|
||||||
|
*/
|
||||||
autoUpload?: boolean;
|
autoUpload?: boolean;
|
||||||
|
/**
|
||||||
|
* Whether to add new items to the start or end of the list
|
||||||
|
* @default "start"
|
||||||
|
*/
|
||||||
|
flow?: "start" | "end";
|
||||||
|
/**
|
||||||
|
* The on rejected callback
|
||||||
|
*/
|
||||||
onRejected?: (files: FileWithPath[]) => void;
|
onRejected?: (files: FileWithPath[]) => void;
|
||||||
|
/**
|
||||||
|
* The on deleted callback
|
||||||
|
*/
|
||||||
onDeleted?: (file: { path: string }) => void;
|
onDeleted?: (file: { path: string }) => void;
|
||||||
|
/**
|
||||||
|
* The on uploaded all callback
|
||||||
|
*/
|
||||||
onUploadedAll?: (files: FileStateWithData[]) => void;
|
onUploadedAll?: (files: FileStateWithData[]) => void;
|
||||||
|
/**
|
||||||
|
* The on uploaded callback
|
||||||
|
*/
|
||||||
onUploaded?: (file: FileStateWithData) => void;
|
onUploaded?: (file: FileStateWithData) => void;
|
||||||
|
/**
|
||||||
|
* The on clicked callback
|
||||||
|
*/
|
||||||
onClick?: (file: FileState) => void;
|
onClick?: (file: FileState) => void;
|
||||||
|
/**
|
||||||
|
* The placeholder to use
|
||||||
|
*/
|
||||||
placeholder?: {
|
placeholder?: {
|
||||||
show?: boolean;
|
show?: boolean;
|
||||||
text?: string;
|
text?: string;
|
||||||
};
|
};
|
||||||
|
/**
|
||||||
|
* The footer to render
|
||||||
|
*/
|
||||||
footer?: ReactNode;
|
footer?: ReactNode;
|
||||||
|
/**
|
||||||
|
* The children to render
|
||||||
|
*/
|
||||||
children?: ReactNode | ((props: DropzoneRenderProps) => ReactNode);
|
children?: ReactNode | ((props: DropzoneRenderProps) => ReactNode);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -10,15 +10,38 @@ import { mediaItemsToFileStates } from "./helper";
|
|||||||
import { useInViewport } from "@mantine/hooks";
|
import { useInViewport } from "@mantine/hooks";
|
||||||
|
|
||||||
export type DropzoneContainerProps = {
|
export type DropzoneContainerProps = {
|
||||||
|
/**
|
||||||
|
* The initial items to display
|
||||||
|
* @default []
|
||||||
|
*/
|
||||||
initialItems?: MediaFieldSchema[] | false;
|
initialItems?: MediaFieldSchema[] | false;
|
||||||
|
/**
|
||||||
|
* Whether to use infinite scrolling
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
infinite?: boolean;
|
infinite?: boolean;
|
||||||
|
/**
|
||||||
|
* If given, the initial media items fetched will be from this entity
|
||||||
|
* @default undefined
|
||||||
|
*/
|
||||||
entity?: {
|
entity?: {
|
||||||
name: string;
|
name: string;
|
||||||
id: PrimaryFieldType;
|
id: PrimaryFieldType;
|
||||||
field: string;
|
field: string;
|
||||||
};
|
};
|
||||||
|
/**
|
||||||
|
* The media config
|
||||||
|
* @default undefined
|
||||||
|
*/
|
||||||
media?: Pick<TAppMediaConfig, "entity_name" | "storage">;
|
media?: Pick<TAppMediaConfig, "entity_name" | "storage">;
|
||||||
|
/**
|
||||||
|
* Query to filter the media items
|
||||||
|
*/
|
||||||
query?: RepoQueryIn;
|
query?: RepoQueryIn;
|
||||||
|
/**
|
||||||
|
* Whether to use a random filename
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
randomFilename?: boolean;
|
randomFilename?: boolean;
|
||||||
} & Omit<Partial<DropzoneProps>, "initialItems">;
|
} & Omit<Partial<DropzoneProps>, "initialItems">;
|
||||||
|
|
||||||
|
|||||||
211
docs/content/docs/(documentation)/extending/plugins.mdx
Normal file
211
docs/content/docs/(documentation)/extending/plugins.mdx
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
---
|
||||||
|
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;
|
||||||
|
```
|
||||||
|
|
||||||
|
### `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`
|
||||||
|
|
||||||
|
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
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -123,9 +123,8 @@ import { serve } from "bknd/adapter/cloudflare";
|
|||||||
export default serve<Env>({
|
export default serve<Env>({
|
||||||
// ...
|
// ...
|
||||||
onBuilt: async (app) => {
|
onBuilt: async (app) => {
|
||||||
// [!code highlight]
|
|
||||||
app.server.get("/hello", (c) => c.json({ hello: "world" })); // [!code highlight]
|
app.server.get("/hello", (c) => c.json({ hello: "world" })); // [!code highlight]
|
||||||
}, // [!code highlight]
|
},
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -141,7 +140,6 @@ With the Cloudflare Workers adapter, you're being offered to 4 modes to choose f
|
|||||||
| `fresh` | On every request, the configuration gets refetched, app built and then served. | Ideal if you don't want to deal with eviction, KV or Durable Objects. |
|
| `fresh` | On every request, the configuration gets refetched, app built and then served. | Ideal if you don't want to deal with eviction, KV or Durable Objects. |
|
||||||
| `warm` | It tries to keep the built app in memory for as long as possible, and rebuilds if evicted. | Better response times, should be the default choice. |
|
| `warm` | It tries to keep the built app in memory for as long as possible, and rebuilds if evicted. | Better response times, should be the default choice. |
|
||||||
| `cache` | The configuration is fetched from KV to reduce the initial roundtrip to the database. | Generally faster response times with irregular access patterns. |
|
| `cache` | The configuration is fetched from KV to reduce the initial roundtrip to the database. | Generally faster response times with irregular access patterns. |
|
||||||
| `durable` | The bknd app is ran inside a Durable Object and can be configured to stay alive. | Slowest boot time, but fastest responses. Can be kept alive for as long as you want, giving similar response times as server instances. |
|
|
||||||
|
|
||||||
### Modes: `fresh` and `warm`
|
### Modes: `fresh` and `warm`
|
||||||
|
|
||||||
@@ -172,76 +170,6 @@ export default serve<Env>({
|
|||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
### Mode: `durable` (advanced)
|
|
||||||
|
|
||||||
To use the `durable` mode, you have to specify the Durable Object to extract from your
|
|
||||||
environment, and additionally export the `DurableBkndApp` class:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
import { serve, DurableBkndApp } from "bknd/adapter/cloudflare";
|
|
||||||
|
|
||||||
export { DurableBkndApp };
|
|
||||||
export default serve<Env>({
|
|
||||||
// ...
|
|
||||||
mode: "durable",
|
|
||||||
bindings: ({ env }) => ({ dobj: env.DOBJ }),
|
|
||||||
keepAliveSeconds: 60, // optional
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
Next, you need to define the Durable Object in your `wrangler.toml` file (refer to the [Durable
|
|
||||||
Objects](https://developers.cloudflare.com/durable-objects/) documentation):
|
|
||||||
|
|
||||||
```toml
|
|
||||||
[[durable_objects.bindings]]
|
|
||||||
name = "DOBJ"
|
|
||||||
class_name = "DurableBkndApp"
|
|
||||||
|
|
||||||
[[migrations]]
|
|
||||||
tag = "v1"
|
|
||||||
new_classes = ["DurableBkndApp"]
|
|
||||||
```
|
|
||||||
|
|
||||||
Since the communication between the Worker and Durable Object is serialized, the `onBuilt`
|
|
||||||
property won't work. To use it (e.g. to specify special routes), you need to extend from the
|
|
||||||
`DurableBkndApp`:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
import type { App } from "bknd";
|
|
||||||
import { serve, DurableBkndApp } from "bknd/adapter/cloudflare";
|
|
||||||
|
|
||||||
export default serve({
|
|
||||||
// ...
|
|
||||||
mode: "durable",
|
|
||||||
bindings: ({ env }) => ({ dobj: env.DOBJ }),
|
|
||||||
keepAliveSeconds: 60, // optional
|
|
||||||
});
|
|
||||||
|
|
||||||
export class CustomDurableBkndApp extends DurableBkndApp {
|
|
||||||
async onBuilt(app: App) {
|
|
||||||
app.modules.server.get("/custom/endpoint", (c) => c.text("Custom"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
In case you've already deployed your Worker, the deploy command may complain about a new class
|
|
||||||
being used. To fix this issue, you need to add a "rename migration":
|
|
||||||
|
|
||||||
```toml
|
|
||||||
[[durable_objects.bindings]]
|
|
||||||
name = "DOBJ"
|
|
||||||
class_name = "CustomDurableBkndApp"
|
|
||||||
|
|
||||||
[[migrations]]
|
|
||||||
tag = "v1"
|
|
||||||
new_classes = ["DurableBkndApp"]
|
|
||||||
|
|
||||||
[[migrations]]
|
|
||||||
tag = "v2"
|
|
||||||
renamed_classes = [{from = "DurableBkndApp", to = "CustomDurableBkndApp"}]
|
|
||||||
deleted_classes = ["DurableBkndApp"]
|
|
||||||
```
|
|
||||||
|
|
||||||
## D1 Sessions (experimental)
|
## D1 Sessions (experimental)
|
||||||
|
|
||||||
D1 now supports to enable [global read replication](https://developers.cloudflare.com/d1/best-practices/read-replication/). This allows to reduce latency by reading from the closest region. In order for this to work, D1 has to be started from a bookmark. You can enable this behavior on bknd by setting the `d1.session` property:
|
D1 now supports to enable [global read replication](https://developers.cloudflare.com/d1/best-practices/read-replication/). This allows to reduce latency by reading from the closest region. In order for this to work, D1 has to be started from a bookmark. You can enable this behavior on bknd by setting the `d1.session` property:
|
||||||
@@ -272,3 +200,83 @@ If bknd is used in a stateful user context (like in a browser), it'll automatica
|
|||||||
```bash
|
```bash
|
||||||
curl -H "x-cf-d1-session: <bookmark>" ...
|
curl -H "x-cf-d1-session: <bookmark>" ...
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Filesystem access with Vite Plugin
|
||||||
|
The [Cloudflare Vite Plugin](https://developers.cloudflare.com/workers/vite-plugin/) allows to use Vite with Miniflare to emulate the Cloudflare Workers runtime. This is great, however, `unenv` disables any Node.js APIs that aren't supported, including the `fs` module. If you want to use plugins such as [`syncTypes`](/extending/plugins#synctypes), this will cause issues.
|
||||||
|
|
||||||
|
To fix this, bknd exports a Vite plugin that provides filesystem access during development. You can use it by adding the following to your `vite.config.ts` file:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { devFsVitePlugin } from "bknd/adapter/cloudflare/vite";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [devFsVitePlugin()], // [!code highlight]
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Now to use this polyfill, you can use the `devFsWrite` function to write files to the filesystem.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { devFsWrite } from "bknd/adapter/cloudflare/vite"; // [!code highlight]
|
||||||
|
import { syncTypes } from "bknd/plugins";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
options: {
|
||||||
|
plugins: [
|
||||||
|
syncTypes({
|
||||||
|
write: async (et) => {
|
||||||
|
await devFsWrite("bknd-types.d.ts", et.toString()); // [!code highlight]
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
},
|
||||||
|
} satisfies BkndConfig;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Cloudflare Bindings in CLI
|
||||||
|
|
||||||
|
The bknd CLI does not automatically have access to the Cloudflare bindings. We need to manually proxy them to the CLI by using the `withPlatformProxy` helper function:
|
||||||
|
|
||||||
|
```typescript title="bknd.config.ts"
|
||||||
|
import { d1 } from "bknd/adapter/cloudflare";
|
||||||
|
import { withPlatformProxy } from "bknd/adapter/cloudflare/proxy";
|
||||||
|
|
||||||
|
export default withPlatformProxy({
|
||||||
|
app: ({ env }) => ({
|
||||||
|
connection: d1({ binding: env.DB }),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Now you can use the CLI with your Cloudflare resources.
|
||||||
|
|
||||||
|
<Callout type="warning">
|
||||||
|
Make sure to not import from this file in your app, as this would include `wrangler` as a dependency.
|
||||||
|
</Callout>
|
||||||
|
|
||||||
|
Instead, it's recommended to split this configuration into separate files, e.g. `bknd.config.ts` and `config.ts`:
|
||||||
|
|
||||||
|
```typescript title="config.ts"
|
||||||
|
import type { CloudflareBkndConfig } from "bknd/adapter/cloudflare";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
app: ({ env }) => ({
|
||||||
|
connection: d1({ binding: env.DB }),
|
||||||
|
}),
|
||||||
|
} satisfies CloudflareBkndConfig;
|
||||||
|
```
|
||||||
|
|
||||||
|
`config.ts` now holds the configuration, and can safely be imported in your app. Since the CLI looks for a `bknd.config.ts` file by default, we change it to wrap the configuration from `config.ts` in the `withPlatformProxy` helper function.
|
||||||
|
|
||||||
|
```typescript title="bknd.config.ts"
|
||||||
|
import { withPlatformProxy } from "bknd/adapter/cloudflare/proxy";
|
||||||
|
import config from "./config";
|
||||||
|
|
||||||
|
export default withPlatformProxy(config);
|
||||||
|
```
|
||||||
|
|
||||||
|
As an additional safe guard, you have to set a `PROXY` environment variable to `1` to enable the proxy.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
PROXY=1 npx bknd types
|
||||||
|
```
|
||||||
@@ -18,6 +18,7 @@
|
|||||||
"---Extending---",
|
"---Extending---",
|
||||||
"./extending/config",
|
"./extending/config",
|
||||||
"./extending/events",
|
"./extending/events",
|
||||||
|
"./extending/plugins",
|
||||||
"---Integration---",
|
"---Integration---",
|
||||||
"./integration/introduction",
|
"./integration/introduction",
|
||||||
"./integration/(frameworks)/",
|
"./integration/(frameworks)/",
|
||||||
|
|||||||
@@ -16,22 +16,24 @@ Here is the output:
|
|||||||
$ npx bknd
|
$ npx bknd
|
||||||
Usage: bknd [options] [command]
|
Usage: bknd [options] [command]
|
||||||
|
|
||||||
⚡ bknd cli v0.16.0
|
⚡ bknd cli v0.17.0
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
-V, --version output the version number
|
-V, --version output the version number
|
||||||
-h, --help display help for command
|
-h, --help display help for command
|
||||||
|
|
||||||
Commands:
|
Commands:
|
||||||
user <action> create/update users, or generate a token (auth)
|
config [options] get app config
|
||||||
types [options] generate types
|
copy-assets [options] copy static assets
|
||||||
schema [options] get schema
|
create [options] create a new project
|
||||||
run [options] run an instance
|
debug <subject> debug bknd
|
||||||
debug <subject> debug bknd
|
mcp [options] mcp server stdio transport
|
||||||
create [options] create a new project
|
run [options] run an instance
|
||||||
copy-assets [options] copy static assets
|
schema [options] get schema
|
||||||
config [options] get default config
|
sync [options] sync database
|
||||||
help [command] display help for command
|
types [options] generate types
|
||||||
|
user [options] <action> create/update users, or generate a token (auth)
|
||||||
|
help [command] display help for command
|
||||||
```
|
```
|
||||||
|
|
||||||
## Starting an instance (`run`)
|
## Starting an instance (`run`)
|
||||||
|
|||||||
@@ -349,6 +349,91 @@ Note that we didn't add relational fields directly to the entity, but instead de
|
|||||||
manually.
|
manually.
|
||||||
</Callout>
|
</Callout>
|
||||||
|
|
||||||
|
### System entities
|
||||||
|
|
||||||
|
There are multiple system entities which are added depending on if the module is enabled:
|
||||||
|
- `users`: if authentication is enabled
|
||||||
|
- `media`: if media is enabled and an adapter is configured
|
||||||
|
|
||||||
|
You can add additional fields to these entities. System-defined fields don't have to be repeated, those are automatically added to the entity, so don't worry about that. It's important though to match the system entities name, otherwise a new unrelated entity will be created.
|
||||||
|
|
||||||
|
If you'd like to connect your entities to system entities, you need them in the schema to access their reference when making relations. From the example above, if you'd like to connect the `posts` entity to the `users` entity, you can do so like this:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { em, entity, text, number, systemEntity } from "bknd";
|
||||||
|
|
||||||
|
const schema = em(
|
||||||
|
{
|
||||||
|
posts: entity("posts", {
|
||||||
|
title: text().required(),
|
||||||
|
slug: text().required(),
|
||||||
|
content: text(),
|
||||||
|
views: number(),
|
||||||
|
// don't add the foreign key field, it's automatically added
|
||||||
|
}),
|
||||||
|
comments: entity("comments", {
|
||||||
|
content: text(),
|
||||||
|
}),
|
||||||
|
// [!code highlight]
|
||||||
|
// add a `users` entity
|
||||||
|
users: systemEntity("users", { // [!code highlight]
|
||||||
|
// [!code highlight]
|
||||||
|
// optionally add additional fields
|
||||||
|
}) // [!code highlight]
|
||||||
|
},
|
||||||
|
// now you have access to the system entity "users"
|
||||||
|
({ relation, index }, { posts, comments, users }) => {
|
||||||
|
// ... other relations
|
||||||
|
relation(posts).manyToOne(users); // [!code highlight]
|
||||||
|
},
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Add media to an entity
|
||||||
|
|
||||||
|
If media is enabled, you can upload media directly or associate it with an entity. E.g. you may want to upload a cover image for a post, but also a gallery of images. Since a relation to the media entity is polymorphic, you have to:
|
||||||
|
1. add a virtual field to your entity (single `medium` or multiple `media`)
|
||||||
|
2. add the relation from the owning entity to the media entity
|
||||||
|
3. specify the mapped field name by using the `mappedBy` option
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { em, entity, text, number, systemEntity, medium, media } from "bknd";
|
||||||
|
|
||||||
|
const schema = em(
|
||||||
|
{
|
||||||
|
posts: entity("posts", {
|
||||||
|
title: text().required(),
|
||||||
|
slug: text().required(),
|
||||||
|
content: text(),
|
||||||
|
views: number(),
|
||||||
|
// [!code highlight]
|
||||||
|
// `medium` represents a single media item
|
||||||
|
cover: medium(), // [!code highlight]
|
||||||
|
// [!code highlight]
|
||||||
|
// `media` represents a list of media items
|
||||||
|
gallery: media(), // [!code highlight]
|
||||||
|
}),
|
||||||
|
comments: entity("comments", {
|
||||||
|
content: text(),
|
||||||
|
}),
|
||||||
|
// [!code highlight]
|
||||||
|
// add the `media` entity
|
||||||
|
media: systemEntity("media", { // [!code highlight]
|
||||||
|
// [!code highlight]
|
||||||
|
// optionally add additional fields
|
||||||
|
}) // [!code highlight]
|
||||||
|
},
|
||||||
|
// now you have access to the system entity "media"
|
||||||
|
({ relation, index }, { posts, comments, media }) => {
|
||||||
|
// add the `cover` relation
|
||||||
|
relation(posts).polyToOne(media, { mappedBy: "cover" }); // [!code highlight]
|
||||||
|
// add the `gallery` relation
|
||||||
|
relation(posts).polyToMany(media, { mappedBy: "gallery" }); // [!code highlight]
|
||||||
|
},
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
### Type completion
|
### Type completion
|
||||||
|
|
||||||
To get type completion, there are two options:
|
To get type completion, there are two options:
|
||||||
|
|||||||
@@ -44,16 +44,8 @@ export default function UserAvatar() {
|
|||||||
|
|
||||||
#### Props
|
#### Props
|
||||||
|
|
||||||
- `initialItems?: xMediaFieldSchema[]`: Initial items to display, must be an array of media objects.
|
<AutoTypeTable path="../app/src/ui/elements/media/DropzoneContainer.tsx" name="DropzoneContainerProps" />
|
||||||
- `entity?: { name: string; id: number; field: string }`: If given, the initial media items fetched will be from this entity.
|
|
||||||
- `query?: RepoQueryIn`: Query to filter the media items.
|
|
||||||
- `overwrite?: boolean`: If true, the media item will be overwritten on entity media uploads if limit was reached.
|
|
||||||
- `maxItems?: number`: Maximum number of media items that can be uploaded.
|
|
||||||
- `autoUpload?: boolean`: If true, the media items will be uploaded automatically.
|
|
||||||
- `onRejected?: (files: FileWithPath[]) => void`: Callback when a file is rejected.
|
|
||||||
- `onDeleted?: (file: FileState) => void`: Callback when a file is deleted.
|
|
||||||
- `onUploaded?: (file: FileState) => void`: Callback when a file is uploaded.
|
|
||||||
- `placeholder?: { show?: boolean; text?: string }`: Placeholder text to show when no media items are present.
|
|
||||||
|
|
||||||
#### Customize Rendering
|
#### Customize Rendering
|
||||||
|
|
||||||
|
|||||||
@@ -199,6 +199,37 @@ To delete many records of an entity, use the `deleteMany` method:
|
|||||||
const { data } = await api.data.deleteMany("posts", { views: { $lte: 1 } });
|
const { data } = await api.data.deleteMany("posts", { views: { $lte: 1 } });
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### `data.readManyByReference([entity], [id], [reference], [query])`
|
||||||
|
|
||||||
|
To retrieve records from a related entity by following a reference, use the `readManyByReference` method:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const { data } = await api.data.readManyByReference("posts", 1, "comments", {
|
||||||
|
limit: 5,
|
||||||
|
sort: "-created_at",
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### `data.count([entity], [where])`
|
||||||
|
|
||||||
|
To count records in an entity that match certain criteria, use the `count` method:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const { data } = await api.data.count("posts", {
|
||||||
|
views: { $gt: 100 }
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### `data.exists([entity], [where])`
|
||||||
|
|
||||||
|
To check if any records exist in an entity that match certain criteria, use the `exists` method:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const { data } = await api.data.exists("posts", {
|
||||||
|
title: "Hello, World!"
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
## Auth (`api.auth`)
|
## Auth (`api.auth`)
|
||||||
|
|
||||||
Access the `Auth` specific API methods at `api.auth`. If there is successful authentication, the
|
Access the `Auth` specific API methods at `api.auth`. If there is successful authentication, the
|
||||||
@@ -241,3 +272,89 @@ To retrieve the current user, use the `me` method:
|
|||||||
```ts
|
```ts
|
||||||
const { data } = await api.auth.me();
|
const { data } = await api.auth.me();
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Media (`api.media`)
|
||||||
|
|
||||||
|
Access the `Media` specific API methods at `api.media`.
|
||||||
|
|
||||||
|
### `media.listFiles()`
|
||||||
|
|
||||||
|
To retrieve a list of all uploaded files, use the `listFiles` method:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const { data } = await api.media.listFiles();
|
||||||
|
// ^? FileListObject[]
|
||||||
|
```
|
||||||
|
|
||||||
|
### `media.getFile([filename])`
|
||||||
|
|
||||||
|
To retrieve a file as a readable stream, use the `getFile` method:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const { data } = await api.media.getFile("image.jpg");
|
||||||
|
// ^? ReadableStream<Uint8Array>
|
||||||
|
```
|
||||||
|
|
||||||
|
### `media.getFileStream([filename])`
|
||||||
|
|
||||||
|
To get a file stream directly, use the `getFileStream` method:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const stream = await api.media.getFileStream("image.jpg");
|
||||||
|
// ^? ReadableStream<Uint8Array>
|
||||||
|
```
|
||||||
|
|
||||||
|
### `media.download([filename])`
|
||||||
|
|
||||||
|
To download a file as a File object, use the `download` method:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const file = await api.media.download("image.jpg");
|
||||||
|
// ^? File
|
||||||
|
```
|
||||||
|
|
||||||
|
### `media.upload([item], [options])`
|
||||||
|
|
||||||
|
To upload a file, use the `upload` method. The item can be any of:
|
||||||
|
- `File` object
|
||||||
|
- `Request` object
|
||||||
|
- `Response` object
|
||||||
|
- `string` (URL)
|
||||||
|
- `ReadableStream`
|
||||||
|
- `Buffer`
|
||||||
|
- `Blob`
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// Upload a File object
|
||||||
|
const { data } = await api.media.upload(item);
|
||||||
|
|
||||||
|
// Upload from a URL
|
||||||
|
const { data } = await api.media.upload("https://example.com/image.jpg");
|
||||||
|
|
||||||
|
// Upload with custom options
|
||||||
|
const { data } = await api.media.upload(item, {
|
||||||
|
filename: "custom-name.jpg",
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### `media.uploadToEntity([entity], [id], [field], [item], [options])`
|
||||||
|
|
||||||
|
To upload a file directly to an entity field, use the `uploadToEntity` method:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const { data } = await api.media.uploadToEntity(
|
||||||
|
"posts",
|
||||||
|
1,
|
||||||
|
"image",
|
||||||
|
item
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### `media.deleteFile([filename])`
|
||||||
|
|
||||||
|
To delete a file, use the `deleteFile` method:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const { data } = await api.media.deleteFile("image.jpg");
|
||||||
|
```
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user