Entity TypeScript ORM

repository·main·Indexed 41 days ago

https://github.com/expo/entity

A 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.

Tokens
28.8K
Snippets
75
Records
106
Agent score
50%

What's inside Entity

  1. What is Entity?

    main
    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.
  2. Use @expo/entity-testing-utils for testing Entity-based applications

    main
    The @expo/entity-testing-utils package provides a collection of utilities designed to simplify the testing process for applications built using the Entity framework. It helps developers write more robust tests by providing helpers specifically tailored to the Entity lifecycle and data patterns.
  3. Use @expo/entity-secondary-cache-local-memory for cross-request caching

    main

    The @expo/entity-secondary-cache-local-memory package provides a cross-request secondary cache for @expo/entity using 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:

    1. Create a concrete implementation of the EntitySecondaryCacheLoader abstract class.
    2. Instantiate your loader with a LocalMemorySecondaryEntityCache.
    3. 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]);
  4. Core features of Entity

    main

    Entity 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.
  5. How Privacy Policies and authorization work

    main

    Unlike 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 PrivacyPolicy for your model containing rules (e.g., readRules). When you request data using a loader initialized with a ViewerContext, the framework automatically applies these rules to ensure the viewer has permission to access the requested object before 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);
    }
  6. When to use the local-memory cache adapter

    main

    The 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.

    • 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.
  7. Core concepts of @expo/entity

    main

    The @expo/entity library is built around four primary abstractions that manage the lifecycle of persisted data:

    1. 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.
    2. EntityPrivacyPolicy: A declarative way to define authorization rules for Create, Read, Update, and Delete (CRUD) operations on an entity.
    3. EntityLoader: Orchestrates the process of loading entities. It handles fetching data from the database, managing caches, and verifying authorization via the privacy policy.
    4. EntityMutator: Orchestrates changes to entities. It handles database writes, ensures the cache is invalidated when data changes, and verifies authorization for mutations and deletions.
  8. Use the Knex database adapter with @expo/entity

    main

    The @expo/entity-database-adapter-knex package provides a Knex-based database adapter for @expo/entity. Currently, this adapter is only supported when using the pg (Postgres) client.

    To use it, you must instantiate a knex instance with a Postgres connection and then provide it to the EntityCompanionProvider by mapping the postgres flavor to the PostgresEntityDatabaseAdapter and its corresponding PostgresEntityQueryContextProvider.

    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
        }
      );
    };
  9. Use @expo/entity-codemod for upgrades

    main

    The @expo/entity-codemod package provides jscodeshift transformations to automate codebase changes during @expo/entity upgrades. These codemods programmatically apply large-scale changes, reducing the need for manual updates across multiple files.

    To use these codemods, you must have the jscodeshift CLI 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.js
  10. Use the Redis cache adapter with @expo/entity

    main

    To use Redis as a cache for @expo/entity, you must provide a RedisCacheAdapterProvider within the configuration object passed to the EntityCompanionProvider constructor. This adapter uses ioredis under the hood.

    When configuring the RedisCacheAdapterProvider, you need to provide a context object containing:

    • redisClient: An instance of ioredis.
    • 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),
          },
        }
      );
    };