Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,63 @@ 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:

#### Migrating topics

> `./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

public age: number

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

@Migrate(1)
public setDefaultName() {
if (!this.name) {
this.name = 'DefaultName' + Math.floor(Math.random() * 1000)
}
}

@Migrate(2)
public setDefaultAge() {
if (!this.age) {
this.age = 0
}
}
}
```

All `ValdiatedTopic` have a `_version`, attribute used for data versioning. The default value is 0.

You can define methods to be used as migration steps. Use the `Migration`
decorator to define the version this step will upgrade the data to.
Only migration steps with a higher step value than the current data will
be executed.

Migrations will **not** be executed on newly created topic instances.

#### Topics as user command parameters

> `./lib/modules/modulename/usercommands/createPlayer.ts`
Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "mage-validator",
"version": "0.9.1",
"version": "0.9.2",
"description": "Validation utility for MAGE user commands & topics (TypeScript)",
"main": "./lib/index.js",
"types": "./lib/index.d.ts",
Expand Down
15 changes: 5 additions & 10 deletions src/classes/ValidatedTomeTopic.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import ValidatedTopic, { IStaticThis, TopicData } from './ValidatedTopic'
import { defaultMetadataStorage } from 'class-transformer/storage'
import * as classTransformer from 'class-transformer'

import { archivist } from 'mage'
import * as mage from 'mage'
Expand Down Expand Up @@ -259,18 +260,13 @@ export default class ValidatedTomeTopic extends ValidatedTopic {
data?: TopicData<T>
): Promise<T> {

const tome: any = Tome.isTome(data) ? data : Tome.conjure(data || {})
const className = this.getClassName()
const instance = new this()
const typeMap: any = {}

// Create default values
if (!data) {
Object.keys(instance).forEach((key) => {
tome.set(key, (<any> instance)[key])
delete (<any> instance)[key]
})
}
const isTome = Tome.isTome(data)
const rawData: any = isTome ? Tome.unTome(data) : data
const instance = classTransformer.plainToClass<T, object>(this, rawData) || new this()
const tome: any = isTome ? data : Tome.conjure(instance)

instance.setTopic(className)
instance.setState(state)
Expand Down Expand Up @@ -316,7 +312,6 @@ export default class ValidatedTomeTopic extends ValidatedTopic {
return target[key]
}


return undefined
}

Expand Down
50 changes: 49 additions & 1 deletion src/classes/ValidatedTopic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export type PartialIndex<I> = {
* that return a properly typed output.
*/
export interface IStaticThis<I, T> {
version: number,
// Todo: any should be I!
indexType: { new(): any },

Expand Down Expand Up @@ -98,8 +99,13 @@ export default class ValidatedTopic {
public static readonly indexType: any
public static readonly vaults = {}

public static version = 0
public static migrations = new Map<number, string>()

private static _className: string

public _version: number = 0

/**
* Return the current class name
*
Expand Down Expand Up @@ -161,6 +167,11 @@ export default class ValidatedTopic {
instance.setState(state)
await instance.setIndex(index)

/* istanbul ignore next */
if (!instance._version) {
instance._version = this.version
}

return instance
}

Expand Down Expand Up @@ -228,7 +239,10 @@ export default class ValidatedTopic {
return undefined
}

return this.create(state, index, data)
const instance = await this.create(state, index, data)
await instance.migrate()

return instance
})
}

Expand Down Expand Up @@ -298,6 +312,8 @@ export default class ValidatedTopic {
const index = queries[i].index
const instance = await this.create(state, index as I, data)

await instance.migrate()

instances.push(instance)
}

Expand Down Expand Up @@ -541,6 +557,38 @@ export default class ValidatedTopic {
}
}

/**
* Migrate data using predefined migration methods
*/
public async migrate() {
const type = this.constructor as typeof ValidatedTopic
const { migrations } = type

let migrated = false

// tslint:disable-next-line:strict-type-predicates
if (this._version === undefined) {
this._version = 0
}

if (this._version === type.version) {
return
}

for (const [version, methodName] of migrations.entries()) {
if (version > this._version) {
const method = (this as any)[methodName]
await method.call(this)
this._version = version
migrated = true
}
}

if (migrated) {
await this.set()
}
}

