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
parent
9ea66c0440
commit
54b3f0db72
|
@ -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'>;
|
||||||
|
|
|
@ -28,13 +28,15 @@ import {
|
||||||
TokenType,
|
TokenType,
|
||||||
} from "./TokenQueryResult";
|
} from "./TokenQueryResult";
|
||||||
|
|
||||||
|
import { TokenId, TokenNamespace } from "./Token";
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Token information
|
* Token information
|
||||||
*/
|
*/
|
||||||
export interface TokenData
|
export interface TokenData
|
||||||
{
|
{
|
||||||
id: string,
|
id: TokenId,
|
||||||
status: TokenStatus,
|
status: TokenStatus,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -61,7 +63,7 @@ export default class TokenDao
|
||||||
*
|
*
|
||||||
* This is used for timestampping token updates.
|
* This is used for timestampping token updates.
|
||||||
*/
|
*/
|
||||||
private readonly _getTimestamp: () => number;
|
private readonly _getTimestamp: () => UnixTimestamp;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -74,7 +76,7 @@ export default class TokenDao
|
||||||
constructor(
|
constructor(
|
||||||
collection: MongoCollection,
|
collection: MongoCollection,
|
||||||
root_field: string,
|
root_field: string,
|
||||||
getTimestamp: () => number,
|
getTimestamp: () => UnixTimestamp,
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
this._collection = collection;
|
this._collection = collection;
|
||||||
|
@ -97,8 +99,8 @@ export default class TokenDao
|
||||||
*/
|
*/
|
||||||
updateToken(
|
updateToken(
|
||||||
quote_id: number,
|
quote_id: number,
|
||||||
ns: string,
|
ns: TokenNamespace,
|
||||||
token: string,
|
token_id: TokenId,
|
||||||
type: TokenType,
|
type: TokenType,
|
||||||
data: string | null,
|
data: string | null,
|
||||||
): Promise<void>
|
): Promise<void>
|
||||||
|
@ -112,13 +114,13 @@ export default class TokenDao
|
||||||
};
|
};
|
||||||
|
|
||||||
const token_data = {
|
const token_data = {
|
||||||
[ root + 'last' ]: token,
|
[ root + 'last' ]: token_id,
|
||||||
[ root + 'lastStatus' ]: token_entry,
|
[ root + 'lastStatus' ]: token_entry,
|
||||||
[ root + token + '.status' ]: token_entry,
|
[ root + token_id + '.status' ]: token_entry,
|
||||||
};
|
};
|
||||||
|
|
||||||
const token_log = {
|
const token_log = {
|
||||||
[ root + token + '.statusLog' ]: token_entry,
|
[ root + token_id + '.statusLog' ]: token_entry,
|
||||||
};
|
};
|
||||||
|
|
||||||
return new Promise( ( resolve, reject ) =>
|
return new Promise( ( resolve, reject ) =>
|
||||||
|
@ -159,7 +161,7 @@ export default class TokenDao
|
||||||
*
|
*
|
||||||
* @return token data
|
* @return token data
|
||||||
*/
|
*/
|
||||||
getToken( quote_id: number, ns: string, token_id: string ):
|
getToken( quote_id: number, ns: TokenNamespace, token_id: TokenId ):
|
||||||
Promise<TokenData|null>
|
Promise<TokenData|null>
|
||||||
{
|
{
|
||||||
const root = this._genRoot( ns ) + '.';
|
const root = this._genRoot( ns ) + '.';
|
||||||
|
@ -240,11 +242,11 @@ export default class TokenDao
|
||||||
* @return data of requested token
|
* @return data of requested token
|
||||||
*/
|
*/
|
||||||
private _getRequestedToken(
|
private _getRequestedToken(
|
||||||
token_id: string,
|
token_id: TokenId,
|
||||||
ns_data: TokenNamespaceData
|
ns_data: TokenNamespaceData
|
||||||
): TokenData | null
|
): TokenData | null
|
||||||
{
|
{
|
||||||
const reqtok = <TokenEntry>ns_data[ token_id ];
|
const reqtok = <TokenEntry>ns_data[ <string>token_id ];
|
||||||
|
|
||||||
if ( !reqtok )
|
if ( !reqtok )
|
||||||
{
|
{
|
||||||
|
@ -265,7 +267,7 @@ export default class TokenDao
|
||||||
*
|
*
|
||||||
* @return token root for namespace NS
|
* @return token root for namespace NS
|
||||||
*/
|
*/
|
||||||
private _genRoot( ns: string ): string
|
private _genRoot( ns: TokenNamespace ): string
|
||||||
{
|
{
|
||||||
// XXX: injectable
|
// XXX: injectable
|
||||||
return this._rootField + '.' + ns;
|
return this._rootField + '.' + ns;
|
||||||
|
|
|
@ -27,6 +27,9 @@
|
||||||
* compatibility with the existing data.
|
* compatibility with the existing data.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { TokenId } from "./Token";
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Token status types as stored in the database
|
* 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
|
* Token data associated with the given namespace
|
||||||
*
|
*
|
||||||
|
@ -128,7 +125,7 @@ export interface TokenStatus
|
||||||
/**
|
/**
|
||||||
* Unix timestamp representing when the status change occurred
|
* Unix timestamp representing when the status change occurred
|
||||||
*/
|
*/
|
||||||
readonly timestamp: number,
|
readonly timestamp: UnixTimestamp,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Arbitrary data associated with the status change
|
* Arbitrary data associated with the status change
|
||||||
|
|
|
@ -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'>;
|
|
@ -29,6 +29,12 @@ import {
|
||||||
TokenData,
|
TokenData,
|
||||||
} from "../../../src/server/token/TokenDao";
|
} from "../../../src/server/token/TokenDao";
|
||||||
|
|
||||||
|
import {
|
||||||
|
TokenId,
|
||||||
|
TokenNamespace,
|
||||||
|
} from "../../../src/server/token/Token";
|
||||||
|
|
||||||
|
|
||||||
import { expect, use as chai_use } from 'chai';
|
import { expect, use as chai_use } from 'chai';
|
||||||
chai_use( require( 'chai-as-promised' ) );
|
chai_use( require( 'chai-as-promised' ) );
|
||||||
|
|
||||||
|
@ -41,20 +47,17 @@ describe( 'server.token.TokenDao', () =>
|
||||||
{
|
{
|
||||||
const field = 'foo_field';
|
const field = 'foo_field';
|
||||||
const qid = 12345;
|
const qid = 12345;
|
||||||
const ns = 'namespace';
|
const ns = <TokenNamespace>'namespace';
|
||||||
const tok_id = 'tok123';
|
const tok_id = <TokenId>'tok123';
|
||||||
const tok_type = 'DONE';
|
const tok_type = 'DONE';
|
||||||
const data = "some data";
|
const data = "some data";
|
||||||
const timestamp = 12345;
|
const timestamp = <UnixTimestamp>12345;
|
||||||
|
|
||||||
const root = field + '.' + ns;
|
const root = field + '.' + ns;
|
||||||
|
|
||||||
const coll: MongoCollection = {
|
const coll: MongoCollection = {
|
||||||
update( selector: any, given_data: any, options, callback )
|
update( selector: any, given_data: any, options, callback )
|
||||||
{
|
{
|
||||||
expect( given_data.$set[ `${root}.lastStatus` ].timestamp )
|
|
||||||
.to.be.greaterThan( 0 );
|
|
||||||
|
|
||||||
const expected_entry: TokenStatus = {
|
const expected_entry: TokenStatus = {
|
||||||
type: tok_type,
|
type: tok_type,
|
||||||
timestamp: timestamp,
|
timestamp: timestamp,
|
||||||
|
@ -102,8 +105,9 @@ describe( 'server.token.TokenDao', () =>
|
||||||
};
|
};
|
||||||
|
|
||||||
return expect(
|
return expect(
|
||||||
new Sut( coll, 'foo', () => 0 )
|
new Sut( coll, 'foo', () => <UnixTimestamp>0 ).updateToken(
|
||||||
.updateToken( 0, 'ns', 'id', 'DONE', null )
|
0, <TokenNamespace>'ns', <TokenId>'id', 'DONE', null
|
||||||
|
)
|
||||||
).to.eventually.be.rejectedWith( expected_error );
|
).to.eventually.be.rejectedWith( expected_error );
|
||||||
} );
|
} );
|
||||||
} );
|
} );
|
||||||
|
@ -113,22 +117,22 @@ describe( 'server.token.TokenDao', () =>
|
||||||
{
|
{
|
||||||
const field = 'get_field';
|
const field = 'get_field';
|
||||||
const qid = 12345;
|
const qid = 12345;
|
||||||
const ns = 'get_ns';
|
const ns = <TokenNamespace>'get_ns';
|
||||||
|
|
||||||
const expected_status: TokenStatus = {
|
const expected_status: TokenStatus = {
|
||||||
type: 'ACTIVE',
|
type: 'ACTIVE',
|
||||||
timestamp: 0,
|
timestamp: <UnixTimestamp>0,
|
||||||
data: "",
|
data: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
( <[string, string, TokenQueryResult, TokenData][]>[
|
( <[string, TokenId, TokenQueryResult, TokenData][]>[
|
||||||
[
|
[
|
||||||
'retrieves token by id',
|
'retrieves token by id',
|
||||||
'tok123',
|
<TokenId>'tok123',
|
||||||
{
|
{
|
||||||
[field]: {
|
[field]: {
|
||||||
[ns]: {
|
[ns]: {
|
||||||
last: 'tok123',
|
last: <TokenId>'tok123',
|
||||||
lastStatus: expected_status,
|
lastStatus: expected_status,
|
||||||
|
|
||||||
tok123: {
|
tok123: {
|
||||||
|
@ -139,18 +143,18 @@ describe( 'server.token.TokenDao', () =>
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'tok123',
|
id: <TokenId>'tok123',
|
||||||
status: expected_status,
|
status: expected_status,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
||||||
[
|
[
|
||||||
'returns null for namespace if token is not found',
|
'returns null for namespace if token is not found',
|
||||||
'tok123',
|
<TokenId>'tok123',
|
||||||
{
|
{
|
||||||
[field]: {
|
[field]: {
|
||||||
[ns]: {
|
[ns]: {
|
||||||
last: 'something',
|
last: <TokenId>'something',
|
||||||
lastStatus: expected_status,
|
lastStatus: expected_status,
|
||||||
|
|
||||||
// just to make sure we don't grab another tok
|
// 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',
|
'returns null for field if namespace is not found',
|
||||||
'tok123',
|
<TokenId>'tok123',
|
||||||
{
|
{
|
||||||
[field]: {},
|
[field]: {},
|
||||||
},
|
},
|
||||||
|
@ -175,11 +179,11 @@ describe( 'server.token.TokenDao', () =>
|
||||||
|
|
||||||
[
|
[
|
||||||
'returns lastest modified token given no token id',
|
'returns lastest modified token given no token id',
|
||||||
'',
|
<TokenId>'',
|
||||||
{
|
{
|
||||||
[field]: {
|
[field]: {
|
||||||
[ns]: {
|
[ns]: {
|
||||||
last: 'toklast',
|
last: <TokenId>'toklast',
|
||||||
lastStatus: expected_status,
|
lastStatus: expected_status,
|
||||||
|
|
||||||
toklast: {
|
toklast: {
|
||||||
|
@ -190,7 +194,7 @@ describe( 'server.token.TokenDao', () =>
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'toklast',
|
id: <TokenId>'toklast',
|
||||||
status: expected_status,
|
status: expected_status,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -207,7 +211,8 @@ describe( 'server.token.TokenDao', () =>
|
||||||
};
|
};
|
||||||
|
|
||||||
return expect(
|
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 );
|
).to.eventually.deep.equal( expected );
|
||||||
} )
|
} )
|
||||||
);
|
);
|
||||||
|
@ -227,7 +232,8 @@ describe( 'server.token.TokenDao', () =>
|
||||||
};
|
};
|
||||||
|
|
||||||
return expect(
|
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 );
|
).to.eventually.be.rejectedWith( expected_error );
|
||||||
} );
|
} );
|
||||||
} );
|
} );
|
||||||
|
|
Loading…
Reference in New Issue