Entity Store
EntityStore[F] manages entity loading for authorization requests.
Overview
Building an EntityStore
- Scala 3
- Scala 2
import cedar4s.entities.EntityStore
import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global
// Import generated entity and ID types
import com.example.myapp.cedar.Entities
import com.example.myapp.cedar.EntityIds.{UserId, FolderId, DocumentId}
val store: EntityStore[Future] = EntityStore.builder[Future]()
.register[Entities.User, UserId](new UserFetcher(db))
.register[Entities.Folder, FolderId](new FolderFetcher(db))
.register[Entities.Document, DocumentId](new DocumentFetcher(db))
.build()
import cedar4s.entities.EntityStore
import scala.concurrent.Future
import scala.concurrent.ExecutionContext
import scala.concurrent.ExecutionContext.Implicits.global
// Import generated entity and ID types
import com.example.myapp.cedar.Entities
import com.example.myapp.cedar.EntityIds.{UserId, FolderId, DocumentId}
val store: EntityStore[Future] = EntityStore.builder[Future]()
.register[Entities.User, UserId](new UserFetcher(db))
.register[Entities.Folder, FolderId](new FolderFetcher(db))
.register[Entities.Document, DocumentId](new DocumentFetcher(db))
.build()
Each register call associates an entity type with its fetcher, using typed IDs. The registration requires an implicit CedarEntityType.Aux[A, Id] which ensures type-safe matching between the entity's ID type and the fetcher's ID type. This is automatically available for generated entity classes.
Type-Safe Registration
The register method uses CedarEntityType.Aux[A, Id] to ensure compile-time type safety:
def register[A, Id](fetcher: EntityFetcher[F, A, Id])(
implicit ev: CedarEntityType.Aux[A, Id],
bij: Bijection[String, Id]
): EntityStoreBuilder[F]
This means:
- The entity type
Amust have aCedarEntityTypeinstance (automatically generated for Cedar entities) - The fetcher's ID type
Idmust match the entity's ID type (enforced byAux[A, Id]) - A
Bijection[String, Id]must be available to convert between String and ID types (automatically generated for newtype IDs)
The compiler will reject mismatched types:
// This won't compile - UserId doesn't match DocumentId
.register[Entities.Document, UserId](documentFetcher) // ❌ Compile error
// This works - types match
.register[Entities.Document, DocumentId](documentFetcher) // ✅
Key Methods
trait EntityStore[F[_]] {
// Load entities for a single authorization request
def loadForRequest(principal: CedarPrincipal, resource: ResourceRef): F[CedarEntities]
// Load entities for batch authorization (multiple resources)
def loadForBatch(principal: CedarPrincipal, resources: Seq[ResourceRef]): F[CedarEntities]
// Load a single entity
def loadEntity(entityType: String, entityId: String): F[Option[CedarEntity]]
// Load multiple entities
def loadEntities(uids: Set[CedarEntityUid]): F[CedarEntities]
// Load entity with parent chain (for .on(id) syntax)
def loadEntityWithParents(entityType: String, entityId: String): F[Option[(CedarEntity, List[(String, String)])]]
}
Usage with CedarSession
EntityStore is typically used with CedarRuntime and CedarSession:
import cedar4s.client.{CedarEngine, CedarRuntime}
import com.example.myapp.cedar.MyApp
// Principal resolver - returns principal entity
def resolvePrincipal(principal: Principal): Future[Option[MyApp.Entity.User]] = {
principal match {
case MyApp.Principal.User(userId) =>
// Load user from database
userRepo.find(userId.value).map { user =>
Some(MyApp.Entity.User(
id = userId,
email = user.email,
name = user.name
))
}
case _ => Future.successful(None)
}
}
// Create runtime with explicit principal type
val runtime = CedarRuntime[Future, MyApp.Entity.User](
engine,
store,
CedarRuntime.resolverFrom(resolvePrincipal)
)
def sessionFor(currentUser: User): CedarSession[Future] =
runtime.session(MyApp.Principal.User(UserId(currentUser.id)))
Adding Caching
Add a cache for better performance:
- Scala 3
- Scala 2
import cedar4s.caffeine.{CaffeineEntityCache, CaffeineCacheConfig}
val cache = CaffeineEntityCache[Future](CaffeineCacheConfig.default)
val store = EntityStore.builder[Future]()
.register[Entities.User, UserId](new UserFetcher(db))
.register[Entities.Document, DocumentId](new DocumentFetcher(db))
.withCache(cache)
.build()
import cedar4s.caffeine.{CaffeineEntityCache, CaffeineCacheConfig}
val cache = CaffeineEntityCache[Future](CaffeineCacheConfig.default)
val store = EntityStore.builder[Future]()
.register[Entities.User, UserId](new UserFetcher(db))
.register[Entities.Document, DocumentId](new DocumentFetcher(db))
.withCache(cache)
.build()
See Caching for configuration details.
Accessing Cache Management
Use .buildCaching() to get a store with cache management methods:
val cachingStore = EntityStore.builder[Future]()
.register[Entities.User, UserId](new UserFetcher(db))
.withCache(cache)
.buildCaching() // Returns CachingEntityStoreGeneric
// Invalidation methods are available directly on the caching store
cachingStore.invalidate(CedarEntityUid("MyApp::User", "user-123"))
cachingStore.invalidateType("MyApp::User")
cachingStore.invalidateAll()
// Or access the underlying cache via getCache
cachingStore.getCache.invalidateWithCascade(uid, EntitySchema.childrenOf)
Lifecycle
EntityStore is typically a singleton in your application:
// At application startup
val entityStore = EntityStore.builder[Future]()
.register[Entities.User, UserId](new UserFetcher(db))
.register[Entities.Folder, FolderId](new FolderFetcher(db))
.register[Entities.Document, DocumentId](new DocumentFetcher(db))
.withCache(CaffeineEntityCache[Future](CaffeineCacheConfig.default))
.build()
// Per-request: create CedarSession with shared EntityStore
def createCedarSession(user: User): CedarSession[Future] =
runtime.session(Principals.User(UserId(user.id)))