diff --git a/app/src/App.ts b/app/src/App.ts index ae0ee55..fb05ee6 100644 --- a/app/src/App.ts +++ b/app/src/App.ts @@ -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 | void>; + /** + * Called before the app is built. + */ beforeBuild?: () => MaybePromise; + /** + * Called after the app is built. + */ onBuilt?: () => MaybePromise; + /** + * Called when the server is initialized. + */ onServerInit?: (server: Hono) => MaybePromise; - onFirstBoot?: () => MaybePromise; + /** + * Called when the app is booted. + */ onBoot?: () => MaybePromise; + /** + * Called when the app is first booted. + */ + onFirstBoot?: () => MaybePromise; }; export type AppPlugin = (app: App) => AppPluginConfig; diff --git a/app/src/adapter/bun/bun.adapter.ts b/app/src/adapter/bun/bun.adapter.ts index c3d271b..03689d5 100644 --- a/app/src/adapter/bun/bun.adapter.ts +++ b/app/src/adapter/bun/bun.adapter.ts @@ -11,7 +11,7 @@ type BunEnv = Bun.Env; export type BunBkndConfig = RuntimeBkndConfig & Omit; export async function createApp( - { distPath, ...config }: BunBkndConfig = {}, + { distPath, serveStatic: _serveStatic, ...config }: BunBkndConfig = {}, args: Env = {} as Env, opts?: RuntimeOptions, ) { @@ -20,7 +20,11 @@ export async function createApp( return await createRuntimeApp( { - serveStatic: serveStatic({ root }), + serveStatic: + _serveStatic ?? + serveStatic({ + root, + }), ...config, }, args ?? (process.env as Env), diff --git a/app/src/adapter/cloudflare/vite.ts b/app/src/adapter/cloudflare/vite.ts index 53ba999..c8c073e 100644 --- a/app/src/adapter/cloudflare/vite.ts +++ b/app/src/adapter/cloudflare/vite.ts @@ -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", }: { diff --git a/app/src/auth/AppAuth.ts b/app/src/auth/AppAuth.ts index 4ee9a8e..a0c6072 100644 --- a/app/src/auth/AppAuth.ts +++ b/app/src/auth/AppAuth.ts @@ -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; declare module "bknd" { interface Users extends AppEntity, UserFieldSchema {} diff --git a/app/src/data/prototype/index.ts b/app/src/data/prototype/index.ts index 4f25aeb..06483f5 100644 --- a/app/src/data/prototype/index.ts +++ b/app/src/data/prototype/index.ts @@ -39,6 +39,9 @@ import { type PolymorphicRelationConfig, } from "data/relations"; +import type { MediaFields } from "media/AppMedia"; +import type { UsersFields } from "auth/AppAuth"; + type Options = { entity: { name: string; fields: Record> }; 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>, +>(name: E, fields: Fields) { + return entity(name, fields as any); +} + export function relation(local: Local) { return { manyToOne: (foreign: Foreign, config?: ManyToOneRelationConfig) => { diff --git a/app/src/index.ts b/app/src/index.ts index bd6515f..46902cd 100644 --- a/app/src/index.ts +++ b/app/src/index.ts @@ -157,6 +157,7 @@ export { medium, make, entity, + systemEntity, relation, index, em, diff --git a/app/src/media/AppMedia.ts b/app/src/media/AppMedia.ts index 4cba790..0971187 100644 --- a/app/src/media/AppMedia.ts +++ b/app/src/media/AppMedia.ts @@ -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; declare module "bknd" { interface Media extends AppEntity, MediaFieldSchema {} diff --git a/app/src/plugins/cloudflare/image-optimization.plugin.ts b/app/src/plugins/cloudflare/image-optimization.plugin.ts index ab88161..cd6742f 100644 --- a/app/src/plugins/cloudflare/image-optimization.plugin.ts +++ b/app/src/plugins/cloudflare/image-optimization.plugin.ts @@ -19,22 +19,46 @@ const schema = s.partialObject({ type ImageOptimizationSchema = s.Static; 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}`); } diff --git a/app/src/ui/elements/media/Dropzone.tsx b/app/src/ui/elements/media/Dropzone.tsx index 27a0142..908c49a 100644 --- a/app/src/ui/elements/media/Dropzone.tsx +++ b/app/src/ui/elements/media/Dropzone.tsx @@ -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; + /** + * 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); }; diff --git a/app/src/ui/elements/media/DropzoneContainer.tsx b/app/src/ui/elements/media/DropzoneContainer.tsx index 2f695a9..46cef55 100644 --- a/app/src/ui/elements/media/DropzoneContainer.tsx +++ b/app/src/ui/elements/media/DropzoneContainer.tsx @@ -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; + /** + * Query to filter the media items + */ query?: RepoQueryIn; + /** + * Whether to use a random filename + * @default false + */ randomFilename?: boolean; } & Omit, "initialItems">; diff --git a/docs/content/docs/(documentation)/extending/plugins.mdx b/docs/content/docs/(documentation)/extending/plugins.mdx new file mode 100644 index 0000000..e18606c --- /dev/null +++ b/docs/content/docs/(documentation)/extending/plugins.mdx @@ -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: + + + +## 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( + +

Pages: {pages.length}

+
    + {pages.map((page: any) => ( +
  • {page.title}
  • + ))} +
+ , + ); + }); + }, +}); + +``` + +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: + + + +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`. + + + + + + + diff --git a/docs/content/docs/(documentation)/integration/(runtimes)/cloudflare.mdx b/docs/content/docs/(documentation)/integration/(runtimes)/cloudflare.mdx index 257d2a7..daeadea 100644 --- a/docs/content/docs/(documentation)/integration/(runtimes)/cloudflare.mdx +++ b/docs/content/docs/(documentation)/integration/(runtimes)/cloudflare.mdx @@ -123,9 +123,8 @@ import { serve } from "bknd/adapter/cloudflare"; export default serve({ // ... onBuilt: async (app) => { - // [!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. | | `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. | -| `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` @@ -172,76 +170,6 @@ export default serve({ }); ``` -### 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({ - // ... - 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 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 curl -H "x-cf-d1-session: " ... ``` + +## 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. + + + Make sure to not import from this file in your app, as this would include `wrangler` as a dependency. + + +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 +``` \ No newline at end of file diff --git a/docs/content/docs/(documentation)/meta.json b/docs/content/docs/(documentation)/meta.json index a476c95..bcd6ef0 100644 --- a/docs/content/docs/(documentation)/meta.json +++ b/docs/content/docs/(documentation)/meta.json @@ -18,6 +18,7 @@ "---Extending---", "./extending/config", "./extending/events", + "./extending/plugins", "---Integration---", "./integration/introduction", "./integration/(frameworks)/", diff --git a/docs/content/docs/(documentation)/usage/cli.mdx b/docs/content/docs/(documentation)/usage/cli.mdx index 2874b94..2ccd875 100644 --- a/docs/content/docs/(documentation)/usage/cli.mdx +++ b/docs/content/docs/(documentation)/usage/cli.mdx @@ -16,22 +16,24 @@ Here is the output: $ npx bknd Usage: bknd [options] [command] -⚡ bknd cli v0.16.0 +⚡ bknd cli v0.17.0 Options: -V, --version output the version number -h, --help display help for command Commands: - user create/update users, or generate a token (auth) - types [options] generate types - schema [options] get schema - run [options] run an instance - debug debug bknd - create [options] create a new project - copy-assets [options] copy static assets - config [options] get default config - help [command] display help for command + config [options] get app config + copy-assets [options] copy static assets + create [options] create a new project + debug debug bknd + mcp [options] mcp server stdio transport + run [options] run an instance + schema [options] get schema + sync [options] sync database + types [options] generate types + user [options] create/update users, or generate a token (auth) + help [command] display help for command ``` ## Starting an instance (`run`) diff --git a/docs/content/docs/(documentation)/usage/database.mdx b/docs/content/docs/(documentation)/usage/database.mdx index 475404f..febffdd 100644 --- a/docs/content/docs/(documentation)/usage/database.mdx +++ b/docs/content/docs/(documentation)/usage/database.mdx @@ -349,6 +349,91 @@ Note that we didn't add relational fields directly to the entity, but instead de manually. +### 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 To get type completion, there are two options: diff --git a/docs/content/docs/(documentation)/usage/elements.mdx b/docs/content/docs/(documentation)/usage/elements.mdx index d034a6e..239687f 100644 --- a/docs/content/docs/(documentation)/usage/elements.mdx +++ b/docs/content/docs/(documentation)/usage/elements.mdx @@ -44,16 +44,8 @@ export default function UserAvatar() { #### Props -- `initialItems?: xMediaFieldSchema[]`: Initial items to display, must be an array of media objects. -- `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 diff --git a/docs/content/docs/(documentation)/usage/sdk.mdx b/docs/content/docs/(documentation)/usage/sdk.mdx index a0c6f63..70c194a 100644 --- a/docs/content/docs/(documentation)/usage/sdk.mdx +++ b/docs/content/docs/(documentation)/usage/sdk.mdx @@ -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 } }); ``` +### `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`) 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 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 +``` + +### `media.getFileStream([filename])` + +To get a file stream directly, use the `getFileStream` method: + +```ts +const stream = await api.media.getFileStream("image.jpg"); +// ^? ReadableStream +``` + +### `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"); +``` +