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, commentsEntity.definegenerates oneFieldper schema property and creates the identity. Calling it twice with the same name makes two different entities.Derived.makestates a readable value's schema and nothing about how it is produced.Entity.derivedis a pipe step because it concerns one entity.Entity.relatetakes the entities and every relation between them, and returns the same entities withrelationsfilled in. Use the returned ones (Blog.Post); thePostyou passed in still has no relations.Relation.one/Relation.manystate what the owner sees.Post.author: oneandAuthor.posts: manytogether 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.