Dynamic (and safe) DynamoDB DSL

Punchcard uses TypeScript’s Advanced Types to derive a DynamoDB DSL from Shapes. This DSL simplifies the code required to marshal and unmarshal items from the DynamoDB format and build Condition, Query and Update Expressions.

To demonstrate, let’s create a DynamoDB.Table “with some Shape”:

class TableRecord extends Type({
  id: string,
  count: integer
}) {}

const table = new DynamoDB.Table(stack, 'my-table', {
  data: TableRecord
  key: {
    partition: 'id'
  }
}, Build.lazy(() => ({
  billingMode: BillingMode.PAY_PER_REQUEST
})));

Table Type

The Table API is derived from the definition encoded within the DynamoDB.Table type, containing the partition key ('id'), sort key (undefined) and the Shape of an item.

DynamoDB.Table<TableRecord, { partition: 'id' }>

This model enables a dynamic interface to DynamoDB while also maintaining type-safety.

Get Item

When getting an item from DynamoDB, there is no need to use AttributeValues such as { S: 'my string' }. You simply use ordinary javascript types:

const item = await table.get({
  id: 'state'
});
item.id; // string
item.count; // number
//item.missing // does not compile

Put Item

Putting an item is as simple as putting to an ordinary Map.

await table.put(new TableRecord({
  id: 'state',
  count: 1,
  //invalid: 'value', // does not compile
}));

A Condition Expression can optionally be included with if:

await table.put(new TableRecord({
  id: 'state',
  count: 1
}), {
  if: _ => _.count.equals(0)
});

Which automatically (and safely) renders the following expression:

{
  ConditionExpression: '#0 = :0',
  ExpressionAttributeNames: {
    '#0': 'count'
  },
  ExpressionAttributeValues: {
    ':0': {
      N: '0'
    }
  }
}

Update Item

Build Update Expressions by assembling an array of actions:

await table.update({
  id: 'state'
}, {
  actions: _ => [
    _.count.increment(1)
  ]
});

Which automaticlaly (and safely) renders the following expression:

{
  UpdateExpression: '#0 = #0 + :0',
  ExpressionAttributeNames: {
    '#0': 'count'
  },
  ExpressionAttributeValues: {
    ':0': {
      N: '1'
    }
  }
}

Query

If you also specified a sortKey for your Table:

const table = new DynamoDB.Table(stack, 'my-table', {
  data: TableRecord,
  key: {
    partition: 'id',
    sort: 'count'
  }
});

(Where the Table type looks like this)

DynamoDB.Table<TableRecord, { partition: 'id', sort: 'count' }>

Then, you can also build typesafe Query Expressions:

await table.query({
  id: 'id',
  count: _ => _.greaterThan(1)
});

Which automatically (and safely) renders the following low-level expression:

{
  KeyConditionExpression: '#0 = :0 AND #1 > :1',
  ExpressionAttributeNames: {
    '#0': 'id',
    '#1': 'count'
  },
  ExpressionAttributeValues: {
    ':0': {
      S: 'id'
    },
    ':1': {
      N: '1'
    }
  }
}

Check out the DynamoDB example app for more magic.

Next

Next: Stream Processing