npm package: @swarm-host/lair
Description: JS/TS database
Author: onechiporenko
Latest version: 5.5.0

# Lair-db

[![Mutation testing badge](https://img.shields.io/endpoint?style=flat&url=https%3A%2F%2Fbadge-api.stryker-mutator.io%2Fgithub.com%2Fonechiporenko%2Fswarm-host-platform%2Fmaster%3Fmodule%3Dlair)](https://dashboard.stryker-mutator.io/reports/github.com/onechiporenko/swarm-host-platform/master?module=lair)
[![npm version](https://badge.fury.io/js/@swarm-host%2Flair.svg)](https://badge.fury.io/js/@swarm-host%2Flair)
[![CI](https://github.com/onechiporenko/swarm-host-platform/actions/workflows/nodejs.yml/badge.svg)](https://github.com/onechiporenko/swarm-host-platform/actions/workflows/nodejs.yml)

## Install

```bash
npm i @swarm-host/lair --save-dev
```

## About Lair-db

Lair-db is a database written on TypeScript. Its main use-case is a test-mode for SPA-applications. Lair-db should be used with fake-server (like Pretender) and data-generator (like Faker.js).

Lair-db consists of two parts - Lair and Factories. Lair is a place where all data is stored. It has several methods which implement basic CRUD operations. Lair is a Singleton. Factories generate Records that are pushed to the Lair and create its initial state. Each Record has `id`-field which value is auto incremented in the scope of factory. Every `id` is a "stringified" number.

To get Lair instance you should use static method `getLair`:

```typescript
import { Lair } from '@swarm-host/lair';

const lair = Lair.getLair();
```

## Lair initial state

Factories are sub-classes of class `Factory`:

```typescript
import { Lair, Factory } from '@swarm-host/lair';

class UnitFactory extends Factory {
  static factoryName = 'unit';
}

const factoryInstance = new UnitFactory();
```

Now we have a factory. Currently, it can't do any useful things. Every generated Record for this Factory will have only single property (`id`). All properties for Records are factory's attributes with `field`-decorator:

```typescript
import { Lair, Factory, field } from '@swarm-host/lair';
import * as faker from 'faker';

class UnitFactory extends Factory {
  static factoryName = 'unit';
  
  @field()
  get firstName() {
    return faker.name.firstName();
  }
  
  @field()
  get lastName() {
    return faker.name.lastName();
  }
}

const factoryInstance = new UnitFactory();
```

Our factory will create Records with fields `id`, `firstName` and `lastName`. Since `faker` returns truly random values we'll get really different Records. We declared `firstName` and `lastName` as `getter`s that return some random values.

```typescript
import { Lair, Factory, field } from '@swarm-host/lair';
import * as faker from 'faker';

class UnitFactory extends Factory {
  static factoryName = 'unit';
    
  @field()
  get firstName() {
    return faker.name.firstName();
  }
    
  @field()
  get lastName() {
    return faker.name.lastName();
  }
    
  @field()
  get fullName() {
    return `${this.firstName} ${this.lastName}`;
  }
}

const factoryInstance = new UnitFactory();
```

Records created with such factory will look like:

```json
{
  "id": "1",
  "firstName": "Jim",
  "lastName": "Raynor",
  "fullName": "Jim Raynor"
}
```

Here `fullName` is a result of concatenation fields `firstName` and `lastName`. This means that `fullName` initially has such value. Iff you update Record's `firstName` or `lastName`, `fullName` **WON'T** be updated automatically.

To create some Records you firstly need to register factory in the Lair:

```typescript
lair.registerFactory(factoryInstance);
```

Here `factoryInstance` is a factory described before. Static field `factoryName` is a Record's type generated by this factory. There is a method called `createRecords` to create some Records and put them in the Lair-db:

```typescript
lair.createRecords('unit', 10);
```

Here we've created 10 Records of type `unit`. 

*You can't create Records of unregistered types.*

Method `createRecords` **MUST** be used only for initial filling of Lair-db.

Records of different types may be linked one to another. There is a special way to describe such links. It's called 'relationships'. Let's say we have two factories for units and squads. One unit may be in the one squad and any squad may contain many units (typical one-to-many or many-to-one relationships):

```typescript
import { Factory, field, hasOne, hasMany } from '@swarm-host/lair';
import * as faker from 'faker';

class UnitFactory extends Factory {
  static factoryName = 'unit';
  
  @field()
  get name() {
    return faker.name.findName();
  }
  
  @field()
  squadName = '';
    
  @hasOne('squad', 'units')
  squad;
}

class SquadFactory extends Factory {
  static factoryName = 'squad';
  
  @field()
  get name() {
    return faker.hacker.abbreviation(); // just some random
  }
  
  @hasMany('unit', 'squad', {
    createRelated: 4
  })
  units;
}

lair.registerFactory(new UnitFactory());
lair.registerFactory(new SquadFactory());
```

Fields `unit.squad` and `squad.units` are described as relationship-fields. Methods `hasOne` and `hasMany` take two arguments. First one is a related Records type and second one is a inverted property name. For `squad`-factory we added new attribute called `createRelated`. Lair uses it to know how many related records should be created "silently". In the example above we set that each `squad` should have 4 `units`. Let's try to create some squads:

```typescript
lair.createRecords('squad', 4);
```

Lair will create 4 `squads` and 16 `units` (4 for each `squad`). We still can create `unit` records with `lair.createRecords('unit', 2)`, however they won't be linked with any `squad` by default.

Sometimes it is useful to update record after it's created. Factory has method `afterCreate` for this case. It receives create Record as argument and must return it. Let's add this method to the `unit` factory:

```typescript
class UnitFactory extends Factory {
  static factoryName = 'unit';
  
  @field()
  get name() {
    return faker.name.findName();
  }
  @field()
  squadName = '';

  @hasOne('squad', 'units')
  squad;

  afterCreate(record) {
    record.squadName = record.squad.name;
    return record;
  }
}
```

Here we update unit's property `squadName` with real `squad.name`. Method `afterCreate` takes record with all related records created before it. You may update any own `unit` property in the `afterCreate`, but you can't update related records and `unit` relationships. This means, that you can't do `delete record.squad` or `record.squad.name = '1234'`. 

### Using fixtures

Lair allows to load predefined data. Method `loadRecords` can be used for this. It takes two arguments - factory name and data-array itself:

```typescript
lair.loadRecords('unit', [/* data */]);
```

This method has several requirements:

* Factory `unit` must be registered
* Factory `unit` must have declared attributes with `@field / @hasOne / @hasMany`, otherwise new records will be almost empty
* Factory `unit` must have attribute `allowCustomIds` to be `true`
* Loaded records must have unique identifiers (`id`-field)
* Related factories must be registered too
* Related records must be already loaded

Once all Factories are created (and registered) and Lair is filled with records you are ready to mock your back-end.

## CRUD operations

Every request to your backend represents one of the four operations with records - Create, Read, Updated or Delete. So, Lair has methods for each request-type.

### `createOne`

Method `createOne` is used to create new record in the Lair. It should not be used for Lair initialize and should be only for PUT/POST request handlers (depends on which request-type is used in your application).

`createOne` takes two arguments. First one is a record type and second one is a data for new record:

```typescript
lair.createOne('unit', {
  name: 'Sarah Kerrigan',
  squad: '1'
});
```

There are few important moments here. Firstly, we don't include `id`. Lair will generate it. Secondly, value for `squad`-field is an identifier for `squad`-record. Only identifiers may be used as values for relationship-fields on create or update records. Thirdly, all related records must be already in the Lair. So, in this example record `squad` with id `1` is already created.

Newly created `unit` will be automatically added to the `squad` with id `1`.

#### Default values for attributes

`Lair` uses attribute `value` as a default value for `createOne` if it's not provided. Method `Factory.field` allows overriding `defaultValue`. It takes hash with two properties `value` and `defaultValue`. First one is same as usual "old" field-declaration. Second one is a value (**not** Function) that will be used in the `createOne` if nothing will be provided for field.

```typescript
class LogFactory extends Factory {
  static factoryName = 'log';
  
  @field({
    defaultValue: 'info'
  })
  get type() {
    return faker.random.arrayElement(['warn', 'info', 'error']);
  }
  
  @field()
  message = '';
}
lair.registerFactory(LogFactory);
const newLog = lair.createOne('log', {message: 'msg'}); // no `type` provided
console.log(newLog); // {message: 'msg', type: 'info'} 'info' - default value for `type` was used
```

### `updateOne`

Method `updateOne` is used to update some record in the Lair. It takes three arguments - record type, record id and new data:

```typescript
lair.updateOne('unit', '1', {
  name: 'Rory Swann',
  squad: '2'
});
```

Here we update some fields for `unit` with id `1` and change its squad to `2` (it must be in the Lair).

### `deleteOne`

Method `deleteOne` is used to delete some record from the Lair. It takes two arguments - record type and record id:

```typescript
lair.deleteOne('unit', '1');
```

Record `unit` with id `1` will be deleted and `squad` where this unit was will be updated.

### `getOne`

There are four methods to get record(s) from the Lair. `getOne` is a first of them. It takes two arguments - record type and record id:

```typescript
lair.getOne('unit', '1');
```

It returns record with all related data:

```json
{
  "id": "1",
  "name": "Jim Raynor",
  "squad": {
    "id": "1",
    "names": "Ravens",
    "units": ["1", "2", "3", "4"]
  }
}
```

### `queryOne`

This method is also used to get a single record. The main difference between methods is that `getOne` uses `id` to get record and `queryOne` uses a callback:

```typescript
lair.queryOne('unit', record => record.id === '1');
```

Method `queryOne` will return first record for which the callback returns true.

### `getAll`

Method `getAll` returns all records of given type:

```typescript
lair.getAll('unit');
```

### `queryMany`

Method `queryMany` returns record of given type that the passed function returns true for:

```typescript
lair.queryMany('unit', record => record.squad === '1' || record.squad === '2');
```

## Go Pro

### Drop relationships

You should set value to `null`(for `hasOne`) or `[]` (for `hasMany`) to drop some relationship for record:

```typescript
lair.updateOne('unit', '1', {
  squad: null
});

lair.updateOne('squad', '1', {
  units: []
});
```

Related records will be updated automatically.

### Random number of related records

You may set `createRelated` as a function:

```typescript
class SquadFactory extends Factory {
  static factoryName = 'squad';
  
  @hasMany('unit', 'squad', {
    createRelated() {
      return faker.random.number({min: 1, max: 10});
    }
  })
  units;
}
```

### Use existing records as related

It's possible to use already existing records as related:

```typescript
class SquadFactory extends Factory {
  static factoryName = 'squad';
  
  @hasMany('unit', 'squad', {
    useExistingAsRelated() { // can be a number or callback with number returned
      return faker.random.number({min: 1, max: 10});
    }
  })
  units;
}
```

Restrictions:

* Extra records `unit` won't be created if there are less existing records than needed.

### Check if Factory creates records in the scope of "createRelated" for another Factory

```typescript
class ParentFactory extends Factory {
  static factoryName = 'parent';
  
  @hasMany('child', 'parent', {
    createRelated: 2
  })
  children;
}
class ChileFactory extends Factory {
  static factoryName = 'child';
  
  @hasOne('parent', 'children')
  parent;
  
  afterCreate(record, extraData) {
    console.warn(extraData); // <--- check this out
    return record;
  }
}

lair.registerFactory(ParentFactory);
lair.registerFactory(ChileFactory);

lair.createRecords('parent', 1);
lair.createRecords('child', 2);
```

Field `extraData` is available in the dynamic fields and contains information about a parent factory that forces Lair to create some records of the child factory.

In the example above `console.log` will be called 4 times. First two times it will output:

```json
{
  "relatedTo": {
    "factoryName": "parent",
    "recordsCount": 2,
    "currentRecordNumber": 1
  }
}
```

```json
{
  "relatedTo": {
    "factoryName": "parent",
    "recordsCount": 2,
    "currentRecordNumber": 2
  }
}
```

Here `relatedTo` contains name of the parent-factory, records count of child factory that will be created and number of creating child-record. `currentRecordNumber` isn't new record identifier, and it's just a sequence number. It will be dropped to `1` for each parent-record.

Last two times `console.log` from the `field`-attribute will out:

```json
{
  "relatedTo": {}
}
```

Field `relatedTo` is empty because child-records are created standalone and not in the scope of the parent factory. 

### One way relationships

Decorators `hasOne` and `hasMany` take two arguments. However, you may set `null` as second parameter. In this case records will be related in one way:
 
```typescript
class SquadFactory extends Factory {
  static factoryName = 'squad';
  
  @hasMany('unit', null) units;
}

class UnitFactory extends Factory {
  static factoryName = 'unit';
  
  @hasOne('squad', 'units') squad;
}
```

Here `squad` records have some related units. When some unit will be added to the squad its `squad`-field won't be updated.

### Reflexive relationships

Good example of reflexive relations is a directories structure. Each directory may have many child-directories and one parent-directory. Lair-db allows you to declare such relationships:

```typescript
class DirFactory extends Factory {
  static factoryName = 'dir';

  @field()
  get name() {
    return faker.internet.domainWord(); // any random name
  }

  @hasMany('dir', 'parent', {
    reflexive: true,
    depth: 3,
    createRelated() {
      return faker.random.number({min: 1, max: 3});
    }
  })
  dirs;

  @hasOne('dir', 'dirs')
  parent;
}

lair.registerFactory(DirFactory);
lair.createRecords('dir', 1);
lair.getOne('dir', '1');
```

Factory `Dir` will create records with a lot of related records. Each `dir` will have 1 - 3 child-directories and 3 levels depth:

```json
{
  "id": "1",
  "name": "wendy",
  "dirs": [
    {
      "id": "2",
      "name": "alysa",
      "dirs": [
        {
          "id": "3",
          "name": "tracy",
          "dirs": [],
          "parent": "2"
        },
        {
          "id": "4",
          "name": "mabelle",
          "dirs": [],
          "parent": "2"
        }
      ],
      "parent": "1"
    }
  ],
  "parent": null
}
```

### Sequences

Lair-db allows creating sequences of values. This means that you can create a time line like:

```typescript
class TimelineFactory extends Factory {
  static factoryName = 'timeline';
  
  @sequenceItem(
    new Date().getTime() - 24 * 3600 * 1000,
    prevValues => prevValues.pop() + 5000
  )
  timestamp;
  
  @field()
  get val() {
    return faker.random.number({min: 1, max: 100});
  };
}
```

Every created record for this factory will have `timestamp`-property greater to 5 seconds than previous. It's useful for graphs and metrics.

`Factory.sequenceItem` takes two mandatory arguments - initial value (it will be set to record with id `1`) and function that calculates value for next record. This callback will get list with all previously generated values for this field. 

Third argument is a POJO with options for sequence. Currently only one option is available. It's called `lastValuesCount`. Its value determines how many items will be passed to the callback:

```typescript
class TimelineFactory extends Factory {
  static factoryName = 'timeline';
  
  @sequenceItem(
    new Date().getTime() - 24 * 3600 * 1000,
    prevValues => prevValues.pop() + 5000, // prevValues will have two values (for id '2' it will have one value)
    {lastValuesCount: 2} // <----
  )
  timestamp;
  
  @field()
  get value() {
    return faker.random.number({min: 1, max: 100});
  }
}
```

Use option `lastValuesCount` if your sequence items depends on limited number of previous values.

### Extending Factories

New Factory may be created based on another Factory:

```typescript
class Parent1Factory extends Factory {
  static factoryName = 'parent1';
  
  @hasMany('child', 'parent', {
    createRelated: 5
  })
  children;
  
  afterCreate(record) {
    console.log('parent 1');
    return record;
  }
}

class Parent2Factory extends Parent1Factory {
  static factoryName = 'parent2';
  
  @hasOne('child', 'parent', {
    createRelated: 1
  }) children;
  
  afterCreate(record) {
    console.log('parent 2');
    return record;
  }
}

class ChildFactory extends Parent2Factory {
  static factoryName = 'child';
}
```

**IMPORTANT** All parent-factories must be registered in the Lair BEFORE any record of child-factory is created.  

### Ignore related factories

There are some cases when you don't need to get all related to the record data. Let's go back to the example with `squad` and `units` (one `squad` has many `units` and one `unit` belongs to only one `squad`). We consider the cases:

Squad info is needed with units ids and not whole units data:

```typescript
lair.getOne('squad', '1', {depth: 1});
// {id: '1', name: 'Ravens', units: ['1', '2', '3', '4']}
```

Squad info is needed without units at all:

```typescript
lair.getOne('squad', '1', {ignoreRelated: ['unit']}); 
// {id: '1', name: 'Ravens'}
```

Here we have two options called `depth` and `ignoreRelared`. First one determines how deeply Lair should go to get data for needed record. Second one determines what factories should be ignored while Lair combines data for needed record. **Important** `ignoreRelated` contains a list of factory names and not attribute names! Both `depth` and `ignoreRelated` may be used together.

Key `ignoreRelated` also can be a boolean. `true` is for case when **all** related factories should be ignored. `false` is for case when **no one** related factory should be ignored (default behavior).

These options are very useful for cases with a lot of related records that may cause performance issues when Lair will collect them from internal store.