Search code examples
reflectionnestjstypeorm

Index specific TypeORM entity fields


I'm building an indexing service to index various entity fields. I'd like to be able to add a decorator like @Searchable or similar to the fields I want to decorate and then using some kind of indexing service (and probably reflection) find all the entity classes (those with the @Entity decorator), then in each one of those, gather up all of the fields with @Searchable decorators applied to them.

The issue is - I'm running into issues trying to use reflection to find the entities an it's starting to feel like I'm approaching the issue wrong.

I've tried putting the smarts in the decorator and having it register with some kind of external service every time it's called, but this feels very fragile and requires a singleton of that service to be available to "hold" the data.


Solution

  • So the (partial) solution ended up being the following code:

    // Turn entities with decorated field into SELECT queries
    const data = this.ds.entityMetadatas
    // Returns only the models that have at least one decorated column
    .filter(({ isJunction, target, columns }) => {
      if (isJunction) return false;
      return columns.some(
        ({ propertyName }) => Reflect
          .hasOwnMetadata(SEARCHABLE_DECORATOR_KEY, target, propertyName)
      );
    })
    .map(({ target, tableName, columns, primaryColumns }) => {
      const query = this.ds.createQueryBuilder()
        .from(tableName, tableName.charAt(0));
      columns.filter(({ propertyName, isPrimary }) =>
        // Returns only the columns that are
        // 1) Decorated and
        // 2) not primary columns (that way we're not
        // indexing primary key columns)
        Reflect.hasOwnMetadata(SEARCHABLE_DECORATOR_KEY, target, propertyName)
        && !isPrimary,
      ).concat(primaryColumns)
       // Uses the query builder to turn those columns into a SELECT query
      .forEach(({ propertyName }) => query.addSelect(`"${propertyName}"`))
      return query;
    });
    

    This returns an (unexecuted) query per table that selects only the decorated columns. So for the following entity:

    @Entity()
    class User {
      @PrimaryColumn()
      id: string;
    
      @Searchable
      @Column()
      name: string;
    
      @Column()
      email: string;
    }
    

    it would generate the following SQL query:

    SELECT id, name FROM "user";
    

    Note that id is still selected because we need that later in order to associate the key/value with the PK value (that way we can retrieve records by PK, which is kinda the point of this after all, right?)

    I also have my @Searchable decorator - the code for which looks like this:

    export const SEARCHABLE_DECORATOR_KEY = Symbol('searchable');
    
    /**
     * Searchable decorator used to make an entity property visible to the search
     * spider
     */
    export const Searchable = (target:Object, property: string) => {
      Reflect
        .defineMetadata(SEARCHABLE_DECORATOR_KEY, true, target.constructor, property);
    }
    

    Note the target.constructor there. For reasons I don't fully understand, if you use target instead of target.constructor - it returns the entity parent class (if there is one) instead of the actual entity you're decorating. So if you have:

    
    class User extends BaseEntity {
      @Searchable
      @Column()
      id: string;
    }
    

    then target would equal BaseEntity while target.entity gives class User extends BaseEntity which is what we want here.