Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/mage/mage-validator

Validation system for MAGE topics and user command input types (TypeScript).
https://github.com/mage/mage-validator

mage nodejs typescript

Last synced: about 22 hours ago
JSON representation

Validation system for MAGE topics and user command input types (TypeScript).

Awesome Lists containing this project

README

        

mage-validator
==============

Validation system for MAGE topics and user command input types (TypeScript).

Installation
-------------

```shell
npm install --save mage-validator class-validator class-transformer reflect-metadata
```

You need to install two peer dependencies alongside `mage-validator`:

1. `class-validator` so that you may add validation decorators to your data class
2. `class-transformer` to control how the received data will be deserialised
3. `relfect-metadata` will be needed by the two modules above to extract type information

You will also need to make sure that the following configuration is set in your
`tsconfig.json`:

```json
{
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
```

Usage
-----

mage-validator exports all functions exposed by class-validator and
class-transformer for convenience. A few changes and additions have
however been made for convenience.

### @Type decorator

```typescript
// Both are equivalent
@Type(Hello)
@Type(() => Hello)
```

The `@Type` decorator has been customized to accept either a type
or a function (instead of only a function).

### @MapOf decorator

```typescript
function validateFunc(key: string, value: Child) {
throw new Error('never valid')
}

class Child { @IsPositive() public id: number }

@MapOf(Child)
class DynamicMap { [key: string]: Child }

class TestTopic extends ValidatedTopic {
// [...]

// Use a Map class
@Type(DynamicMap) public map: DynamicMap

// Use an anonymous object as a map
@MapOf(Child) public anonymousMap: { [key: string]: Child }

@MapOf(Child, validateFunc) public anotherMap: { [key: string]: Child }
}
```

`mage-validator` also provides an additional `@MapOf` decorator for dealing with
key-value map objects; using this type will both ensure nested maps entries
will be typed and that each entries will be validated (so `@ValidateNested`
is not required).

### (Optional) Project structure

In normal MAGE projects, you need to put all your topics configuration under
`./lib/archivist/index.ts`. However, this quickly becomes hard to manage as
the number of topics grows.

Instead, we recommend using the following file structure:

```plaintext
lib/
archivist/
index.ts
modules/
modulename/
topics/
Players.ts
types/
PlayerConfig.ts
SomethingElse.ts
usercommands/
addGems.ts
commandTwo.ts
index.ts
index.ts
```

In other words, we recommend to separate MAGE topics and attach them to their
related modules under the `topics` folder; we also recommend to put other
types (example: user command custom parameter type) under a `types` folder.

To make it easier for you to set this up, `mage-validator` comes with a method you
can simply drop into `./lib/archivist/index.ts`; it will scan each one of your
modules and attempt to load all topics defined in them.

> `./lib/archivist/index.ts`

```typescript
import { loadTopicsFromModules } from 'mage-validator'

loadTopicsFromModules(exports)
```

### User command parameters and return data validation

Validation works by encapsulating messages into types:

> `./lib/modules/modulename/types/PlayerData.ts`

```typescript
import { IsInt, Max } from 'mage-validator';

export default class {
@IsInt()
@Max(5)
public count: gems = 1 // Default value
}
```

Which then can be used as user commands types:

> `./lib/modules/modulename/usercommands/addGems.ts`

```typescript
import * as mage from 'mage'
import { Type, IsInt, Max, ValidateNested, Acl } from 'mage-validator'
import PlayerData from '../types/PlayerData'

export default class {
@IsInt()
@Min(1)
public gemRegisterBonus: number

@ValidateNested()
@Type(() => PlayerData)
public data: PlayerData

@Acl('*')
public static async execute(state: mage.core.IState, data: PlayerData, gemRegisterBonus: number) {
data.gems += gemRegisterBonus
return data
}
}
```

In this case, both `data` and `gemRegisterBonus` will be validated prior to execution, and
`data` will be validated once again once it is returned.

### Topics

#### Topic definition

Topics work the same way as types, except that they contain the topic
configuration as static parameters:

> `./lib/modules/modulename/topics/Player.ts`

```typescript
import { ValidatedTopic, ValidateNested, IsUUID, IsAlpha } from 'mage-validator';
import { Type } from 'class-transform';
import PlayerData from '../topics/PlayerData'

class Index {
@isUUID(5)
playerId: string
}

export default class {
// Index configuration
public static readonly index = ['playerId']
public static readonly indexType = Index

// Vaults configuration (optional)
public static readonly vaults = {}

// Attribute instances
@IsAlpha()
public name: string

@ValidateNested()
@Type(() => PlayerData)
public data: PlayerData
}
```

If you wish to use [tome](https://github.com/Wizcorp/node-tomes) instead, you can
you can extend the `ValidatedTomeTopic` class instead. Unlike normal `tomes`, you will
simply access and set object values directly (instead of using `assign`, `set`, `valueOf`
and so on).

#### Loading topics

> `./lib/modules/modulename/usercommands/getPlayer.ts`

```typescript
import * as mage from 'mage'
import { Acl } from 'mage-validator'
import Player from '../topics/Player'

export default class {
@IsUUID(5)
public id: string

@Acl('*')
public static async execute(state: mage.core.IState, id: string) {
return await Player.get(state, { playerId: id })
}
}
```

#### Storing topics

Topic instances also work as state wrappers:

> `./lib/modules/modulename/usercommands/createPlayer.ts`

```typescript
import * as mage from 'mage'
import { Acl } from 'mage-validator'
import PlayerData from '../types/PlayerData'
import Player from '../topics/Player'

export default class {
@Acl('*')
public static async execute(state: mage.core.IState, name: string) {
const player: Player = Player.create(state, { playerId: '123' })
player.name = name
player.data = new PlayerData()
player.data.gems = 5

return player.add() // Same a state.archivist.add()
}
}
```

In this particular case, you could even simply put the player
topic directly as you user command parameter; all you will
then need to do is to set the index at some point before you attempt
to record any operations:

#### Topics as user command parameters

> `./lib/modules/modulename/usercommands/createPlayer.ts`

```typescript
import * as mage from 'mage'
import { ValidateNested, Type, Acl } from 'mage-validator'
import Player from '../topics/Player'

export default class {
@ValidateNested()
@Type(() => Player)
public player: Player

@Acl('*')
public static async execute(state: mage.core.IState, player: Player) {
player.add()
return player
}
}
```

License
-------

MIT.