Access to Table Metadata Problematic on Generic Type
bliles opened this issue · 4 comments
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.
Hi, just to be clear, can you explain what are the exact metadata you'd wish to access from table? is it tableName? or..
tableName, as well as the names of the partitionKey and sortKey. Thanks very much for thinking about this request.
So you guys want to synchronize DynamoDB changes to ES right?
-
have you considered about just using DynamoDB Stream? https://github.com/serverless-seoul/dynamorm-stream/blob/master/src/stream_handler.ts
-
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?
- 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()
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;
}
}