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 ); } ); } );