From fb88ceeae65418fcefeb75b4e49a1aeeaaea3998 Mon Sep 17 00:00:00 2001 From: Mike Gerwitz Date: Mon, 16 Sep 2019 13:38:13 -0400 Subject: [PATCH] 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'. --- src/server/daemon/controller.js | 6 +- src/server/service/TokenedService.js | 8 +- src/server/token/MongoTokenDao.ts | 269 ++++++++++++ src/server/token/TokenDao.ts | 382 +++++++----------- src/server/token/TokenQueryResult.ts | 138 ------- .../{TokenDaoTest.ts => MongoTokenDaoTest.ts} | 8 +- 6 files changed, 413 insertions(+), 398 deletions(-) create mode 100644 src/server/token/MongoTokenDao.ts delete mode 100644 src/server/token/TokenQueryResult.ts rename test/server/token/{TokenDaoTest.ts => MongoTokenDaoTest.ts} (98%) diff --git a/src/server/daemon/controller.js b/src/server/daemon/controller.js index 36dca21..18a2635 100644 --- a/src/server/daemon/controller.js +++ b/src/server/daemon/controller.js @@ -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' ); diff --git a/src/server/service/TokenedService.js b/src/server/service/TokenedService.js index 08a5e6d..ead005e 100644 --- a/src/server/service/TokenedService.js +++ b/src/server/service/TokenedService.js @@ -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' ); diff --git a/src/server/token/MongoTokenDao.ts b/src/server/token/MongoTokenDao.ts new file mode 100644 index 0000000..f77a436 --- /dev/null +++ b/src/server/token/MongoTokenDao.ts @@ -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 . + */ + +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 + { + 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 + { + 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 = data[ this._rootField ] + || {}; + + if ( !field[ ns ] ) + { + resolve( null ); + return; + } + + const ns_data = 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 = ns_data[ 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; + } +}; + diff --git a/src/server/token/TokenDao.ts b/src/server/token/TokenDao.ts index 75b7710..fcb06c2 100644 --- a/src/server/token/TokenDao.ts +++ b/src/server/token/TokenDao.ts @@ -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 . + * + * 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; + + + getToken( + doc_id: DocumentId, + ns: TokenNamespace, + token_id: TokenId + ): Promise; +} + + +/** + * 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 - { - 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 - { - 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 = data[ this._rootField ] - || {}; - - if ( !field[ ns ] ) - { - resolve( null ); - return; - } - - const ns_data = 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 = ns_data[ 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; - } -}; - diff --git a/src/server/token/TokenQueryResult.ts b/src/server/token/TokenQueryResult.ts deleted file mode 100644 index 5faeab4..0000000 --- a/src/server/token/TokenQueryResult.ts +++ /dev/null @@ -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 . - * - * 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, -} diff --git a/test/server/token/TokenDaoTest.ts b/test/server/token/MongoTokenDaoTest.ts similarity index 98% rename from test/server/token/TokenDaoTest.ts rename to test/server/token/MongoTokenDaoTest.ts index c992632..74f9c13 100644 --- a/test/server/token/TokenDaoTest.ts +++ b/test/server/token/MongoTokenDaoTest.ts @@ -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,