serverless-seoul / dynamorm

AWS DynamoDB ORM in Typescript
Apache License 2.0
55 stars 4 forks source link

Maintain class structure on stringify #43

Closed mrgatto closed 3 years ago

mrgatto commented 3 years ago

Is there a way to maintain the class structure on stringify an object ?

@Decorator.Table
export class Entity extends Table {
    @Decorator.Attribute({ name: "MY_ID" })
    code: number
}

const entity = new Entity()
entity.code = 10
json = JSON.stringify(entity)

What I expected:

{
 code: 10
}

What it actualy returns:

{
 attributes: {
    MY_ID: 10
 },
  __writer: null
}
mooyoul commented 3 years ago

Unfortunately, We don't provide a built-in json serializer.

As you may know, Attributes can be defined as private field and implement custom serde function using getter/setters. For example:

@Decorator.Table()
class Room extends Table {
  // ... truncated

  // Date serde
  @Decorator.Attribute()
  private createdAtTimestamp: number;

  public get createAt(): Date {
    return new Date(this.createdAtTimestamp);
  }

  public set createdAt(date: Date) {
    this.createdAtTimestamp = date.getTime();
  }

  // Binary encoded value serde
  //
  // CREATE_INSTANT_INVITE | 0x01 (1 << 0) | Allows creation of instant invites
  // EMBED_LINKS                     | 0x02 (1 << 1) | Links sent by users with this permission will be auto-embedded
  // ATTACH_FILES                    | 0x04 (1 << 2) | Allows creation of instant invites
  @Decorator.Attribute()
  public encodedPermissions: number;

  public get inviteAllowed(): boolean {
    return Boolean(this.encodedPermissions & 0x01);
  }
  public set inviteAllowed(allow: boolean) {
    this.encodedPermissions = allow 
     ? this.encodedPermissions | 0x01
     : this.encodedPermissions & ~0x01
  }
}

Dynamorm know which class properties are DynamoDB Attribute, but there's no way to figure out whether given property is public or not. These limitations make it hard to make decisions - That's why we don't provide a built-in json serializer. you must implement your own toJSON method to convert given Entity instance to plain object.

For example:

@Decorator.Table
export class Entity extends Table {
    @Decorator.Attribute({ name: "MY_ID" })
    code: number

  public toJSON() {
    return { 
      code: this.code,
    };
  }
}

const entity = new Entity()
entity.code = 10
const json = JSON.stringify(entity) // will be `{"code": 10}`. toJSON method will be automatically called by JSON.stringify.

const obj = entity.toJSON(); // of course you can use serialized object if needed
mrgatto commented 3 years ago

Ok, I ended up with a generic implementation.

public toJSON(): T {
    const tableClass = (this as any).writer.tableClass

    const model = {}
    tableClass.metadata.attributes.forEach(atr => {
         model[atr.propertyName] = this.getAttribute(atr.name)
    })

     return <T>model
}

And to not expose internal attributes of the lastEvaluatedKey:

const result = new APIQueryResponse()
result.content = ... // toJSON of records
result.count = queryResult.count

const lastKeys = queryResult.lastEvaluatedKey || {}
for (const key of Object.keys(lastKeys)) {
  const tableClass = (records[0] as any).writer.tableClass
  const property = tableClass.metadata.attributes.find(atr => atr.name == key)

  result.lastEvaluatedKey[property.propertyName] = lastKeys[key]
}