/**
* Throw a ValidateError including relevant details
*/
Expand Down
17 changes: 16 additions & 1 deletion src/decorators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ function wrapCreate(target: any, key: string, childrenType: any, validateFunctio
}

for (const [subkey, value] of Object.entries(this[key])) {
errors = errors.concat(await classValidator.validate(value))
errors = errors.concat(await classValidator.validate(value as any))

if (!validateFunction) {
continue
Expand Down Expand Up @@ -245,3 +245,18 @@ export function Acl(...acl: string[]) {
}
}
}

export function Migrate(version: number) {
return function (topic: ValidatedTopic, method: string) {
const type = topic.constructor as typeof ValidatedTopic
const { migrations } = type
migrations.set(version, method)

const migrationsArray = [...migrations.entries()]
const sortedMigrationArrays = migrationsArray.sort(([a], [b]) => a - b)
const sortedMigrations = new Map<number, any>(sortedMigrationArrays)

type.migrations = sortedMigrations
type.version = Array.from(sortedMigrations.keys()).pop() as number
}
}
12 changes: 11 additions & 1 deletion test/tomeTopic/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ class TestTomeTopic extends ValidatedTomeTopic {

@Type(() => TestTome)
@ValidateNested()
public children: TestTome[]
public children: TestTome[] = []
public whatever: any
}

Expand Down Expand Up @@ -80,6 +80,16 @@ describe('create', function () {
assert.strictEqual(tTest.name, 'ohai')
})

it('tome nested values are properly set and accessible', async () => {
const test = new TestTome()
test.childId = '1'

const tome = Tome.conjure({ name: 'ohai', children: [test] })
const tTest = await TestTomeTopic.create(state, { id: '1' }, tome)
assert.strictEqual(tTest.children.length, 1)
assert.strictEqual(tTest.children[0].childId, '1')
})

it('getData returns the tome instance', async () => {
const tTest = await TestTomeTopic.create(state, { id: '1' })
const data = tTest.getData()
Expand Down
6 changes: 3 additions & 3 deletions test/tomeTopic/iterate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@ describe('iterate', function () {

const vals = Object.values(tTest)

assert.strictEqual(vals[0], 'hello')
assert.strictEqual(vals[1][0], '1')
assert.strictEqual(vals[1], 'hello')
assert.strictEqual(vals[2][0], '1')
// assert.deepStrictEqual(vals, ['hello', ['1']])
})

Expand All @@ -72,7 +72,7 @@ describe('iterate', function () {
tTest.list = []
tTest.children = []

assert.deepStrictEqual(Object.keys(tTest), ['list', 'children'])
assert.deepStrictEqual(Object.keys(tTest), ['_version', 'list', 'children'])
})

it('lists nested tome keys', async () => {
Expand Down
6 changes: 3 additions & 3 deletions test/tomeTopic/log.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ describe('log, inspect, etc', function () {
tTest.list = ['b', 'c']
tTest.num = 1

const res = '{"name":"my name","list":["b","c"],"num":1}'
const res = '{"_version":0,"name":"my name","list":["b","c"],"num":1}'
assert.strictEqual(tTest.toString(), res)
assert.strictEqual((<any> tTest)[Symbol.toStringTag](), res)
assert.strictEqual(tTest.list.toString(), '["b","c"]')
Expand All @@ -63,9 +63,9 @@ describe('log, inspect, etc', function () {
tTest.name = 'my name'
tTest.list = ['b', 'c']

const res = 'TestTopic -> { name: \'my name\', list: [ \'b\', \'c\' ] }'
const res = 'TestTopic -> { _version: 0, name: \'my name\', list: [ \'b\', \'c\' ] }'

assert.strictEqual((<any> tTest).inspect(0, {}), 'TestTopic -> { name: \'my name\', list: [Array] }')
assert.strictEqual((<any> tTest).inspect(0, {}), 'TestTopic -> { _version: 0, name: \'my name\', list: [Array] }')
assert.strictEqual((<any> tTest).inspect(1, {}), res)
assert.strictEqual((<any> tTest)[inspect.custom](), res)
})
Expand Down
1 change: 1 addition & 0 deletions test/topic/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,5 @@ describe('Validated Topics', function () {
require('./add-set-touch')
require('./del')
require('./type-decorator')
require('./migrate')
})