From 54b3f0db726cef32838653a396d8aff02f835d8e Mon Sep 17 00:00:00 2001 From: Mike Gerwitz Date: Tue, 10 Sep 2019 12:09:08 -0400 Subject: [PATCH] TokenDao: Nominal typing This beings an experiment with nominal typing using what the TS community calls "branding". The lack of nominal types was one of my biggest disappointments with TS, so this should really help to mitigate bugs resulting from misappropriation of data. * src/server/token/Token.ts: New file. * src/server/token/TokenDao.ts: Use Token{Id,Namespace}. * src/server/token/TokenQueryResult.ts: Likewise. * src/types/misc.d.ts: Introduce NominalType and UnixTimestamp. * test/server/token/TokenDaoTest.ts: Use nominal types. --- src/server/token/Token.ts | 26 +++++++++++++++ src/server/token/TokenDao.ts | 28 ++++++++-------- src/server/token/TokenQueryResult.ts | 11 +++--- src/types/misc.d.ts | 46 +++++++++++++++++++++++++ test/server/token/TokenDaoTest.ts | 50 ++++++++++++++++------------ 5 files changed, 119 insertions(+), 42 deletions(-) create mode 100644 src/server/token/Token.ts create mode 100644 src/types/misc.d.ts diff --git a/src/server/token/Token.ts b/src/server/token/Token.ts new file mode 100644 index 0000000..5c13587 --- /dev/null +++ b/src/server/token/Token.ts @@ -0,0 +1,26 @@ +/** + * Token abstraction + * + * Copyright (C) 2010-2019 R-T Specialty, LLC. + * + * This file is part of the Liza Data Collection Framework. + * + * liza is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + + +export type TokenId = NominalType; + +export type TokenNamespace = NominalType; + diff --git a/src/server/token/TokenDao.ts b/src/server/token/TokenDao.ts index c357e63..c5a7d99 100644 --- a/src/server/token/TokenDao.ts +++ b/src/server/token/TokenDao.ts @@ -28,13 +28,15 @@ import { TokenType, } from "./TokenQueryResult"; +import { TokenId, TokenNamespace } from "./Token"; + /** * Token information */ export interface TokenData { - id: string, + id: TokenId, status: TokenStatus, } @@ -61,7 +63,7 @@ export default class TokenDao * * This is used for timestampping token updates. */ - private readonly _getTimestamp: () => number; + private readonly _getTimestamp: () => UnixTimestamp; /** @@ -74,7 +76,7 @@ export default class TokenDao constructor( collection: MongoCollection, root_field: string, - getTimestamp: () => number, + getTimestamp: () => UnixTimestamp, ) { this._collection = collection; @@ -97,8 +99,8 @@ export default class TokenDao */ updateToken( quote_id: number, - ns: string, - token: string, + ns: TokenNamespace, + token_id: TokenId, type: TokenType, data: string | null, ): Promise @@ -112,13 +114,13 @@ export default class TokenDao }; const token_data = { - [ root + 'last' ]: token, - [ root + 'lastStatus' ]: token_entry, - [ root + token + '.status' ]: token_entry, + [ root + 'last' ]: token_id, + [ root + 'lastStatus' ]: token_entry, + [ root + token_id + '.status' ]: token_entry, }; const token_log = { - [ root + token + '.statusLog' ]: token_entry, + [ root + token_id + '.statusLog' ]: token_entry, }; return new Promise( ( resolve, reject ) => @@ -159,7 +161,7 @@ export default class TokenDao * * @return token data */ - getToken( quote_id: number, ns: string, token_id: string ): + getToken( quote_id: number, ns: TokenNamespace, token_id: TokenId ): Promise { const root = this._genRoot( ns ) + '.'; @@ -240,11 +242,11 @@ export default class TokenDao * @return data of requested token */ private _getRequestedToken( - token_id: string, + token_id: TokenId, ns_data: TokenNamespaceData ): TokenData | null { - const reqtok = ns_data[ token_id ]; + const reqtok = ns_data[ token_id ]; if ( !reqtok ) { @@ -265,7 +267,7 @@ export default class TokenDao * * @return token root for namespace NS */ - private _genRoot( ns: string ): string + private _genRoot( ns: TokenNamespace ): string { // XXX: injectable return this._rootField + '.' + ns; diff --git a/src/server/token/TokenQueryResult.ts b/src/server/token/TokenQueryResult.ts index 86666d5..5faeab4 100644 --- a/src/server/token/TokenQueryResult.ts +++ b/src/server/token/TokenQueryResult.ts @@ -27,6 +27,9 @@ * compatibility with the existing data. */ +import { TokenId } from "./Token"; + + /** * Token status types as stored in the database */ @@ -53,12 +56,6 @@ export interface TokenNamespaceResults } -/** - * Token identifier (alias for clarity in interface definitions) - */ -type TokenId = string; - - /** * Token data associated with the given namespace * @@ -128,7 +125,7 @@ export interface TokenStatus /** * Unix timestamp representing when the status change occurred */ - readonly timestamp: number, + readonly timestamp: UnixTimestamp, /** * Arbitrary data associated with the status change diff --git a/src/types/misc.d.ts b/src/types/misc.d.ts new file mode 100644 index 0000000..6560f2b --- /dev/null +++ b/src/types/misc.d.ts @@ -0,0 +1,46 @@ +/** + * Miscellaneous types + * + * Copyright (C) 2010-2019 R-T Specialty, LLC. + * + * This file is part of the Liza Data Collection Framework. + * + * liza is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +/** + * Define a nominal type + * + * Nominal types are types that are enforced by name. Typescript implements + * structural subtyping (duck typing), which means that two values with the + * same structure are considered to be compatable. This opens the + * opportunity for certain classes of bugs: if we're expecting a Unix + * timestamp, but we're given a user id, it'd be nice if we could catch that + * at compile time. + * + * This uses a method the TS community calls "branding". It is abstracted + * behind a generic. See example uses below. I used the name `NominalType` + * rather than `Brand` since searching for the former provides much better + * literature on the topic, which will hopefully help in debugging when + * errors are inevitable encountered. + */ +type NominalType = K & { __nominal_type__: T }; + + +/** + * Unix timestamp + * + * Number of seconds since the Unix epoch (1970-01-01 UTC). + */ +type UnixTimestamp = NominalType; diff --git a/test/server/token/TokenDaoTest.ts b/test/server/token/TokenDaoTest.ts index b0b9f31..ad06159 100644 --- a/test/server/token/TokenDaoTest.ts +++ b/test/server/token/TokenDaoTest.ts @@ -29,6 +29,12 @@ import { TokenData, } from "../../../src/server/token/TokenDao"; +import { + TokenId, + TokenNamespace, +} from "../../../src/server/token/Token"; + + import { expect, use as chai_use } from 'chai'; chai_use( require( 'chai-as-promised' ) ); @@ -41,20 +47,17 @@ describe( 'server.token.TokenDao', () => { const field = 'foo_field'; const qid = 12345; - const ns = 'namespace'; - const tok_id = 'tok123'; + const ns = 'namespace'; + const tok_id = 'tok123'; const tok_type = 'DONE'; const data = "some data"; - const timestamp = 12345; + const timestamp = 12345; const root = field + '.' + ns; const coll: MongoCollection = { update( selector: any, given_data: any, options, callback ) { - expect( given_data.$set[ `${root}.lastStatus` ].timestamp ) - .to.be.greaterThan( 0 ); - const expected_entry: TokenStatus = { type: tok_type, timestamp: timestamp, @@ -102,8 +105,9 @@ describe( 'server.token.TokenDao', () => }; return expect( - new Sut( coll, 'foo', () => 0 ) - .updateToken( 0, 'ns', 'id', 'DONE', null ) + new Sut( coll, 'foo', () => 0 ).updateToken( + 0, 'ns', 'id', 'DONE', null + ) ).to.eventually.be.rejectedWith( expected_error ); } ); } ); @@ -113,22 +117,22 @@ describe( 'server.token.TokenDao', () => { const field = 'get_field'; const qid = 12345; - const ns = 'get_ns'; + const ns = 'get_ns'; const expected_status: TokenStatus = { type: 'ACTIVE', - timestamp: 0, + timestamp: 0, data: "", }; - ( <[string, string, TokenQueryResult, TokenData][]>[ + ( <[string, TokenId, TokenQueryResult, TokenData][]>[ [ 'retrieves token by id', - 'tok123', + 'tok123', { [field]: { [ns]: { - last: 'tok123', + last: 'tok123', lastStatus: expected_status, tok123: { @@ -139,18 +143,18 @@ describe( 'server.token.TokenDao', () => }, }, { - id: 'tok123', + id: 'tok123', status: expected_status, }, ], [ 'returns null for namespace if token is not found', - 'tok123', + 'tok123', { [field]: { [ns]: { - last: 'something', + last: 'something', lastStatus: expected_status, // just to make sure we don't grab another tok @@ -166,7 +170,7 @@ describe( 'server.token.TokenDao', () => [ 'returns null for field if namespace is not found', - 'tok123', + 'tok123', { [field]: {}, }, @@ -175,11 +179,11 @@ describe( 'server.token.TokenDao', () => [ 'returns lastest modified token given no token id', - '', + '', { [field]: { [ns]: { - last: 'toklast', + last: 'toklast', lastStatus: expected_status, toklast: { @@ -190,7 +194,7 @@ describe( 'server.token.TokenDao', () => }, }, { - id: 'toklast', + id: 'toklast', status: expected_status, }, ], @@ -207,7 +211,8 @@ describe( 'server.token.TokenDao', () => }; return expect( - new Sut( coll, field, () => 0 ).getToken( qid, ns, tok_id ) + new Sut( coll, field, () => 0 ) + .getToken( qid, ns, tok_id ) ).to.eventually.deep.equal( expected ); } ) ); @@ -227,7 +232,8 @@ describe( 'server.token.TokenDao', () => }; return expect( - new Sut( coll, 'foo', () => 0 ).getToken( 0, 'ns', 'id' ) + new Sut( coll, 'foo', () => 0 ) + .getToken( 0, 'ns', 'id' ) ).to.eventually.be.rejectedWith( expected_error ); } ); } );