UNPKG

mage-validator

Version:

Validation utility for MAGE user commands & topics (TypeScript)

283 lines (209 loc) 6.97 kB
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.