Skip to content

Commit d478e3b

Browse files
committed
feature: migrations
- Add `Migration` decorator - code, docs, and tests
1 parent 864cc36 commit d478e3b

File tree

6 files changed

+129
-8
lines changed

6 files changed

+129
-8
lines changed

Readme.md

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,63 @@ topic directly as you user command parameter; all you will
254254
then need to do is to set the index at some point before you attempt
255255
to record any operations:
256256

257+
#### Migrating topics
258+
259+
> `./lib/modules/modulename/topics/Player.ts`
260+
261+
```typescript
262+
import { ValidatedTopic, ValidateNested, IsUUID, IsAlpha } from 'mage-validator';
263+
import { Type } from 'class-transform';
264+
import PlayerData from '../topics/PlayerData'
265+
266+
class Index {
267+
@isUUID(5)
268+
playerId: string
269+
}
270+
271+
export default class {
272+
// Index configuration
273+
public static readonly index = ['playerId']
274+
public static readonly indexType = Index
275+
276+
// Vaults configuration (optional)
277+
public static readonly vaults = {}
278+
279+
// Attribute instances
280+
@IsAlpha()
281+
public name: string
282+
283+
public age: number
284+
285+
@ValidateNested()
286+
@Type(() => PlayerData)
287+
public data: PlayerData
288+
289+
@Migrate(1)
290+
public setDefaultName() {
291+
if (!this.name) {
292+
this.name = 'DefaultName' + Math.floor(Math.random() * 1000)
293+
}
294+
}
295+
296+
@Migrate(2)
297+
public setDefaultAge() {
298+
if (!this.age) {
299+
this.age = 0
300+
}
301+
}
302+
}
303+
```
304+
305+
All `ValdiatedTopic` have a `_version`, attribute used for data versioning. The default value is 0.
306+
307+
You can define methods to be used as migration steps. Use the `Migration`
308+
decorator to define the version this step will upgrade the data to.
309+
Only migration steps with a higher step value than the current data will
310+
be executed.
311+
312+
Migrations will **not** be executed on newly created topic instances.
313+
257314
#### Topics as user command parameters
258315

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

src/classes/ValidatedTopic.ts

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ export type PartialIndex<I> = {
3434
* that return a properly typed output.
3535
*/
3636
export interface IStaticThis<I, T> {
37+
version: number,
3738
// Todo: any should be I!
3839
indexType: { new(): any },
3940

@@ -98,8 +99,13 @@ export default class ValidatedTopic {
9899
public static readonly indexType: any
99100
public static readonly vaults = {}
100101

102+
public static version = 0
103+
public static migrations = new Map<number, string>()
104+
101105
private static _className: string
102106

107+
public _version: number = 0
108+
103109
/**
104110
* Return the current class name
105111
*
@@ -161,6 +167,11 @@ export default class ValidatedTopic {
161167
instance.setState(state)
162168
await instance.setIndex(index)
163169

170+
/* istanbul ignore next */
171+
if (!instance._version) {
172+
instance._version = this.version
173+
}
174+
164175
return instance
165176
}
166177

@@ -228,7 +239,10 @@ export default class ValidatedTopic {
228239
return undefined
229240
}
230241

231-
return this.create(state, index, data)
242+
const instance = await this.create(state, index, data)
243+
await instance.migrate()
244+
245+
return instance
232246
})
233247
}
234248

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

315+
await instance.migrate()
316+
301317
instances.push(instance)
302318
}
303319

@@ -541,6 +557,38 @@ export default class ValidatedTopic {
541557
}
542558
}
543559

560+
/**
561+
* Migrate data using predefined migration methods
562+
*/
563+
public async migrate() {
564+
const type = this.constructor as typeof ValidatedTopic
565+
const { migrations } = type
566+
567+
let migrated = false
568+
569+
// tslint:disable-next-line:strict-type-predicates
570+
if (this._version === undefined) {
571+
this._version = 0
572+
}
573+
574+
if (this._version === type.version) {
575+
return
576+
}
577+
578+
for (const [version, methodName] of migrations.entries()) {
579+
if (version > this._version) {
580+
const method = (this as any)[methodName]
581+
await method.call(this)
582+
this._version = version
583+
migrated = true
584+
}
585+
}
586+
587+
if (migrated) {
588+
await this.set()
589+
}
590+
}
591+
544592
/**
545593
* Throw a ValidateError including relevant details
546594
*/

src/decorators.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ function wrapCreate(target: any, key: string, childrenType: any, validateFunctio
6565
}
6666

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

7070
if (!validateFunction) {
7171
continue
@@ -245,3 +245,18 @@ export function Acl(...acl: string[]) {
245245
}
246246
}
247247
}
248+
249+
export function Migrate(version: number) {
250+
return function (topic: ValidatedTopic, method: string) {
251+
const type = topic.constructor as typeof ValidatedTopic
252+
const { migrations } = type
253+
migrations.set(version, method)
254+
255+
const migrationsArray = [...migrations.entries()]
256+
const sortedMigrationArrays = migrationsArray.sort(([a], [b]) => a - b)
257+
const sortedMigrations = new Map<number, any>(sortedMigrationArrays)
258+
259+
type.migrations = sortedMigrations
260+
type.version = Array.from(sortedMigrations.keys()).pop() as number
261+
}
262+
}

test/tomeTopic/iterate.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,8 @@ describe('iterate', function () {
4747

4848
const vals = Object.values(tTest)
4949

50-
assert.strictEqual(vals[0], 'hello')
51-
assert.strictEqual(vals[1][0], '1')
50+
assert.strictEqual(vals[1], 'hello')
51+
assert.strictEqual(vals[2][0], '1')
5252
// assert.deepStrictEqual(vals, ['hello', ['1']])
5353
})
5454

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

75-
assert.deepStrictEqual(Object.keys(tTest), ['list', 'children'])
75+
assert.deepStrictEqual(Object.keys(tTest), ['_version', 'list', 'children'])
7676
})
7777

7878
it('lists nested tome keys', async () => {

test/tomeTopic/log.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ describe('log, inspect, etc', function () {
5252
tTest.list = ['b', 'c']
5353
tTest.num = 1
5454

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

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

68-
assert.strictEqual((<any> tTest).inspect(0, {}), 'TestTopic -> { name: \'my name\', list: [Array] }')
68+
assert.strictEqual((<any> tTest).inspect(0, {}), 'TestTopic -> { _version: 0, name: \'my name\', list: [Array] }')
6969
assert.strictEqual((<any> tTest).inspect(1, {}), res)
7070
assert.strictEqual((<any> tTest)[inspect.custom](), res)
7171
})

test/topic/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,4 +58,5 @@ describe('Validated Topics', function () {
5858
require('./add-set-touch')
5959
require('./del')
6060
require('./type-decorator')
61+
require('./migrate')
6162
})

0 commit comments

Comments
 (0)