Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Access to Table Metadata Problematic on Generic Type #29

Open
bliles opened this issue Mar 25, 2021 · 8 comments
Open

Access to Table Metadata Problematic on Generic Type #29

bliles opened this issue Mar 25, 2021 · 8 comments

Comments

@bliles
Copy link

bliles commented Mar 25, 2021

Due to the fact that table metadata is static on a type, it is not possible to obtain the table metadata on an instance of a Table object (without accessing private members).

We have a need to write to both DynamoDB and Elasticsearch, I would like to maintain this code in one place, but doing that effectively is difficult without access to the table metadata.

Possible solutions that I can easily see:
Make it possible to get the table metadata from an instance.
Change Query.Writer.tableClass from private to protected so we can extend Query.Writer.

I know that what I'm doing may be an edge case, but it's also frustrating to not be able to extend something so that I can implement a desired functionality in one place instead of in every place that we call Table.save.

@breath103
Copy link
Contributor

Hi, just to be clear, can you explain what are the exact metadata you'd wish to access from table? is it tableName? or..

@bliles
Copy link
Author

bliles commented Mar 26, 2021

tableName, as well as the names of the partitionKey and sortKey. Thanks very much for thinking about this request.

@breath103
Copy link
Contributor

breath103 commented Mar 28, 2021

So you guys want to synchronize DynamoDB changes to ES right?

  1. have you considered about just using DynamoDB Stream? https://github.com/serverless-seoul/dynamorm-stream/blob/master/src/stream_handler.ts

  2. code wise, why does it required to be on instance level?

function updatePost(post: Post, title: string) {
   post.title = title
   await post.save();

   ES.updateDocument(Post.metadata.name, { id: post.id, title: post.title... }); 
}

or are you dealing with more complex scenario?

  1. since developer, not the library defines tableName / PK / SK, you can just do
const tableName = "tableName";
@Decorator.Table({ name: tableName })
export class Post extends Table {
  @Decorator.HashPrimaryKey("id")
  public static readonly primaryKey: Query.HashPrimaryKey<Post, string>;

  public get tableName() { return tableName; }
  public get primaryKeyName() { return "id"; }
}

this is essentially samething as you accessing Post.metadata()

@bliles
Copy link
Author

bliles commented Mar 29, 2021

Yes you're absolutely correct, however that all assumes that you are working with an instance of a specific entity. Here is what I am trying to do:

I have a BaseEntity class that extends Table. All of my models extend BaseEntity. So I want to create a .save method on BaseEntity that calls super.save() and then in the local environment does the work to save to Elasticsearch. In our production environment, we are using dynamo streams and a lambda to keep Elasticsearch in sync.

In order to make a change in one place in the code to add this sync to Elasticsearch, I want to be able to work with anything that extends Table, which prevents me from doing something like you mentioned and adding methods to each of the models.

I could also add code to each of the services that work with each type, but again I'm trying to have this code in one place.

Let me show you the hack I'm currently using, perhaps that will make it clear.

import { Table, Query, Metadata } from '@serverless-seoul/dynamorm';
import { ElasticsearchService } from '@api-services/ElasticsearchService';

import { EntityKey, KeyType } from './interfaces/EntityKey.interface';

export abstract class BaseEntity extends Table {

  static readonly environmentName = process.env.ENVIRONMENT?.toLowerCase() || 'local';

  /**
   * Dynamo tables must be unique to the region, so we are
   * setting a namespace here.
   */
  static readonly tablePrefix = `${BaseEntity.environmentName}`;

  entityStatus!: number;

  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  private static _getIndexName(tableClass: any): string {
    return tableClass.modelName.toLowerCase();
  }

  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  private static _getEntityKey(tableClass: any): EntityKey {
    const metadata: Metadata.Table.Metadata = tableClass.__metadata;

    return {
      type: metadata.primaryKey.type == 'HASH' ? KeyType.Partition : KeyType.PartitionAndSort,
      partitionKey: metadata.primaryKey.hash.propertyName,
      sortKey: metadata.primaryKey.type == 'FULL' ? metadata.primaryKey.range.propertyName : '',
    };
  }

  public static getElasticsearchId<T extends Table>(entity: T, key: EntityKey) {
    let id;
    if (key.type == KeyType.Partition) {
      id = entity.getAttribute(key.partitionKey);
    } else {
      id = entity.getAttribute(key.partitionKey) + '-sk-' +
        entity.getAttribute(key.sortKey);
    }
    return id;
  }

  public async save<T extends Table>(this: T, options?: Partial<{
    condition?: Query.Conditions<T> | Array<Query.Conditions<T>>;
  }>): Promise<Table> {
    const result = await super.save(options);

    if (BaseEntity.environmentName == 'local') {
      // WARNING! Horrible hack here to get the table metadata.
      // Dynamorm doesn't expose the metadata in a way that we can consume in this
      // generic function since we only have an instance of the entity. The table
      // metadata is a static property of the entity type class and therefore not
      // something we can obtain through proper methods.
      // We consider this acceptable in this case since this code will not
      // be used in production.

      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      const tableClass = (this as any).__writer.tableClass;
      const indexName = BaseEntity._getIndexName(tableClass);
      const id = BaseEntity.getElasticsearchId(this, BaseEntity._getEntityKey(tableClass));

      const esClient = await ElasticsearchService.createESClient();
      const esService = new ElasticsearchService(esClient);

      await esService.indexEntity(indexName, id, this);
    }

    return result;
  }

  public async delete<T extends Table>(this: T, options?: Partial<{
      condition?: Query.Conditions<T> | Array<Query.Conditions<T>>;
  }>): Promise<void> {
    const result = await super.delete(options);

    if (BaseEntity.environmentName == 'local') {
      // WARNING! Horrible hack here to get the table metadata.
      // Dynamorm doesn't expose the metadata in a way that we can consume in this
      // generic function since we only have an instance of the entity. The table
      // metadata is a static property of the entity type class and therefore not
      // something we can obtain through proper methods.
      // We consider this acceptable in this case since this code will not
      // be used in production.

      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      const tableClass = (this as any).__writer.tableClass;
      const indexName = BaseEntity._getIndexName(tableClass);
      const id = BaseEntity.getElasticsearchId(this, BaseEntity._getEntityKey(tableClass));

      const esClient = await ElasticsearchService.createESClient();
      const esService = new ElasticsearchService(esClient);

      await esService.deleteDocument(indexName, id);
    }

    return result;
  }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants
@breath103 @bliles and others