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:
dswbx
2025-08-29 12:50:23 +02:00
committed by GitHub
parent 5b879ac6bf
commit 5ed1cf19b6
17 changed files with 655 additions and 100 deletions

View File

@@ -23,13 +23,34 @@ import type { IEmailDriver, ICacheDriver } from "core/drivers";
import { Api, type ApiOptions } from "Api";
export type AppPluginConfig = {
/**
* The name of the plugin.
*/
name: string;
/**
* The schema of the plugin.
*/
schema?: () => MaybePromise<ReturnType<typeof prototypeEm> | void>;
/**
* Called before the app is built.
*/
beforeBuild?: () => MaybePromise<void>;
/**
* Called after the app is built.
*/
onBuilt?: () => MaybePromise<void>;
/**
* Called when the server is initialized.
*/
onServerInit?: (server: Hono<ServerEnv>) => MaybePromise<void>;
onFirstBoot?: () => MaybePromise<void>;
/**
* Called when the app is booted.
*/
onBoot?: () => MaybePromise<void>;
/**
* Called when the app is first booted.
*/
onFirstBoot?: () => MaybePromise<void>;
};
export type AppPlugin = (app: App) => AppPluginConfig;

View File

@@ -11,7 +11,7 @@ type BunEnv = Bun.Env;
export type BunBkndConfig<Env = BunEnv> = RuntimeBkndConfig<Env> & Omit<ServeOptions, "fetch">;
export async function createApp<Env = BunEnv>(
{ distPath, ...config }: BunBkndConfig<Env> = {},
{ distPath, serveStatic: _serveStatic, ...config }: BunBkndConfig<Env> = {},
args: Env = {} as Env,
opts?: RuntimeOptions,
) {
@@ -20,7 +20,11 @@ export async function createApp<Env = BunEnv>(
return await createRuntimeApp(
{
serveStatic: serveStatic({ root }),
serveStatic:
_serveStatic ??
serveStatic({
root,
}),
...config,
},
args ?? (process.env as Env),

View File

@@ -6,7 +6,7 @@ import { resolve } from "node:path";
* Vite plugin that provides Node.js filesystem access during development
* by injecting a polyfill into the SSR environment
*/
export function devFsPlugin({
export function devFsVitePlugin({
verbose = false,
configFile = "bknd.config.ts",
}: {

View File

@@ -14,6 +14,7 @@ import { usersFields } from "./auth-entities";
import { Authenticator } from "./authenticate/Authenticator";
import { Role } from "./authorize/Role";
export type UsersFields = typeof AppAuth.usersFields;
export type UserFieldSchema = FieldSchema<typeof AppAuth.usersFields>;
declare module "bknd" {
interface Users extends AppEntity, UserFieldSchema {}

View File

@@ -39,6 +39,9 @@ import {
type PolymorphicRelationConfig,
} from "data/relations";
import type { MediaFields } from "media/AppMedia";
import type { UsersFields } from "auth/AppAuth";
type Options<Config = any> = {
entity: { name: string; fields: Record<string, Field<any, any, any>> };
field_name: string;
@@ -199,6 +202,18 @@ export function entity<
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) {
return {
manyToOne: <Foreign extends Entity>(foreign: Foreign, config?: ManyToOneRelationConfig) => {

View File

@@ -157,6 +157,7 @@ export {
medium,
make,
entity,
systemEntity,
relation,
index,
em,

View File

@@ -9,6 +9,7 @@ import { buildMediaSchema, registry, type TAppMediaConfig } from "./media-schema
import { mediaFields } from "./media-entities";
import * as MediaPermissions from "media/media-permissions";
export type MediaFields = typeof AppMedia.mediaFields;
export type MediaFieldSchema = FieldSchema<typeof AppMedia.mediaFields>;
declare module "bknd" {
interface Media extends AppEntity, MediaFieldSchema {}

View File

@@ -19,22 +19,46 @@ const schema = s.partialObject({
type ImageOptimizationSchema = s.Static<typeof schema>;
export type CloudflareImageOptimizationOptions = {
/**
* The url to access the image optimization plugin
* @default /api/plugin/image/optimize
*/
accessUrl?: string;
/**
* The path to resolve the image from
* @default /api/media/file
*/
resolvePath?: string;
/**
* Whether to explain the image optimization schema
* @default false
*/
explain?: boolean;
/**
* The default options to use
* @default {}
*/
defaultOptions?: ImageOptimizationSchema;
/**
* The fixed options to use
* @default {}
*/
fixedOptions?: ImageOptimizationSchema;
/**
* The cache control to use
* @default public, max-age=31536000, immutable
*/
cacheControl?: string;
};
export function cloudflareImageOptimization({
accessUrl = "/_plugin/image/optimize",
accessUrl = "/api/plugin/image/optimize",
resolvePath = "/api/media/file",
explain = false,
defaultOptions = {},
fixedOptions = {},
}: CloudflareImageOptimizationOptions = {}): AppPlugin {
const disallowedAccessUrls = ["/api", "/admin", "/_optimize"];
const disallowedAccessUrls = ["/api", "/admin", "/api/plugin"];
if (disallowedAccessUrls.includes(accessUrl) || accessUrl.length < 2) {
throw new Error(`Disallowed accessUrl: ${accessUrl}`);
}

View File

@@ -46,24 +46,73 @@ export type DropzoneRenderProps = {
};
export type DropzoneProps = {
/**
* Get the upload info for a file
*/
getUploadInfo: (file: { path: string }) => { url: string; headers?: Headers; method?: string };
/**
* Handle the deletion of a file
*/
handleDelete: (file: { path: string }) => Promise<boolean>;
/**
* The initial items to display
*/
initialItems?: FileState[];
flow?: "start" | "end";
/**
* Maximum number of media items that can be uploaded
*/
maxItems?: number;
/**
* The allowed mime types
*/
allowedMimeTypes?: string[];
/**
* If true, the media item will be overwritten on entity media uploads if limit was reached
*/
overwrite?: boolean;
/**
* If true, the media items will be uploaded automatically
*/
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;
/**
* The on deleted callback
*/
onDeleted?: (file: { path: string }) => void;
/**
* The on uploaded all callback
*/
onUploadedAll?: (files: FileStateWithData[]) => void;
/**
* The on uploaded callback
*/
onUploaded?: (file: FileStateWithData) => void;
/**
* The on clicked callback
*/
onClick?: (file: FileState) => void;
/**
* The placeholder to use
*/
placeholder?: {
show?: boolean;
text?: string;
};
/**
* The footer to render
*/
footer?: ReactNode;
/**
* The children to render
*/
children?: ReactNode | ((props: DropzoneRenderProps) => ReactNode);
};

View File

@@ -10,15 +10,38 @@ import { mediaItemsToFileStates } from "./helper";
import { useInViewport } from "@mantine/hooks";
export type DropzoneContainerProps = {
/**
* The initial items to display
* @default []
*/
initialItems?: MediaFieldSchema[] | false;
/**
* Whether to use infinite scrolling
* @default false
*/
infinite?: boolean;
/**
* If given, the initial media items fetched will be from this entity
* @default undefined
*/
entity?: {
name: string;
id: PrimaryFieldType;
field: string;
};
/**
* The media config
* @default undefined
*/
media?: Pick<TAppMediaConfig, "entity_name" | "storage">;
/**
* Query to filter the media items
*/
query?: RepoQueryIn;
/**
* Whether to use a random filename
* @default false
*/
randomFilename?: boolean;
} & Omit<Partial<DropzoneProps>, "initialItems">;