Entity / foldkit-entity

Relations and derived values

ts
import { Schema } from 'effect'
import { Derived, Entity, Relation } from 'foldkit-entity'

const Author = Entity.define('Author', Schema.Struct({ id: Schema.String, name: Schema.String }))
const Comment = Entity.define('Comment', Schema.Struct({ id: Schema.String, body: Schema.String }))
const Post = Entity.define(
  'Post',
  Schema.Struct({ id: Schema.String, title: Schema.String, published: Schema.Boolean }),
).pipe(Entity.derived({ commentCount: Derived.make(Schema.Number) }))

const Blog = Entity.relate(
  { Author, Post, Comment },
  {
    Post: {
      author: Relation.one(Author),
      editor: Relation.one(Author, { optional: true }),
      comments: Relation.many(Comment),
    },
    Comment: { post: Relation.one(Post) },
  },
)

Blog.Post.schema // Schema.Struct of id, title, published: relations are not in it
Blog.Post.fields.title.schema // Schema.String
Blog.Post.relations.editor.optional // true
Blog.Post.derived.commentCount.schema // Schema.Number
Blog.Post.relations.comments.target() // Blog.Comment
Blog.Post.relations.comments.target().relations.post.target() // Blog.Post, fully typed
Object.keys(Blog.Post.members) // id, title, published, commentCount, author, editor, comments
  • Entity.define generates one Field per schema property and creates the identity. Calling it twice with the same name makes two different entities.
  • Derived.make states a readable value's schema and nothing about how it is produced. Entity.derived is a pipe step because it concerns one entity.
  • Entity.relate takes the entities and every relation between them, and returns the same entities with relations filled in. Use the returned ones (Blog.Post); the Post you passed in still has no relations.
  • Relation.one / Relation.many state what the owner sees. Post.author: one and Author.posts: many together are the familiar one-to-many; neither side names it.
  • target() returns the related entity, so relations can be followed from there, around a cycle too.

Entity.relate throws, and the types reject, a relation key that is already a member, an owner that is not in the first argument, and a target that is not in the first argument.

Why relations are declared in one step

Entities point at each other: a Post has Comments and a Comment has a Post. If each entity declared its own relations, Post would be typed in terms of Comment and Comment in terms of Post, and TypeScript cannot infer two constants that way. Declaring the relations over entities that already exist avoids the cycle, and lets relate check every target up front.