1
0
Fork 0

TokenDao: Better error handling for unknown tokens

Rather than replying with null, which complicates using the returned promise
efficiently, we'll respond with a unique error that allows us to distinguish
between a database failure and a missing token.

These are more traditional errors, but we're moving toward structured
logging, so I want error objects that provide more context.  I'll explore
that a bit more in next commit.  Unfortunately, the untypedness of Promise
rejections make for a less than desirable situation here.  Async/await is
not yet an option since we're still compiling to ES5 (have to support
IE11), and TS compiles async/await into generators for environments that
don't support them, which also are not available in ES5.

* src/server/service/TokenedService.js (_getQuoteToken): Remove null check,
  since this situation can no longer occur.
* src/server/token/MongoTokenDao.ts (getToken): Remove null from return type
  union; reject with `UnknownTokenError' instead.
* src/server/token/TokenDao.ts: Modify interface accordingly.
* src/server/token/UnknownTokenError.ts: New class.
* test/server/token/MongoTokenDaoTest.ts: Modify tests accordingly.  Add
  missing test for latest token namespace missing.
master
Mike Gerwitz 2019-09-17 09:44:13 -04:00
parent fb88ceeae6
commit 1f66a25658
5 changed files with 109 additions and 36 deletions

View File

@ -237,20 +237,7 @@ module.exports = Trait( 'TokenedService' )
'private _getQuoteToken': function( quote, tokid, callback )
{
this._dao.getToken( quote.getId(), this._ns, tokid )
.then( token =>
{
if ( tokid && !token )
{
callback(
Error( "Token not found: " + tokid ),
null
);
return;
}
callback( null, token );
} )
.then( token => callback( null, token ) )
.catch( err => callback( err, null ) );
},

View File

@ -30,8 +30,9 @@ import {
TokenType,
} from "./TokenDao";
import { TokenId, TokenNamespace } from "./Token";
import { DocumentId } from "../../document/Document";
import { TokenId, TokenNamespace } from "./Token";
import { UnknownTokenError } from "./UnknownTokenError";
/**
@ -155,7 +156,7 @@ export class MongoTokenDao implements TokenDao
* @return token data
*/
getToken( doc_id: DocumentId, ns: TokenNamespace, token_id: TokenId ):
Promise<TokenData|null>
Promise<TokenData>
{
const root = this._genRoot( ns ) + '.';
const fields: any = {};
@ -187,15 +188,17 @@ export class MongoTokenDao implements TokenDao
if ( !field[ ns ] )
{
resolve( null );
reject( new UnknownTokenError(
`Unknown token namespace '${ns}' for document '${doc_id}`
) );
return;
}
const ns_data = <TokenNamespaceData>field[ ns ];
resolve( ( token_id )
? this._getRequestedToken( token_id, ns_data )
: this._getLatestToken( ns_data )
? this._getRequestedToken( doc_id, ns, token_id, ns_data )
: this._getLatestToken( doc_id, ns, ns_data )
);
}
);
@ -204,19 +207,30 @@ export class MongoTokenDao implements TokenDao
/**
* Retrieve latest token data, or `null` if none
* Retrieve latest token data
*
* @param doc_id document id
* @param ns token namespace
* @param ns_data namespace data
*
* @return data of latest token in namespace
*
* @throws UnknownTokenError if last token data is missing
*/
private _getLatestToken( ns_data: TokenNamespaceData ): TokenData | null
private _getLatestToken(
doc_id: DocumentId,
ns: TokenNamespace,
ns_data: TokenNamespaceData
): TokenData
{
var last = ns_data.last;
if ( !last )
{
return null;
throw new UnknownTokenError(
`Failed to locate last token for namespace '${ns}'` +
`on document '${doc_id}'`
);
}
return {
@ -227,23 +241,32 @@ export class MongoTokenDao implements TokenDao
/**
* Retrieve latest token data, or `null` if none
* Retrieve latest token data
*
* @param doc_id document id
* @param ns token namespace
* @param token_id token identifier for namespace associated with NS_DATA
* @param ns_data namespace data
*
* @return data of requested token
*
* @throws UnknownTokenError if token data is missing
*/
private _getRequestedToken(
doc_id: DocumentId,
ns: TokenNamespace,
token_id: TokenId,
ns_data: TokenNamespaceData
): TokenData | null
): TokenData
{
const reqtok = <TokenEntry>ns_data[ <string>token_id ];
if ( !reqtok )
{
return null;
throw new UnknownTokenError(
`Missing data for requested token '${ns}.${token_id}'` +
`for document '${doc_id}'`
);
}
return {

View File

@ -47,7 +47,7 @@ export interface TokenDao
doc_id: DocumentId,
ns: TokenNamespace,
token_id: TokenId
): Promise<TokenData|null>;
): Promise<TokenData>;
}

View File

@ -0,0 +1,27 @@
/**
* Unknown token error
*
* 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/>.
*
* This still uses ease.js because it does a good job of transparently
* creating Error subtypes.
*/
const { Class } = require( 'easejs' );
export const UnknownTokenError = Class( 'UnknownTokenError' ).extend( Error, {} );

View File

@ -33,6 +33,7 @@ import {
} from "../../../src/server/token/Token";
import { DocumentId } from "../../../src/document/Document";
import { UnknownTokenError } from "../../../src/server/token/UnknownTokenError";
import { expect, use as chai_use } from 'chai';
@ -129,7 +130,7 @@ describe( 'server.token.TokenDao', () =>
data: "",
};
( <[string, TokenId, TokenQueryResult, TokenData][]>[
( <[string, TokenId, TokenQueryResult, TokenData|null, any][]>[
[
'retrieves token by id',
<TokenId>'tok123',
@ -150,10 +151,11 @@ describe( 'server.token.TokenDao', () =>
id: <TokenId>'tok123',
status: expected_status,
},
null,
],
[
'returns null for namespace if token is not found',
'rejects for namespace if token is not found',
<TokenId>'tok123',
{
[field]: {
@ -170,19 +172,21 @@ describe( 'server.token.TokenDao', () =>
},
},
null,
`${ns}.tok123`,
],
[
'returns null for field if namespace is not found',
'rejects if namespace is not found',
<TokenId>'tok123',
{
[field]: {},
},
null,
ns,
],
[
'returns lastest modified token given no token id',
'returns last modified token given no token id',
<TokenId>'',
{
[field]: {
@ -201,23 +205,55 @@ describe( 'server.token.TokenDao', () =>
id: <TokenId>'toklast',
status: expected_status,
},
null,
],
] ).forEach( ( [ label, tok_id, result, expected ] ) =>
[
'rejects unknown last modified token given no token id',
<TokenId>'',
{
[field]: {
[ns]: {},
},
},
null,
ns,
],
[
'rejects unknown namespace token given no token id',
<TokenId>'',
{
[field]: {},
},
null,
ns,
],
] ).forEach( ( [ label, tok_id, dbresult, expected, failure ] ) =>
it( label, () =>
{
const coll: MongoCollection = {
findOne( _selector, _fields, callback )
{
callback( null, result );
callback( null, dbresult );
},
update() {},
};
return expect(
new Sut( coll, field, () => <UnixTimestamp>0 )
.getToken( did, ns, tok_id )
).to.eventually.deep.equal( expected );
const result = new Sut( coll, field, () => <UnixTimestamp>0 )
.getToken( did, ns, tok_id );
return ( failure !== null )
? Promise.all( [
expect( result ).to.eventually.be.rejectedWith(
UnknownTokenError, failure
),
expect( result ).to.eventually.be.rejectedWith(
UnknownTokenError, ''+did
),
] )
: expect( result ).to.eventually.deep.equal( expected );
} )
);