npm package: @growsari/dbal
Description: Database access layer
Latest version: 2.3.0

# Database Access Layer

Database Access Layer (DBAL) is a wrapper for DynamoDB Client. It provides the basic create, read, update and delete (CRUD) functionality and lets you group data into an entity.

## Installation and Basic Usage

Install the Database Access Layer

```
npm i @growsari/dbal
```

Require or import the package as `Entity`

```javascript
const Entity = require('@growsari/dbal')
```

Before we could use it, the project must have the following environment variables

| ENV | Description | Default Value | Other Value |
|---|---|---|---|
| IS_OFFLINE | Flag for local development | false | true | 
| DB_ADAPTER |  Database to be used | dynamodb | ~ |
| DYNAMODB_ENDPOINT | DynamoDB endpoint  | http://localhost:8000 | ~ |
| TABLE_NAME | DynamoDB table name | ~ | ~ |

Create a new class and extend `Entity`

```javascript
'use strict'

const Entity = require('@growsari/dbal')

class User extends Entity {
  constructor() {
    super()
    this.attributes = {
      email: [
        'identifier'
      ],
      group_id: [
        'foreign'
      ]
    }
    this.sort = [
      {
        index: 'SortByCreatedAtIndex',
        field: 'created_at'
      },
      {
        index: 'SortByNameIndex',
        field: 'name'
      }
    ]
  }
}

module.exports = User

```

Require `User` entity and create item:

```javascript
await User.create({
  name: 'John Doe',
  email: 'john.doe@example.com',
  group_id: 'BDS45HX47I9CT'
})

// {
//   "id": "659312a0-1b5c-11ea-a271-e732f3f9733b",
//   "created_at": "2019-12-10T14:50:24.330Z",
//   "updated_at": "2019-12-10T14:50:24.330Z",
//   "name": "John Doe",
//   "email": "john.doe@example.com",
//   "group_id": "BDS45HX47I9CT"
// }
```

Read item by:

```javascript
await User.getById('659312a0-1b5c-11ea-a271-e732f3f9733b')
```

Update item by:

```javascript
await User.updateById('659312a0-1b5c-11ea-a271-e732f3f9733b', {
  name: 'John Doe II'
})
```
Delete item by:

```javascript
await User.deleteById('659312a0-1b5c-11ea-a271-e732f3f9733b')
```

Update item by id based on condition options.

```javascript
await User.updateByIdWithConditionOrFail('659312a0-1b5c-11ea-a271-e732f3f9733b', {name: 'John'}, ['AND', {name: {'!=': 'John Doe II'}}])
```
## Entity Class Properties

These are the properties for configuring an entity

### attributes

Lets you configure a special field in an item which implies uniqueness, foreign key and so on

| Value | Description |
|---|---|
| identifier | Creates a field with unique value within the entity |
| foreign | Creates a foreign key (id in another microservice) |
| namespace:field | Creates an identifier within a namespace (determined by field) |

Sample

```javascript
this.attributes = {
  [attribute_name] : [
    'identifier',
    'foreign',
    'namespace:field'
  ]
}
```

### sort

List of DynamoDB global secondary indexes (GSI) for data retrieval sorting

## Entity Class Functions

These are the available functions for managing the data within an entity

```javascript
async create(data)
async updateById(id, data)
async deleteById(id)
async bulkCreate(collection)
async bulkUpdate(collection)
async bulkDelete(collection)
async getById(id)
async getByIdOrFail(id)
async getByIdentifier(identifier, value)
async getByIdentifierWithinNamespace(namespace, identifier, value)
async getByIds(ids)
async getAll(first, after, sortBy, filterConditions)
async getAllByForeignKey(foreignKey, foreignValue, first, after, sortBy, filterConditions)
async search(query, rawQuery, fields, perPage, page, filters, sort)
async reindex()
async syncWithSearchEngine()
```

##  Sort By

A string which is composed of `field` name and `order` joined by `|`.

```javascript
const sortBy = 'name|asc'
await Product.getAll(first, after, sortBy, filterConditions)
```

##  Filter Conditions

> NOTE: To effeciently use the filter conditions in terms of DynamoDB RCU and WCU, I recommend using it with getAll() API. If you can't avoid using it with getAllByForeignKey() API please use filter with the defined sort fields.

These are the recognized compound expressions:

### `AND` expression

Asserts that all of the subexpressions' conditions are satisfied.

```javascript
const filterConditions = [
  'AND',
  { name: 'Coke' },
  { is_available: true },
  // more subexpressions
]
await Product.getAll(first, after, sortBy, filterConditions)
```

The above condition is equivalent to the following boolean expression:

```javascript
name === 'COKE' && is_available === true && ...
```


### `OR` expression

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

```javascript
const filterConditions = [
  'OR',
  { name: 'Coke' },
  { brand: 'Coke' },
  // more subexpressions
]
await Product.getAll(first, after, sortBy, filterConditions)
```

The above condition is equivalent to the following boolean expression:

```javascript
name === 'COKE' || brand === 'Coke' || ...
```

### `NOT` expression

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

```javascript
const filterConditions = [
  'AND',
  { name: 'Coke' },
  ['NOT', { is_available: true }]
]
await Product.getAll(first, after, sortBy, filterConditions)
```

The above condition is equivalent to the following boolean expression:

```javascript
name === 'COKE' && is_available !== true
```

### `Equal` expression

An object with one pair of key and value. The object's key represents the `field` name. The object's value is also an object and it represents the condition where the condition's key is the `operation`.

`==` operation creates a condtion which is true if the `field` is equal to the defined `value`.

```javascript
const filterConditions = { name: { '==': 'Coke' } }
// can be simplified to:
// const filterConditions = { name: 'Coke' }
await Product.getAll(first, after, sortBy, filterConditions)
```

### `NotEqual` expression

`!=` operation creates a condtion which is true if the `field` is not equal to the defined `value`.

```javascript
const filterConditions = { is_available: { '!=': false } }
await Product.getAll(first, after, sortBy, filterConditions)
```

### `LessThan` expression

`<` operation creates a condtion which is true if the `field` is less than to the defined `value`.

```javascript
const filterConditions = { available: { '<': 10 } }
await Product.getAll(first, after, sortBy, filterConditions)
```

### `LessThanOrEqualTo` expression

`<=` operation creates a condtion which is true if the `field` is less than or equal to the defined `value`.

```javascript
const filterConditions = { available: { '<=': 10 } }
await Product.getAll(first, after, sortBy, filterConditions)
```

### `GreaterThan` expression

`>` operation creates a condtion which is true if the `field` is greater than to the defined `value`.

```javascript
const filterConditions = { available: { '>': 10 } }
await Product.getAll(first, after, sortBy, filterConditions)
```

### `GreaterThanOrEqualTo` expression

`>=` operation creates a condtion which is true if the `field` is greater than or equal to the defined `value`.

```javascript
const filterConditions = { available: { '>=': 10 } }
await Product.getAll(first, after, sortBy, filterConditions)
```

### `Between` expression

`between` operation creates a condtion which is true if the `field` is between `value[0]` and `value[1]`.

```javascript
const filterConditions = {
  created_at: {
    'between': ['2020-06-01T00:00:00.000Z', '2020-06-15T00:00:00.000Z']
    }
  }
await Product.getAll(first, after, sortBy, filterConditions)
```

### `In` expression

`in` operation creates a condtion which is true if the `field` is equal to an element the defined set of `value`s.

```javascript
const filterConditions = {
  tags: {
    'in': ['bottle', 'soda', 'promo', 'top_10', 'drink']
    }
  }
await Product.getAll(first, after, sortBy, filterConditions)
```

### `BeginsWith` expression

> NOTE! This operation will only work with sort fields

`beginsWith` operation creates a condtion which is true if the `field` is a string and begins with or has a prefix of `value`.

```javascript
const filterConditions = { status: { 'beginsWith': 'active_' } }
await Product.getAll(first, after, sortBy, filterConditions)
```

### `Contains` expression

> NOTE! This operation will only work with sort fields

`contains` operation creates a condtion which is true if the `field` contains the defined `value`.

```javascript
const filterConditions = { status: { 'contains': 'active' } }
await Product.getAll(first, after, sortBy, filterConditions)
```

## Errors

| Code | Message |
| --- | --- |
| DBAL-001 | Database not supported |
| DBAL-002 | Item not found for 'entity' entity |
| DBAL-003 | Invalid pagination cursor |
| DBAL-004 | Identifier 'field' must be unique |
| DBAL-005 | Identifier 'field' within the namespace 'field' must be unique |
| DBAL-006 | Sort key 'field' is not provided |
| DBAL-007 | Invalid value for foreign key 'field' |
| DBAL-008 | Duplicate entries in array as input is not allowed |
| DBAL-009 | Invalid search engine setup |
| DBAL-010 | Missing 'REGION' for OpenSearch Serverless |