diff --git a/app/src/auth/AppAuth.ts b/app/src/auth/AppAuth.ts index 4ee9a8eb..a0c60727 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 4f25aebf..06483f58 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 bd6515f0..46902cd2 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 4cba790f..0971187a 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/docs/content/docs/(documentation)/usage/database.mdx b/docs/content/docs/(documentation)/usage/database.mdx index 475404f7..febffddb 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: