1
0
Fork 0

TokenDao: class=>interface

TokenDao has been renamed to MongoTokenDao.  While it's good for this to
have its own interface anyway, the immediate motivation was for unit tests:
I started playing with mocking with TypeScript and researching some
libraries, but I don't have time to research enough to commit to any of them
at the moment.  Interfaces remove the need to mock at all.

This also stops using `export default' in favor of just importing by
name.  Using defaults only saves us a few characters, and it makes for
awkward syntax in various cases (e.g. with multiple exports).  But I'm still
new to TS, so who knows if I'll be flip-flopping on this decision in the
future.  If we kept to our normal 1:1 file:definition convention, it
wouldn't cause problems, but based on the types I've had to define so far,
that'd cause way too much bloat and boilerplate.

* src/server/daemon/controller.js: No long import `default'.  Use
  `MongoTokenDao'.
* src/server/token/TokenedService.js: Stop checking type (since TS
  interfaces do not result in compiler output, easejs cannot validate
  against them.)
* src/server/token/MongoTokenDao.ts: Rename from `TokenDao.ts'.
* src/server/token/TokenDao.ts: Rename from `TokenQueryResult.ts'.
  (TokenDao): New interface.
* src/server/token/TokenQueryResult.ts: Rename to `TokenDao.ts'.
* test/server/token/MongoTokenDaoTest.ts: Rename from `TokenDaoTest.ts'.
master
Mike Gerwitz 2019-09-16 13:38:13 -04:00
parent 37f1b86ac1
commit fb88ceeae6
6 changed files with 413 additions and 398 deletions

View File

@ -90,8 +90,8 @@ const {
},
token: {
TokenDao: {
default: TokenDao,
MongoTokenDao: {
MongoTokenDao
},
},
@ -246,7 +246,7 @@ function _initExportService( db, callback )
ExportService
.use( TokenedService(
'c1import',
new TokenDao( collection, "exports", getUnixTimestamp ),
new MongoTokenDao( collection, "exports", getUnixTimestamp ),
function tokgen()
{
var shasum = crypto.createHash( 'sha1' );

View File

@ -21,8 +21,7 @@
var Trait = require( 'easejs' ).Trait,
Class = require( 'easejs' ).Class,
Service = require( './Service' ),
TokenDao = require( '../token/TokenDao' ).default;
Service = require( './Service' );
/**
@ -96,11 +95,6 @@ module.exports = Trait( 'TokenedService' )
*/
__mixin: function( namespace, dao, tokgen, capture_gen )
{
if ( !Class.isA( TokenDao, dao ) )
{
throw TypeError( 'Instance of TokenDao expected' );
}
if ( typeof tokgen !== 'function' )
{
throw TypeError( 'Token generator must be a function' );

View File

@ -0,0 +1,269 @@
/**
* Token state management
*
* 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/>.
*/
import {
TokenDao,
TokenData,
TokenEntry,
TokenNamespaceData,
TokenNamespaceResults,
TokenQueryResult,
TokenStatus,
TokenType,
} from "./TokenDao";
import { TokenId, TokenNamespace } from "./Token";
import { DocumentId } from "../../document/Document";
/**
* Manages token updates
*
* This uses MongoDB as the underlying database.
*/
export class MongoTokenDao implements TokenDao
{
/**
* Mongo database collection
*/
private readonly _collection: MongoCollection;
/**
* Field storing token data, relative to document root
*/
private readonly _rootField: string;
/**
* Retrieve a Unix timestamp
*
* This is used for timestampping token updates.
*/
private readonly _getTimestamp: () => UnixTimestamp;
/**
* Initialize connection
*
* @param collection Mongo collection
* @param root_field topmost field in mongo document
* @param date_ctor Date constructor
*/
constructor(
collection: MongoCollection,
root_field: string,
getTimestamp: () => UnixTimestamp,
)
{
this._collection = collection;
this._rootField = root_field;
this._getTimestamp = getTimestamp;
}
/**
* Create or update a token record
*
* The token entry is entered in the token log, and then the current
* entry is updated to reflect the changes. The operation is atomic.
*
* @param doc_id unique document identifier
* @param ns token namespace
* @param token token value
* @param data token data, if any
* @param status arbitrary token type
*/
updateToken(
doc_id: DocumentId,
ns: TokenNamespace,
token_id: TokenId,
type: TokenType,
data: string | null,
): Promise<void>
{
const root = this._genRoot( ns ) + '.';
const token_entry: TokenStatus = {
type: type,
timestamp: this._getTimestamp(),
data: data,
};
const token_data = {
[ root + 'last' ]: token_id,
[ root + 'lastStatus' ]: token_entry,
[ root + token_id + '.status' ]: token_entry,
};
const token_log = {
[ root + token_id + '.statusLog' ]: token_entry,
};
return new Promise( ( resolve, reject ) =>
{
this._collection.update(
{ id: +doc_id },
{
$set: token_data,
$push: token_log
},
{ upsert: true },
function ( err: Error|null )
{
if ( err )
{
reject( err );
return;
}
resolve();
}
);
} );
}
/**
* Retrieve existing token under the namespace NS, if any, for the doc
* identified by DOC_ID
*
* If a TOKEN_ID is provided, only that token will be queried; otherwise,
* the most recently created token will be the subject of the query.
*
* @param doc_id document identifier
* @param ns token namespace
* @param token_id token identifier (unique to NS)
*
* @return token data
*/
getToken( doc_id: DocumentId, ns: TokenNamespace, token_id: TokenId ):
Promise<TokenData|null>
{
const root = this._genRoot( ns ) + '.';
const fields: any = {};
fields[ root + 'last' ] = 1;
fields[ root + 'lastStatus' ] = 1;
if ( token_id )
{
// XXX: injectable
fields[ root + token_id ] = 1;
}
return new Promise( ( resolve, reject ) =>
{
this._collection.findOne(
{ id: +doc_id },
{ fields: fields },
( err: Error|null, data: TokenQueryResult ) =>
{
if ( err || !data )
{
reject( err );
return;
}
const field = <TokenNamespaceResults>data[ this._rootField ]
|| {};
if ( !field[ ns ] )
{
resolve( null );
return;
}
const ns_data = <TokenNamespaceData>field[ ns ];
resolve( ( token_id )
? this._getRequestedToken( token_id, ns_data )
: this._getLatestToken( ns_data )
);
}
);
} );
}
/**
* Retrieve latest token data, or `null` if none
*
* @param ns_data namespace data
*
* @return data of latest token in namespace
*/
private _getLatestToken( ns_data: TokenNamespaceData ): TokenData | null
{
var last = ns_data.last;
if ( !last )
{
return null;
}
return {
id: last,
status: ns_data.lastStatus,
};
}
/**
* Retrieve latest token data, or `null` if none
*
* @param token_id token identifier for namespace associated with NS_DATA
* @param ns_data namespace data
*
* @return data of requested token
*/
private _getRequestedToken(
token_id: TokenId,
ns_data: TokenNamespaceData
): TokenData | null
{
const reqtok = <TokenEntry>ns_data[ <string>token_id ];
if ( !reqtok )
{
return null;
}
return {
id: token_id,
status: reqtok.status,
};
}
/**
* Determine token root for the given namespace
*
* @param ns token namespace
*
* @return token root for namespace NS
*/
private _genRoot( ns: TokenNamespace ): string
{
// XXX: injectable
return this._rootField + '.' + ns;
}
};

View File

@ -1,5 +1,5 @@
/**
* Token state management
* Token data access
*
* Copyright (C) 2010-2019 R-T Specialty, LLC.
*
@ -17,21 +17,148 @@
*
* 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/>.
*
* These types are used to describe the structure of the token data as it
* is stored in Mongo. It has a number of undesirable properties and
* duplicates data---this was intended to make querying easier and work
* around Mongo limitations.
*
* This structure can be changed in the future, but we'll need to maintain
* compatibility with the existing data.
*/
import {
TokenEntry,
TokenNamespaceData,
TokenNamespaceResults,
TokenQueryResult,
TokenStatus,
TokenType,
} from "./TokenQueryResult";
import { TokenId, TokenNamespace } from "./Token";
import { DocumentId } from "../../document/Document";
/** Manage token updates */
export interface TokenDao
{
updateToken(
doc_id: DocumentId,
ns: TokenNamespace,
token_id: TokenId,
type: TokenType,
data: string | null,
): Promise<void>;
getToken(
doc_id: DocumentId,
ns: TokenNamespace,
token_id: TokenId
): Promise<TokenData|null>;
}
/**
* Token status types as stored in the database
*/
export type TokenType = 'ACTIVE' | 'DONE' | 'ACCEPTED' | 'DEAD';
/**
* Result of a Mongo query
*
* The returned property depends on the actual query.
*/
export interface TokenQueryResult
{
readonly [propName: string]: TokenNamespaceResults | null,
}
/**
* Token data for requested namespaces
*/
export interface TokenNamespaceResults
{
readonly [propName: string]: TokenNamespaceData | null,
}
/**
* Token data associated with the given namespace
*
* This contains duplicate information in order to work around inconvenient
* limitations in [earlier] versions of Mongo.
*/
export interface TokenNamespaceData
{
/**
* Identifier of last token touched in this namespace
*/
readonly last: TokenId,
/**
* Most recent token status
*
* This is a duplicate of the last entry in `TokenEntry#statusLog`.
*/
readonly lastStatus: TokenStatus,
/**
* Tokens indexed by identifier
*
* These data are inconveniently placed---the type definition here is to
* accommodate the above fields. Anything using this should cast to
* `TokenEntry`.
*/
readonly [propName: string]: TokenEntry | TokenStatus | TokenId | null,
}
/**
* Information about a given token
*/
export interface TokenEntry
{
/**
* Current token status
*
* This is a duplicate of the last element of `statusLog`.
*/
readonly status: TokenStatus,
/**
* Log of all past status changes and any associated data
*
* This is pushed to on each status change. The last element is
* duplicated in `status`.
*/
readonly statusLog: TokenStatus[],
}
/**
* Status of the token (past or present)
*
* A status is a `TokenType`, along with a timestamp of occurrence and
* optional data.
*/
export interface TokenStatus
{
/**
* State of the token
*/
readonly type: TokenType,
/**
* Unix timestamp representing when the status change occurred
*/
readonly timestamp: UnixTimestamp,
/**
* Arbitrary data associated with the status change
*
* For example, a token of status `DONE` may be associated with the
* fulfillment of a request, in which case this may contain the response
* data.
*/
readonly data: string | null,
}
/**
* Token information
*/
@ -40,238 +167,3 @@ export interface TokenData
id: TokenId,
status: TokenStatus,
}
/**
* Manages token updates
*
* Note that this is tightly coupled with MongoDB.
*/
export default class TokenDao
{
/**
* Mongo database collection
*/
private readonly _collection: MongoCollection;
/**
* Field storing token data, relative to document root
*/
private readonly _rootField: string;
/**
* Retrieve a Unix timestamp
*
* This is used for timestampping token updates.
*/
private readonly _getTimestamp: () => UnixTimestamp;
/**
* Initialize connection
*
* @param collection Mongo collection
* @param root_field topmost field in mongo document
* @param date_ctor Date constructor
*/
constructor(
collection: MongoCollection,
root_field: string,
getTimestamp: () => UnixTimestamp,
)
{
this._collection = collection;
this._rootField = root_field;
this._getTimestamp = getTimestamp;
}
/**
* Create or update a token record
*
* The token entry is entered in the token log, and then the current
* entry is updated to reflect the changes. The operation is atomic.
*
* @param doc_id unique document identifier
* @param ns token namespace
* @param token token value
* @param data token data, if any
* @param status arbitrary token type
*/
updateToken(
doc_id: DocumentId,
ns: TokenNamespace,
token_id: TokenId,
type: TokenType,
data: string | null,
): Promise<void>
{
const root = this._genRoot( ns ) + '.';
const token_entry: TokenStatus = {
type: type,
timestamp: this._getTimestamp(),
data: data,
};
const token_data = {
[ root + 'last' ]: token_id,
[ root + 'lastStatus' ]: token_entry,
[ root + token_id + '.status' ]: token_entry,
};
const token_log = {
[ root + token_id + '.statusLog' ]: token_entry,
};
return new Promise( ( resolve, reject ) =>
{
this._collection.update(
{ id: +doc_id },
{
$set: token_data,
$push: token_log
},
{ upsert: true },
function ( err: Error|null )
{
if ( err )
{
reject( err );
return;
}
resolve();
}
);
} );
}
/**
* Retrieve existing token under the namespace NS, if any, for the doc
* identified by DOC_ID
*
* If a TOKEN_ID is provided, only that token will be queried; otherwise,
* the most recently created token will be the subject of the query.
*
* @param doc_id document identifier
* @param ns token namespace
* @param token_id token identifier (unique to NS)
*
* @return token data
*/
getToken( doc_id: DocumentId, ns: TokenNamespace, token_id: TokenId ):
Promise<TokenData|null>
{
const root = this._genRoot( ns ) + '.';
const fields: any = {};
fields[ root + 'last' ] = 1;
fields[ root + 'lastStatus' ] = 1;
if ( token_id )
{
// XXX: injectable
fields[ root + token_id ] = 1;
}
return new Promise( ( resolve, reject ) =>
{
this._collection.findOne(
{ id: +doc_id },
{ fields: fields },
( err: Error|null, data: TokenQueryResult ) =>
{
if ( err || !data )
{
reject( err );
return;
}
const field = <TokenNamespaceResults>data[ this._rootField ]
|| {};
if ( !field[ ns ] )
{
resolve( null );
return;
}
const ns_data = <TokenNamespaceData>field[ ns ];
resolve( ( token_id )
? this._getRequestedToken( token_id, ns_data )
: this._getLatestToken( ns_data )
);
}
);
} );
}
/**
* Retrieve latest token data, or `null` if none
*
* @param ns_data namespace data
*
* @return data of latest token in namespace
*/
private _getLatestToken( ns_data: TokenNamespaceData ): TokenData | null
{
var last = ns_data.last;
if ( !last )
{
return null;
}
return {
id: last,
status: ns_data.lastStatus,
};
}
/**
* Retrieve latest token data, or `null` if none
*
* @param token_id token identifier for namespace associated with NS_DATA
* @param ns_data namespace data
*
* @return data of requested token
*/
private _getRequestedToken(
token_id: TokenId,
ns_data: TokenNamespaceData
): TokenData | null
{
const reqtok = <TokenEntry>ns_data[ <string>token_id ];
if ( !reqtok )
{
return null;
}
return {
id: token_id,
status: reqtok.status,
};
}
/**
* Determine token root for the given namespace
*
* @param ns token namespace
*
* @return token root for namespace NS
*/
private _genRoot( ns: TokenNamespace ): string
{
// XXX: injectable
return this._rootField + '.' + ns;
}
};

View File

@ -1,138 +0,0 @@
/**
* Result of querying for a token from Mongo
*
* 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/>.
*
* These types are used to describe the structure of the token data as it
* is stored in Mongo. It has a number of undesirable properties and
* duplicates data---this was intended to make querying easier and work
* around Mongo limitations.
*
* This structure can be changed in the future, but we'll need to maintain
* compatibility with the existing data.
*/
import { TokenId } from "./Token";
/**
* Token status types as stored in the database
*/
export type TokenType = 'ACTIVE' | 'DONE' | 'ACCEPTED' | 'DEAD';
/**
* Result of a Mongo query
*
* The returned property depends on the actual query.
*/
export interface TokenQueryResult
{
readonly [propName: string]: TokenNamespaceResults | null,
}
/**
* Token data for requested namespaces
*/
export interface TokenNamespaceResults
{
readonly [propName: string]: TokenNamespaceData | null,
}
/**
* Token data associated with the given namespace
*
* This contains duplicate information in order to work around inconvenient
* limitations in [earlier] versions of Mongo.
*/
export interface TokenNamespaceData
{
/**
* Identifier of last token touched in this namespace
*/
readonly last: TokenId,
/**
* Most recent token status
*
* This is a duplicate of the last entry in `TokenEntry#statusLog`.
*/
readonly lastStatus: TokenStatus,
/**
* Tokens indexed by identifier
*
* These data are inconveniently placed---the type definition here is to
* accommodate the above fields. Anything using this should cast to
* `TokenEntry`.
*/
readonly [propName: string]: TokenEntry | TokenStatus | TokenId | null,
}
/**
* Information about a given token
*/
export interface TokenEntry
{
/**
* Current token status
*
* This is a duplicate of the last element of `statusLog`.
*/
readonly status: TokenStatus,
/**
* Log of all past status changes and any associated data
*
* This is pushed to on each status change. The last element is
* duplicated in `status`.
*/
readonly statusLog: TokenStatus[],
}
/**
* Status of the token (past or present)
*
* A status is a `TokenType`, along with a timestamp of occurrence and
* optional data.
*/
export interface TokenStatus
{
/**
* State of the token
*/
readonly type: TokenType,
/**
* Unix timestamp representing when the status change occurred
*/
readonly timestamp: UnixTimestamp,
/**
* Arbitrary data associated with the status change
*
* For example, a token of status `DONE` may be associated with the
* fulfillment of a request, in which case this may contain the response
* data.
*/
readonly data: string | null,
}

View File

@ -20,15 +20,13 @@
*/
import {
TokenData,
TokenQueryResult,
TokenStatus,
} from "../../../src/server/token/TokenQueryResult";
import {
default as Sut,
TokenData,
} from "../../../src/server/token/TokenDao";
import { MongoTokenDao as Sut } from "../../../src/server/token/MongoTokenDao";
import {
TokenId,
TokenNamespace,