npm package: @nova-odm/expressions
Description: Composable expression objects for Amazon DynamoDB
Author: Arseny Yankovski
Latest version: 0.8.7

# Nova ODM / Amazon DynamoDB Expressions

[![Apache 2 License](https://img.shields.io/github/license/ArsenyYankovsky/nova-odm.svg?style=flat)](https://www.apache.org/licenses/LICENSE-2.0)

This library provides a number of abstractions designed to make dealing with
Amazon DynamoDB expressions easier and more natural for JavaScript developers.

## Attribute paths

The `AttributePath` class provides a simple way to write [DynamoDB document
paths](http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.Attributes.html#Expressions.Attributes.NestedElements.DocumentPathExamples).
If the constructor receives a string, it will parse the path by scanning for
dots (`.`), which designate map property dereferencing and left brackets (`[`),
which designate list attribute dereferencing. For example,
`'ProductReviews.FiveStar[0].reviewer.username'` would be understood as
referring to the `username` property of the `reviewer` property of the first
element of the list stored at the `FiveStar` property of the top-level
`ProductReviews` document attribute.

If a property name contains a left bracket or dot, it may be escaped with a
backslash `\`. For example, `Product\.Reviews` would be interpreted as a single
top-level document attribute rather than as a map property access.

## Attribute values

This library will marshall values encountered using runtime type detection. If
you have a value that is already in the format expected by DynamoDB, you may
pass it to the `AttributeValue` constructor to direct other expression helpers
not to marshall the value further.

## Condition expressions

DynamoDB condition expressions may come in the form of a function call or as the
combination of values and infix operators. This library therefore defines a
`ConditionExpression` as the union of [`FunctionExpression`](#function-expressions)
and a tagged union of the expression operator types. Expressions may be compound
or simple.

### Compound expressions

These expressions envelope one or more simple expressions and are true or false
based on the value of the subexpressions they contain. The recognized compound
expressions are:

#### `And` expressions

Asserts that all of the subexpressions' conditions are satisfied.

```typescript
import {ConditionExpression} from '@nova-odm/expressions';

const andExpression: ConditionExpression = {
    type: 'And',
    conditions: [
        // one or more subexpressions
    ]
};
```

#### `Or` expressions

Asserts that at least one of the subexpressions' conditions are satisfied.

```typescript
import {ConditionExpression} from '@nova-odm/expressions';

const orExpression: ConditionExpression = {
    type: 'Or',
    conditions: [
        // one or more subexpressions
    ]
};
```

#### `Not` expressions

Asserts that the subexpression's condition is not satisfied.

```typescript
import {ConditionExpression} from '@nova-odm/expressions';

const notExpression: ConditionExpression = {
    type: 'Not',
    condition: {
        type: 'LessThan',
        subject: 'foo',
        object: 100
    }
};
```

### Simple expressions

These expressions make an assertion about a property in a DynamoDB object known
as the expression's `subject`. The `subject` must be a string or an [attribute
path](#attribute-paths).

The particular assertion used is referred to in this library as a
`ConditionExpressionPredicate`. A predicate may be declared separately from its
`subject` but only becomes a valid expression when paired with a `subject`. The
supported condition expression predicates are:

#### `Equals` expression predicate

Creates a condition which is true if the defined `subject` is equal to the
defined `object`. For example, the following predicate object asserts that the
subject has a value of `'bar'`:

```typescript
import {
    ConditionExpression,
    ConditionExpressionPredicate,
} from '@nova-odm/expressions';

let equalsExpressionPredicate: ConditionExpressionPredicate = {
    type: 'Equals',
    object: 'bar'
};

// you can also define an equality predicate with the `equals` helper method
import {equals} from '@nova-odm/expressions';

equalsExpressionPredicate = equals('bar');

// combine with a subject to create a valid condition expression
const equalsExpression: ConditionExpression = {
    ...equalsExpressionPredicate,
    subject: 'foo'
};
```

`object` may be an [attribute path](#attribute-paths), an [attribute
value](#attribute-values), or another type. If the lattermost type is received,
it will be serialized using the `@nova-odm/auto-marshaller` package.

#### `NotEquals` expression predicate

Creates a condition which is true if the defined `subject` is NOT equal to the
defined `object`. For example, the following predicate object asserts that the
subject does not have a value of `'bar'`:

```typescript
import {
    ConditionExpression,
    ConditionExpressionPredicate,
} from '@nova-odm/expressions';

let equalsExpressionPredicate: ConditionExpressionPredicate = {
    type: 'NotEquals',
    object: 'bar'
};

// you can also define an equality predicate with the `equals` helper method
import {notEquals} from '@nova-odm/expressions';

equalsExpressionPredicate = notEquals('bar');

// combine with a subject to create a valid condition expression
const equalsExpression: ConditionExpression = {
    ...equalsExpressionPredicate,
    subject: 'foo'
};
```

`object` may be an [attribute path](#attribute-paths), an [attribute
value](#attribute-values), or another type. If the lattermost type is received,
it will be serialized using the `@nova-odm/auto-marshaller` package.

#### `LessThan` expression predicate

Creates a condition which is true if the defined `subject` is less than the
defined `object`. For example, the following predicate object asserts that the
subject is less than 10:

```typescript
import {
    ConditionExpression,
    ConditionExpressionPredicate,
} from '@nova-odm/expressions';

let equalsExpressionPredicate: ConditionExpressionPredicate = {
    type: 'LessThan',
    object: 10
};

// you can also define an equality predicate with the `equals` helper method
import {lessThan} from '@nova-odm/expressions';

equalsExpressionPredicate = lessThan(10);

// combine with a subject to create a valid condition expression
const equalsExpression: ConditionExpression = {
    ...equalsExpressionPredicate,
    subject: 'foo'
};
```

`object` may be an [attribute path](#attribute-paths), an [attribute
value](#attribute-values), or another type. If the lattermost type is received,
it will be serialized using the `@nova-odm/auto-marshaller` package.

#### `LessThanOrEqualTo` expression predicate

Creates a condition which is true if the defined `subject` is less than or equal
to the defined `object`. For example, the following predicate object asserts
that the subject is less than or equal to 10:

```typescript
import {
    ConditionExpression,
    ConditionExpressionPredicate,
} from '@nova-odm/expressions';

let equalsExpressionPredicate: ConditionExpressionPredicate = {
    type: 'LessThanOrEqualTo',
    object: 10
};

// you can also define an equality predicate with the `equals` helper method
import {lessThanOrEqualTo} from '@nova-odm/expressions';

equalsExpressionPredicate = lessThanOrEqualTo(10);

// combine with a subject to create a valid condition expression
const equalsExpression: ConditionExpression = {
    ...equalsExpressionPredicate,
    subject: 'foo'
};
```

`object` may be an [attribute path](#attribute-paths), an [attribute
value](#attribute-values), or another type. If the lattermost type is received,
it will be serialized using the `@nova-odm/auto-marshaller` package.

#### `GreaterThan` expression predicate

Creates a condition which is true if the defined `subject` is greater than the
defined `object`. For example, the following predicate object asserts that the
subject is greater than 10:

```typescript
import {
    ConditionExpression,
    ConditionExpressionPredicate,
} from '@nova-odm/expressions';

let equalsExpressionPredicate: ConditionExpressionPredicate = {
    type: 'GreaterThan',
    object: 10
};

// you can also define an equality predicate with the `equals` helper method
import {greaterThan} from '@nova-odm/expressions';

equalsExpressionPredicate = greaterThan(10);

// combine with a subject to create a valid condition expression
const equalsExpression: ConditionExpression = {
    ...equalsExpressionPredicate,
    subject: 'foo'
};
```

`object` may be an [attribute path](#attribute-paths), an [attribute
value](#attribute-values), or another type. If the lattermost type is received,
it will be serialized using the `@nova-odm/auto-marshaller` package.

#### `GreaterThanOrEqualTo` expression predicate

Creates a condition which is true if the defined `subject` is greater than or
equal to the defined `object`. For example, the following predicate object
asserts that the subject is greater than or equal to 10:

```typescript
import {
    ConditionExpression,
    ConditionExpressionPredicate,
} from '@nova-odm/expressions';

let equalsExpressionPredicate: ConditionExpressionPredicate = {
    type: 'GreaterThanOrEqualTo',
    object: 10
};

// you can also define an equality predicate with the `equals` helper method
import {greaterThanOrEqualTo} from '@nova-odm/expressions';

equalsExpressionPredicate = greaterThanOrEqualTo(10);

// combine with a subject to create a valid condition expression
const equalsExpression: ConditionExpression = {
    ...equalsExpressionPredicate,
    subject: 'foo'
};
```

`object` may be an [attribute path](#attribute-paths), an [attribute
value](#attribute-values), or another type. If the lattermost type is received,
it will be serialized using the `@nova-odm/auto-marshaller` package.

#### `Between` expression predicate

Creates a condition which is true if the defined `subject` is between a defined
`lowerBound` and `upperBound`. For example, the following predicate object
asserts that the subject is greater than or equal to 10 and less than or equal
to 99:

```typescript
import {
    ConditionExpression,
    ConditionExpressionPredicate,
} from '@nova-odm/expressions';

let equalsExpressionPredicate: ConditionExpressionPredicate = {
    type: 'Between',
    lowerBound: 10,
    upperBound: 99
};

// you can also define an equality predicate with the `equals` helper method
import {between} from '@nova-odm/expressions';

equalsExpressionPredicate = between(10, 99);

// combine with a subject to create a valid condition expression
const equalsExpression: ConditionExpression = {
    ...equalsExpressionPredicate,
    subject: 'foo'
};
```

`lowerBound` and `upperBound` may both be an [attribute path](#attribute-paths),
an [attribute value](#attribute-values), or another type. If the lattermost type
is received, it will be serialized using the `@nova-odm/auto-marshaller`
package.

#### `Membership` expression predicate

Creates a condition which is true if the defined `subject` is equal to a member
of a list of defined values. For example, the following predicate object asserts
that the subject is one of `'fizz'`, `'buzz'`, or `'fizzbuzz'`:

```typescript
import {
    ConditionExpression,
    ConditionExpressionPredicate,
} from '@nova-odm/expressions';

let equalsExpressionPredicate: ConditionExpressionPredicate = {
    type: 'Membership',
    values: ['fizz', 'buzz', 'fizzbuzz']
};

// you can also define an equality predicate with the `equals` helper method
import {inList} from '@nova-odm/expressions';

equalsExpressionPredicate = inList('fizz', 'buzz', 'fizzbuzz');

// combine with a subject to create a valid condition expression
const equalsExpression: ConditionExpression = {
    ...equalsExpressionPredicate,
    subject: 'foo'
};
```

Each value in the `values` array may be an [attribute path](#attribute-paths),
an [attribute value](#attribute-values), or another type. If the lattermost type
is received, it will be serialized using the `@nova-odm/auto-marshaller`
package.

### Serializing condition expressions

To serialize a condition expression, pass a `ConditionExpression` object and an
instance of `ExpressionAttributes`.

## Expression attributes

Amazon DynamoDB expressions are serialized as strings with semantically
important control characters and reserved words. The `ExpressionAttributes`
object will escape both attribute names and attribute values for safe use in
any expression. When a full DynamoDB request input is ready to be sent, you can
retrieve a the `ExpressionAttributeNames` and `ExpressionAttributeValues` shapes
to send alongside the input:

```typescript
import {
    AttributePath,
    AttributeValue,
    ExpressionAttributes,
} from '@nova-odm/expressions';
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';

const attributes = new ExpressionAttributes();

// you can add a string attribute name
const escapedFoo = attributes.addName('foo');
// or a complex path
const escapedPath = attributes.addName('bar.baz[3].snap.crackle.pop');
// or an already parsed attribute path
attributes.addName(new AttributePath('path.to.nested.field'));

// raw JavaScript values added will be converted to AttributeValue objects
const escapedRaw = attributes.addValue(42);
// already marshalled values must be wrapped in an AttributeValue object
const escapedMarshalled = attributes.addValue(new AttributeValue({N: "42"}));

const client = new DynamoDBClient({ region: 'eu-west-1' });
client.query({
    TableName: 'my_table',
    KeyConditionExpression: `${escapedFoo} = ${escapedRaw} AND ${escapedPath} = ${escapedMarshalled}`,
    ExpressionAttributeNames: attributes.names,
    ExpressionAttributeValues: attributes.values,
})
```

## Function expressions

Function expressions represent named functions that DynamoDB will execute on
your behalf. The first parameter passed to the `FunctionExpression` represents
the function name and must be a string; all subsequent parameters represent
arguments to pass to the function. These parameters may be instances of
`AttributePath` (to have the function evaluate part of the DynamoDB document to
which the function applies), `AttributeValue` (for already-marshalled
AttributeValue objects), or arbitrary JavaScript values (these will be converted
by the `@nova-odm/marshaller` package's `Marshaller`):

```typescript
import {
    AttributePath,
    ExpressionAttributes,
    FunctionExpression,
} from '@nova-odm/expressions';

const expr = new FunctionExpression(
    'list_append', 
    new AttributePath('path.to.list'),
    'foo'
);
const attributes = new ExpressionAttributes();
// serializes as 'list_append(#attr0.#attr1.#attr2, :val3)'
const serialized = expr.serialize(attributes);
console.log(attributes.names); // {'#attr0': 'path', '#attr1': 'to', '#attr2': 'list'}
console.log(attributes.values); // {':val3': {S: 'foo'}}
```

## Mathematical expressions

Mathematical expressions are used in the `SET` clause of update expressions to
add or subtract numbers from attribute properties containing number values:

```typescript
import {MathematicalExpression} from '@nova-odm/expressions';

const expr = new MathematicalExpression('version', '+', 1);
```

## Projection Expressions

Projection expressions tell DynamoDB which attributes to include in fetched
records returned by `GetItem`, `Query`, or `Scan` operations. This library uses
`ProjectionExpression` as a type alias for an array of strings and
`AttributePath` objects.

## Update Expressions

Update expressions allow the partial, in place update of a record in DynamoDB.
The expression may have up to four clauses, one containing directives to set
values in the record, one containing directives to remove attributes from the
record, one containing directives to add values to a set, and the last
containing directives to delete values from a set.

```typescript
import {
    AttributePath,
    FunctionExpression,
    UpdateExpression,
} from '@nova-odm/expressions';

const expr = new UpdateExpression();

// set a value by providing its key and the desired value
expr.set('foo', 'bar');
// you may also set properties in nested maps and lists
expr.set(
    'path.to.my.desired[2].property',
    new FunctionExpression(
        'list_append',
        new AttributePath('path.to.my.desired[2].property'),
        'baz'   
    )
);

// remove a value by providing its key or path
expr.remove('fizz.buzz.pop[0]');

// add a value to a set
expr.add('string_set', 'foo');

// delete a value from the same set
expr.delete('string_set', 'bar');
```