Entity TypeScript ORM
repository·main·Indexed 41 days ago
https://github.com/expo/entityA privacy-aware TypeScript ORM that integrates declarative authorization (Privacy Policies) directly into the data access layer. Entity ensures that data loading and permission checks are inseparable for CRUD operations. It features configurable storage via database adapters (such as Knex for Postgres), full-object read-through caching with Redis or local memory adapters, and utilizes Dataloader for in-memory batching and memoization.
What's inside Entity
- Entity is a privacy-aware data layer designed for defining, caching, and authorizing access to application data models. It functions as a TypeScript ORM that integrates authorization directly into the data access lifecycle, ensuring that CRUD operations cannot be performed without satisfying defined Privacy Policies.
Use @expo/entity-testing-utils for testing Entity-based applications
mainThe@expo/entity-testing-utilspackage provides a collection of utilities designed to simplify the testing process for applications built using theEntityframework. It helps developers write more robust tests by providing helpers specifically tailored to the Entity lifecycle and data patterns.Use @expo/entity-secondary-cache-local-memory for cross-request caching
mainThe
@expo/entity-secondary-cache-local-memorypackage provides a cross-request secondary cache for@expo/entityusing an LRU (Least Recently Used) strategy.Warning: This cache is shared between requests within the same Node.js process and is considered nonstandard. Use it with caution.
To implement it, you must:
- Create a concrete implementation of the
EntitySecondaryCacheLoaderabstract class. - Instantiate your loader with a
LocalMemorySecondaryEntityCache. - Provide an LRU cache instance via
GenericLocalMemoryCacher.createLRUCache.
import { LocalMemorySecondaryEntityCache, GenericLocalMemoryCacher } from '@expo/entity-secondary-cache-local-memory'; // 1. Implementation of EntitySecondaryCacheLoader (e.g., TestSecondaryLocalMemoryCacheLoader) // 2. Instantiate with LocalMemorySecondaryEntityCache and an LRU cache const secondaryCacheLoader = new TestSecondaryLocalMemoryCacheLoader( new LocalMemorySecondaryEntityCache( GenericLocalMemoryCacher.createLRUCache<LocalMemoryTestEntityFields>({}), ), LocalMemoryTestEntity.loader(viewerContext), ); // 3. Load entities const loadParams = { id: createdEntity.getID() }; const results = await secondaryCacheLoader.loadManyAsync([loadParams]);- Create a concrete implementation of the
Core features of Entity
mainEntity provides several key capabilities for managing application data:
- TypeScript ORM: A type-safe object-relational mapper.
- Declarative Privacy Policies: Actor pre-authorization for all CRUD operations using rules that define access.
- Configurable Storage: Support for different storage backends via Database Adapters (e.g., Knex).
- Configurable Caching: Optional, full-object read-through caching using Cache Adapters (e.g., Redis, Local Memory).
- Batching & Memoization: Uses Dataloader for in-memory batching and read-through caching to optimize data fetching.
How Privacy Policies and authorization work
mainUnlike traditional authorization patterns where you load data and then manually check permissions (which is error-prone), Entity combines data loading and authorization into a single step.
You define a
PrivacyPolicyfor your model containing rules (e.g.,readRules). When you request data using aloaderinitialized with aViewerContext, the framework automatically applies these rules to ensure theviewerhas permission to access the requestedobjectbefore returning it.Conceptual Example
class PhotoPrivacyPolicy { // Define which rules allow a user to read a photo readRules = [new AllowIfOwnerRule(), new AllowIfOrganizationPermissionRule()]; } // In your application logic (e.g., a view or controller) async function get_photo_page(viewer: ViewerContext): Promise<string> { // The loader is initialized with the viewer's context // Authorization is performed automatically during the load const photo = await PhotoEntity.loader(viewer).loadById(id); return render_html(photo); }class PhotoPrivacyPolicy { readRules = [new AllowIfOwnerRule(), new AllowIfOrganizationPermissionRule()]; } // in the view, for example async function get_photo_page(viewer: ViewerContext): string { const photo = await PhotoEntity.loader(viewer).loadById(id); return render_html(photo); }When to use the local-memory cache adapter
mainThe local-memory cache is an in-memory cache, meaning it is local to the specific Node.js process and cannot be invalidated across multiple machines.
Recommended Use Cases
- When objects are mostly immutable.
- When reading a stale object for a short TTL (Time To Live) is acceptable.
- When you have a low TTL setting.
- When you want to reduce trips to your cache cluster or backing datastore for frequently fetched objects.
When NOT to use this cache
- If the objects stored are mutable.
- If your application cannot tolerate reading stale data.
- If you require cross-machine cache invalidation.
Core concepts of @expo/entity
mainThe
@expo/entitylibrary is built around four primary abstractions that manage the lifecycle of persisted data:- Entity: Represents a single "row" or object of data in a database. An entity instance is tied to a
ViewerContext, which defines the permissions of the user attempting to access it. - EntityPrivacyPolicy: A declarative way to define authorization rules for Create, Read, Update, and Delete (CRUD) operations on an entity.
- EntityLoader: Orchestrates the process of loading entities. It handles fetching data from the database, managing caches, and verifying authorization via the privacy policy.
- EntityMutator: Orchestrates changes to entities. It handles database writes, ensures the cache is invalidated when data changes, and verifies authorization for mutations and deletions.
- Entity: Represents a single "row" or object of data in a database. An entity instance is tied to a
Configure jscodeshift for TypeScript
mainTo run
@expo/entity-codemodon TypeScript projects, you need to configurejscodeshiftto use the TypeScript parser and extensions. Add the following script to yourpackage.json:"jscodeshift": "jscodeshift --extensions=ts --parser=ts"Use the Knex database adapter with @expo/entity
mainThe
@expo/entity-database-adapter-knexpackage provides a Knex-based database adapter for@expo/entity. Currently, this adapter is only supported when using thepg(Postgres) client.To use it, you must instantiate a
knexinstance with a Postgres connection and then provide it to theEntityCompanionProviderby mapping thepostgresflavor to thePostgresEntityDatabaseAdapterand its correspondingPostgresEntityQueryContextProvider.import { knex, Knex } from 'knex'; // 1. Create your Knex instance (Postgres only) const knexInstance = knex({ client: 'pg', connection: { user: process.env['PGUSER'], password: process.env['PGPASSWORD'], host: process.env['PGHOST'], port: parseInt(process.env['PGPORT']!, 10), database: process.env['PGDATABASE'], }, }); // 2. Register the adapter during EntityCompanionProvider instantiation export const createDefaultEntityCompanionProvider = ( metricsAdapter: IEntityMetricsAdapter = new NoOpEntityMetricsAdapter() ): EntityCompanionProvider => { return new EntityCompanionProvider( metricsAdapter, { // add the knex database adapter flavor ['postgres']: { adapter: PostgresEntityDatabaseAdapter, queryContextProvider: new PostgresEntityQueryContextProvider(knexInstance), }, }, { // ... other options } ); };Use @expo/entity-codemod for upgrades
mainThe
@expo/entity-codemodpackage providesjscodeshifttransformations to automate codebase changes during@expo/entityupgrades. These codemods programmatically apply large-scale changes, reducing the need for manual updates across multiple files.To use these codemods, you must have the
jscodeshiftCLI installed and configured to handle TypeScript files.# 1. Install the codemod package as a dev dependency yarn add -D @expo/entity-codemod # 2. Run the transformation using jscodeshift # Replace the path to the transform file with the specific version you need yarn jscodeshift src -t node_modules/@expo/entity-codemod/build/v0.39.0-v0.40.0.jsUse the Redis cache adapter with @expo/entity
mainTo use Redis as a cache for
@expo/entity, you must provide aRedisCacheAdapterProviderwithin the configuration object passed to theEntityCompanionProviderconstructor. This adapter usesioredisunder the hood.When configuring the
RedisCacheAdapterProvider, you need to provide a context object containing:redisClient: An instance ofioredis.cacheKeyDelimiter: A string used to separate parts of the cache key.cacheKeyPrefix: A string prefix for all cache keys.ttlSecondsPositive: TTL in seconds for positive cache entries.ttlSecondsNegative: TTL in seconds for negative cache entries.
import Redis from 'ioredis'; import { EntityCompanionProvider } from '@expo/entity'; import { RedisCacheAdapterProvider } from '@expo/entity-cache-adapter-redis'; const genericRedisCacherContext = { redisClient: new Redis(new URL(process.env['REDIS_URL']!).toString()), cacheKeyDelimiter: ':', cacheKeyPrefix: 'ent-', ttlSecondsPositive: 86400, // 1 day ttlSecondsNegative: 600, // 10 minutes }; export const createDefaultEntityCompanionProvider = ( metricsAdapter: IEntityMetricsAdapter = new NoOpEntityMetricsAdapter() ): EntityCompanionProvider => { return new EntityCompanionProvider( metricsAdapter, { // ... other EntityCompanionProvider options }, { ['redis']: { cacheAdapterProvider: new RedisCacheAdapterProvider(genericRedisCacherContext), }, } ); };Install @expo/entity-cache-adapter-local-memory
mainThe@expo/entity-cache-adapter-local-memorypackage provides a cross-request LRU (Least Recently Used) cache adapter for@expo/entity. It is designed to share a cache between requests within a single Node.js process.