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
parent
fb88ceeae6
commit
1f66a25658
|
@ -237,20 +237,7 @@ module.exports = Trait( 'TokenedService' )
|
||||||
'private _getQuoteToken': function( quote, tokid, callback )
|
'private _getQuoteToken': function( quote, tokid, callback )
|
||||||
{
|
{
|
||||||
this._dao.getToken( quote.getId(), this._ns, tokid )
|
this._dao.getToken( quote.getId(), this._ns, tokid )
|
||||||
.then( token =>
|
.then( token => callback( null, token ) )
|
||||||
{
|
|
||||||
if ( tokid && !token )
|
|
||||||
{
|
|
||||||
callback(
|
|
||||||
Error( "Token not found: " + tokid ),
|
|
||||||
null
|
|
||||||
);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
callback( null, token );
|
|
||||||
} )
|
|
||||||
.catch( err => callback( err, null ) );
|
.catch( err => callback( err, null ) );
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -30,8 +30,9 @@ import {
|
||||||
TokenType,
|
TokenType,
|
||||||
} from "./TokenDao";
|
} from "./TokenDao";
|
||||||
|
|
||||||
import { TokenId, TokenNamespace } from "./Token";
|
|
||||||
import { DocumentId } from "../../document/Document";
|
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
|
* @return token data
|
||||||
*/
|
*/
|
||||||
getToken( doc_id: DocumentId, ns: TokenNamespace, token_id: TokenId ):
|
getToken( doc_id: DocumentId, ns: TokenNamespace, token_id: TokenId ):
|
||||||
Promise<TokenData|null>
|
Promise<TokenData>
|
||||||
{
|
{
|
||||||
const root = this._genRoot( ns ) + '.';
|
const root = this._genRoot( ns ) + '.';
|
||||||
const fields: any = {};
|
const fields: any = {};
|
||||||
|
@ -187,15 +188,17 @@ export class MongoTokenDao implements TokenDao
|
||||||
|
|
||||||
if ( !field[ ns ] )
|
if ( !field[ ns ] )
|
||||||
{
|
{
|
||||||
resolve( null );
|
reject( new UnknownTokenError(
|
||||||
|
`Unknown token namespace '${ns}' for document '${doc_id}`
|
||||||
|
) );
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ns_data = <TokenNamespaceData>field[ ns ];
|
const ns_data = <TokenNamespaceData>field[ ns ];
|
||||||
|
|
||||||
resolve( ( token_id )
|
resolve( ( token_id )
|
||||||
? this._getRequestedToken( token_id, ns_data )
|
? this._getRequestedToken( doc_id, ns, token_id, ns_data )
|
||||||
: this._getLatestToken( 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
|
* @param ns_data namespace data
|
||||||
*
|
*
|
||||||
* @return data of latest token in namespace
|
* @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;
|
var last = ns_data.last;
|
||||||
|
|
||||||
if ( !last )
|
if ( !last )
|
||||||
{
|
{
|
||||||
return null;
|
throw new UnknownTokenError(
|
||||||
|
`Failed to locate last token for namespace '${ns}'` +
|
||||||
|
`on document '${doc_id}'`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
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 token_id token identifier for namespace associated with NS_DATA
|
||||||
* @param ns_data namespace data
|
* @param ns_data namespace data
|
||||||
*
|
*
|
||||||
* @return data of requested token
|
* @return data of requested token
|
||||||
|
*
|
||||||
|
* @throws UnknownTokenError if token data is missing
|
||||||
*/
|
*/
|
||||||
private _getRequestedToken(
|
private _getRequestedToken(
|
||||||
|
doc_id: DocumentId,
|
||||||
|
ns: TokenNamespace,
|
||||||
token_id: TokenId,
|
token_id: TokenId,
|
||||||
ns_data: TokenNamespaceData
|
ns_data: TokenNamespaceData
|
||||||
): TokenData | null
|
): TokenData
|
||||||
{
|
{
|
||||||
const reqtok = <TokenEntry>ns_data[ <string>token_id ];
|
const reqtok = <TokenEntry>ns_data[ <string>token_id ];
|
||||||
|
|
||||||
if ( !reqtok )
|
if ( !reqtok )
|
||||||
{
|
{
|
||||||
return null;
|
throw new UnknownTokenError(
|
||||||
|
`Missing data for requested token '${ns}.${token_id}'` +
|
||||||
|
`for document '${doc_id}'`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -47,7 +47,7 @@ export interface TokenDao
|
||||||
doc_id: DocumentId,
|
doc_id: DocumentId,
|
||||||
ns: TokenNamespace,
|
ns: TokenNamespace,
|
||||||
token_id: TokenId
|
token_id: TokenId
|
||||||
): Promise<TokenData|null>;
|
): Promise<TokenData>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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, {} );
|
|
@ -33,6 +33,7 @@ import {
|
||||||
} from "../../../src/server/token/Token";
|
} from "../../../src/server/token/Token";
|
||||||
|
|
||||||
import { DocumentId } from "../../../src/document/Document";
|
import { DocumentId } from "../../../src/document/Document";
|
||||||
|
import { UnknownTokenError } from "../../../src/server/token/UnknownTokenError";
|
||||||
|
|
||||||
|
|
||||||
import { expect, use as chai_use } from 'chai';
|
import { expect, use as chai_use } from 'chai';
|
||||||
|
@ -129,7 +130,7 @@ describe( 'server.token.TokenDao', () =>
|
||||||
data: "",
|
data: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
( <[string, TokenId, TokenQueryResult, TokenData][]>[
|
( <[string, TokenId, TokenQueryResult, TokenData|null, any][]>[
|
||||||
[
|
[
|
||||||
'retrieves token by id',
|
'retrieves token by id',
|
||||||
<TokenId>'tok123',
|
<TokenId>'tok123',
|
||||||
|
@ -150,10 +151,11 @@ describe( 'server.token.TokenDao', () =>
|
||||||
id: <TokenId>'tok123',
|
id: <TokenId>'tok123',
|
||||||
status: expected_status,
|
status: expected_status,
|
||||||
},
|
},
|
||||||
|
null,
|
||||||
],
|
],
|
||||||
|
|
||||||
[
|
[
|
||||||
'returns null for namespace if token is not found',
|
'rejects for namespace if token is not found',
|
||||||
<TokenId>'tok123',
|
<TokenId>'tok123',
|
||||||
{
|
{
|
||||||
[field]: {
|
[field]: {
|
||||||
|
@ -170,19 +172,21 @@ describe( 'server.token.TokenDao', () =>
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
null,
|
null,
|
||||||
|
`${ns}.tok123`,
|
||||||
],
|
],
|
||||||
|
|
||||||
[
|
[
|
||||||
'returns null for field if namespace is not found',
|
'rejects if namespace is not found',
|
||||||
<TokenId>'tok123',
|
<TokenId>'tok123',
|
||||||
{
|
{
|
||||||
[field]: {},
|
[field]: {},
|
||||||
},
|
},
|
||||||
null,
|
null,
|
||||||
|
ns,
|
||||||
],
|
],
|
||||||
|
|
||||||
[
|
[
|
||||||
'returns lastest modified token given no token id',
|
'returns last modified token given no token id',
|
||||||
<TokenId>'',
|
<TokenId>'',
|
||||||
{
|
{
|
||||||
[field]: {
|
[field]: {
|
||||||
|
@ -201,23 +205,55 @@ describe( 'server.token.TokenDao', () =>
|
||||||
id: <TokenId>'toklast',
|
id: <TokenId>'toklast',
|
||||||
status: expected_status,
|
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, () =>
|
it( label, () =>
|
||||||
{
|
{
|
||||||
const coll: MongoCollection = {
|
const coll: MongoCollection = {
|
||||||
findOne( _selector, _fields, callback )
|
findOne( _selector, _fields, callback )
|
||||||
{
|
{
|
||||||
callback( null, result );
|
callback( null, dbresult );
|
||||||
},
|
},
|
||||||
|
|
||||||
update() {},
|
update() {},
|
||||||
};
|
};
|
||||||
|
|
||||||
return expect(
|
const result = new Sut( coll, field, () => <UnixTimestamp>0 )
|
||||||
new Sut( coll, field, () => <UnixTimestamp>0 )
|
.getToken( did, ns, tok_id );
|
||||||
.getToken( did, ns, tok_id )
|
|
||||||
).to.eventually.deep.equal( expected );
|
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 );
|
||||||
} )
|
} )
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue