1
0
Fork 0

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.
master
Mike Gerwitz 2019-09-10 12:09:08 -04:00
parent 9ea66c0440
commit 54b3f0db72
5 changed files with 119 additions and 42 deletions

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
export type TokenId = NominalType<string, 'TokenId'>;
export type TokenNamespace = NominalType<string, 'TokenNamespace'>;

View File

@ -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<void>
@ -112,13 +114,13 @@ export default class TokenDao
};
const token_data = {
[ root + 'last' ]: token,
[ root + 'last' ]: token_id,
[ root + 'lastStatus' ]: token_entry,
[ root + token + '.status' ]: 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<TokenData|null>
{
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 = <TokenEntry>ns_data[ token_id ];
const reqtok = <TokenEntry>ns_data[ <string>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;

View File

@ -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

46
src/types/misc.d.ts vendored 100644
View File

@ -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 <http://www.gnu.org/licenses/>.
*/
/**
* 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, T> = K & { __nominal_type__: T };
/**
* Unix timestamp
*
* Number of seconds since the Unix epoch (1970-01-01 UTC).
*/
type UnixTimestamp = NominalType<number, 'UnixTimestamp'>;

View File

@ -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 = <TokenNamespace>'namespace';
const tok_id = <TokenId>'tok123';
const tok_type = 'DONE';
const data = "some data";
const timestamp = 12345;
const timestamp = <UnixTimestamp>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', () => <UnixTimestamp>0 ).updateToken(
0, <TokenNamespace>'ns', <TokenId>'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 = <TokenNamespace>'get_ns';
const expected_status: TokenStatus = {
type: 'ACTIVE',
timestamp: 0,
timestamp: <UnixTimestamp>0,
data: "",
};
( <[string, string, TokenQueryResult, TokenData][]>[
( <[string, TokenId, TokenQueryResult, TokenData][]>[
[
'retrieves token by id',
'tok123',
<TokenId>'tok123',
{
[field]: {
[ns]: {
last: 'tok123',
last: <TokenId>'tok123',
lastStatus: expected_status,
tok123: {
@ -139,18 +143,18 @@ describe( 'server.token.TokenDao', () =>
},
},
{
id: 'tok123',
id: <TokenId>'tok123',
status: expected_status,
},
],
[
'returns null for namespace if token is not found',
'tok123',
<TokenId>'tok123',
{
[field]: {
[ns]: {
last: 'something',
last: <TokenId>'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',
<TokenId>'tok123',
{
[field]: {},
},
@ -175,11 +179,11 @@ describe( 'server.token.TokenDao', () =>
[
'returns lastest modified token given no token id',
'',
<TokenId>'',
{
[field]: {
[ns]: {
last: 'toklast',
last: <TokenId>'toklast',
lastStatus: expected_status,
toklast: {
@ -190,7 +194,7 @@ describe( 'server.token.TokenDao', () =>
},
},
{
id: 'toklast',
id: <TokenId>'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, () => <UnixTimestamp>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', () => <UnixTimestamp>0 )
.getToken( 0, <TokenNamespace>'ns', <TokenId>'id' )
).to.eventually.be.rejectedWith( expected_error );
} );
} );