diff --git a/package-lock.json b/package-lock.json index 20807a1fc..46db15a96 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4399,9 +4399,9 @@ } }, "highlight.js": { - "version": "9.13.0", - "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-9.13.0.tgz", - "integrity": "sha512-2B90kcNnErqRTmzdZw6IPLEC9CdsiIMhj+r8L3LJKRCgtEJ+LY5yzWuQCVnADTI0wwocQinFzaaL/JjTQNqI/g==", + "version": "9.13.1", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-9.13.1.tgz", + "integrity": "sha512-Sc28JNQNDzaH6PORtRLMvif9RSn1mYuOoX3omVjnb0+HbpPygU2ALBI0R/wsiqCb4/fcp07Gdo8g+fhtFrQl6A==", "dev": true }, "hmac-drbg": { @@ -11737,9 +11737,9 @@ "dev": true }, "progress": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.0.tgz", - "integrity": "sha1-ihvjZr+Pwj2yvSPxDG/pILQ4nR8=", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.1.tgz", + "integrity": "sha512-OE+a6vzqazc+K6LxJrX5UPyKFvGnL5CYmq2jFGNIBWHpc4QyE49/YOumcrpQFJpfejmvRtbJzgO1zPmMCqlbBg==", "dev": true }, "prompt": { @@ -11754,6 +11754,43 @@ "revalidator": "0.1.x", "utile": "0.3.x", "winston": "2.1.x" + }, + "dependencies": { + "async": { + "version": "1.0.0", + "resolved": "http://registry.npmjs.org/async/-/async-1.0.0.tgz", + "integrity": "sha1-+PwEyjoTeErenhZBr5hXjPvWR6k=", + "dev": true + }, + "winston": { + "version": "2.1.1", + "resolved": "http://registry.npmjs.org/winston/-/winston-2.1.1.tgz", + "integrity": "sha1-PJNJ0ZYgf9G9/51LxD73JRDjoS4=", + "dev": true, + "requires": { + "async": "~1.0.0", + "colors": "1.0.x", + "cycle": "1.0.x", + "eyes": "0.1.x", + "isstream": "0.1.x", + "pkginfo": "0.3.x", + "stack-trace": "0.0.x" + }, + "dependencies": { + "colors": { + "version": "1.0.3", + "resolved": "http://registry.npmjs.org/colors/-/colors-1.0.3.tgz", + "integrity": "sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs=", + "dev": true + }, + "pkginfo": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/pkginfo/-/pkginfo-0.3.1.tgz", + "integrity": "sha1-Wyn2qB9wcXFC4J52W76rl7T4HiE=", + "dev": true + } + } + } } }, "prompts": { @@ -12011,7 +12048,8 @@ "reflect-metadata": { "version": "0.1.12", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.12.tgz", - "integrity": "sha512-n+IyV+nGz3+0q3/Yf1ra12KpCyi001bi4XFxSjbiWWjfqb52iTTtpGXmCCAOWWIAn9KEuFZKGqBERHmrtScZ3A==" + "integrity": "sha512-n+IyV+nGz3+0q3/Yf1ra12KpCyi001bi4XFxSjbiWWjfqb52iTTtpGXmCCAOWWIAn9KEuFZKGqBERHmrtScZ3A==", + "dev": true }, "regenerator-runtime": { "version": "0.11.0", @@ -14682,9 +14720,9 @@ "dev": true }, "typedoc": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.12.0.tgz", - "integrity": "sha512-dsdlaYZ7Je8JC+jQ3j2Iroe4uyD0GhqzADNUVyBRgLuytQDP/g0dPkAw5PdM/4drnmmJjRzSWW97FkKo+ITqQg==", + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.13.0.tgz", + "integrity": "sha512-jQWtvPcV+0fiLZAXFEe70v5gqjDO6pJYJz4mlTtmGJeW2KRoIU/BEfktma6Uj8Xii7UakuZjbxFewl3UYOkU/w==", "dev": true, "requires": { "@types/fs-extra": "^5.0.3", @@ -14703,13 +14741,13 @@ "progress": "^2.0.0", "shelljs": "^0.8.2", "typedoc-default-themes": "^0.5.0", - "typescript": "3.0.x" + "typescript": "3.1.x" }, "dependencies": { "shelljs": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.2.tgz", - "integrity": "sha512-pRXeNrCA2Wd9itwhvLp5LZQvPJ0wU6bcjaTMywHHGX5XWhVN2nzSu7WV0q+oUY7mGK3mgSkDDzP3MgjqdyIgbQ==", + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.3.tgz", + "integrity": "sha512-fc0BKlAWiLpwZljmOvAOTE/gXawtCoNrP5oaY7KIaQbbyHeQVg01pSEuEGvGh3HEdBU4baCD7wQBwADmM/7f7A==", "dev": true, "requires": { "glob": "^7.0.0", @@ -14718,9 +14756,9 @@ } }, "typescript": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.0.3.tgz", - "integrity": "sha512-kk80vLW9iGtjMnIv11qyxLqZm20UklzuR2tL0QAnDIygIUIemcZMxlMWudl9OOt76H3ntVzcTiddQ1/pAAJMYg==", + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.1.6.tgz", + "integrity": "sha512-tDMYfVtvpb96msS1lDX9MEdHrW4yOuZ4Kdc4Him9oU796XldPYF/t2+uKoX0BBa0hXXwDlqYQbXY5Rzjzc5hBA==", "dev": true } } @@ -15076,41 +15114,6 @@ "dev": true, "optional": true }, - "winston": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/winston/-/winston-2.1.1.tgz", - "integrity": "sha1-PJNJ0ZYgf9G9/51LxD73JRDjoS4=", - "dev": true, - "requires": { - "async": "~1.0.0", - "colors": "1.0.x", - "cycle": "1.0.x", - "eyes": "0.1.x", - "isstream": "0.1.x", - "pkginfo": "0.3.x", - "stack-trace": "0.0.x" - }, - "dependencies": { - "async": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/async/-/async-1.0.0.tgz", - "integrity": "sha1-+PwEyjoTeErenhZBr5hXjPvWR6k=", - "dev": true - }, - "colors": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz", - "integrity": "sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs=", - "dev": true - }, - "pkginfo": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/pkginfo/-/pkginfo-0.3.1.tgz", - "integrity": "sha1-Wyn2qB9wcXFC4J52W76rl7T4HiE=", - "dev": true - } - } - }, "word-wrap": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", diff --git a/package.json b/package.json index 7b8076bdf..97c5d202a 100644 --- a/package.json +++ b/package.json @@ -123,6 +123,5 @@ "rxjs": "^6.0.0", "reflect-metadata": "^0.1.12" }, - "dependencies": { - } + "dependencies": {} } diff --git a/src/config/config.ts b/src/config/config.ts index 284fb113b..9b103953e 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -1,6 +1,7 @@ +import { LogReceiver } from '../logger/log-receiver.type' import { DateTypes } from './date-types.type' export interface Config { dateType: DateTypes - debug: boolean + logReceiver: LogReceiver } diff --git a/src/config/dynamo-easy-config.ts b/src/config/dynamo-easy-config.ts index 094054444..da9bd8465 100644 --- a/src/config/dynamo-easy-config.ts +++ b/src/config/dynamo-easy-config.ts @@ -1,9 +1,15 @@ import { Config } from './config' export class DynamoEasyConfig { - static config: Config = { dateType: 'moment', debug: true } + static config: Config = { + dateType: 'moment', + logReceiver: () => {}, + } - static updateConfig(config: Config): void { + static updateConfig(config: Partial): void { + if (config.logReceiver !== undefined && typeof config.logReceiver !== 'function') { + throw new Error('Config.logReceiver has to be a function') + } Object.assign(DynamoEasyConfig.config, config) } diff --git a/src/dynamo-easy.ts b/src/dynamo-easy.ts index 7afd467b9..c69ec8101 100644 --- a/src/dynamo-easy.ts +++ b/src/dynamo-easy.ts @@ -11,5 +11,6 @@ import 'reflect-metadata' export * from './config' export * from './decorator' export * from './dynamo' +export * from './logger' export * from './mapper' export * from './model' diff --git a/src/dynamo/dynamo-rx.ts b/src/dynamo/dynamo-rx.ts index c4ca7e037..d84ef6e8f 100644 --- a/src/dynamo/dynamo-rx.ts +++ b/src/dynamo/dynamo-rx.ts @@ -62,7 +62,7 @@ export class DynamoRx { return this.sessionValidityEnsurer().pipe(switchMap(() => from(this.dynamoDb.query(params).promise()))) } - makeRequest(operation: string, params?: { [key: string]: any }): any { + makeRequest(operation: string, params?: { [key: string]: any }): Observable { return this.sessionValidityEnsurer().pipe( switchMap(() => from(this.dynamoDb.makeRequest(operation, params).promise())) ) diff --git a/src/dynamo/dynamo-store.ts b/src/dynamo/dynamo-store.ts index 6eee1677a..454dc4ac4 100644 --- a/src/dynamo/dynamo-store.ts +++ b/src/dynamo/dynamo-store.ts @@ -1,6 +1,8 @@ import * as DynamoDB from 'aws-sdk/clients/dynamodb' import { Observable } from 'rxjs' +import { tap } from 'rxjs/operators' import { MetadataHelper } from '../decorator/metadata/metadata-helper' +import { createLogger, Logger } from '../logger/logger' import { ModelConstructor } from '../model/model-constructor' import { DEFAULT_SESSION_VALIDITY_ENSURER } from './default-session-validity-ensurer.const' import { DEFAULT_TABLE_NAME_RESOLVER } from './default-table-name-resolver.const' @@ -19,6 +21,7 @@ import { SessionValidityEnsurer } from './session-validity-ensurer.type' import { TableNameResolver } from './table-name-resolver.type' export class DynamoStore { + private readonly logger: Logger private readonly dynamoRx: DynamoRx readonly tableName: string @@ -28,6 +31,7 @@ export class DynamoStore { tableNameResolver: TableNameResolver = DEFAULT_TABLE_NAME_RESOLVER, sessionValidityEnsurer: SessionValidityEnsurer = DEFAULT_SESSION_VALIDITY_ENSURER ) { + this.logger = createLogger('dynamo.DynamoStore', modelClazz) this.dynamoRx = new DynamoRx(sessionValidityEnsurer) const tableName = tableNameResolver(MetadataHelper.get(this.modelClazz).modelOptions.tableName) if (!REGEX_TABLE_NAME.test(tableName)) { @@ -37,6 +41,7 @@ export class DynamoStore { } this.tableName = tableName + this.logger.debug('instance created') } get dynamoDb(): DynamoDB { @@ -86,7 +91,8 @@ export class DynamoStore { } makeRequest(operation: DynamoApiOperations, params?: { [key: string]: any }): Observable { - return this.dynamoRx.makeRequest(operation, params) + this.logger.debug('request', params) + return this.dynamoRx.makeRequest(operation, params).pipe(tap(response => this.logger.debug('response', response))) } // Commented because not used at the moment diff --git a/src/dynamo/request/batchgetsingletable/batch-get-single-table.request.ts b/src/dynamo/request/batchgetsingletable/batch-get-single-table.request.ts index 34befa027..2e50a3eaf 100644 --- a/src/dynamo/request/batchgetsingletable/batch-get-single-table.request.ts +++ b/src/dynamo/request/batchgetsingletable/batch-get-single-table.request.ts @@ -1,9 +1,10 @@ import { BatchGetItemInput } from 'aws-sdk/clients/dynamodb' import { isObject } from 'lodash' import { Observable } from 'rxjs' -import { map } from 'rxjs/operators' +import { map, tap } from 'rxjs/operators' import { Metadata } from '../../../decorator/metadata/metadata' import { MetadataHelper } from '../../../decorator/metadata/metadata-helper' +import { createLogger, Logger } from '../../../logger/logger' import { Mapper } from '../../../mapper/mapper' import { Attributes } from '../../../mapper/type/attribute.type' import { ModelConstructor } from '../../../model/model-constructor' @@ -12,6 +13,7 @@ import { BatchGetSingleTableResponse } from './batch-get-single-table.response' // TODO add support for indexes export class BatchGetSingleTableRequest { + private readonly logger: Logger readonly dynamoRx: DynamoRx readonly params: BatchGetItemInput readonly modelClazz: ModelConstructor @@ -20,6 +22,7 @@ export class BatchGetSingleTableRequest { private _metadata: Metadata constructor(dynamoRx: DynamoRx, modelClazz: ModelConstructor, tableName: string, keys: any[]) { + this.logger = createLogger('dynamo.request.BatchGetSingleTableRequest', modelClazz) this.dynamoRx = dynamoRx if (modelClazz === null || modelClazz === undefined) { @@ -45,7 +48,9 @@ export class BatchGetSingleTableRequest { } execFullResponse(): Observable> { + this.logger.debug('request', this.params) return this.dynamoRx.batchGetItems(this.params).pipe( + tap(response => this.logger.debug('response', response)), map(response => { let items: T[] if (response.Responses && Object.keys(response.Responses).length && response.Responses[this.tableName]) { @@ -62,12 +67,15 @@ export class BatchGetSingleTableRequest { UnprocessedKeys: response.UnprocessedKeys, ConsumedCapacity: response.ConsumedCapacity, } - }) + }), + tap(response => this.logger.debug('mapped items', response.Items)) ) } exec(): Observable { + this.logger.debug('request', this.params) return this.dynamoRx.batchGetItems(this.params).pipe( + tap(response => this.logger.debug('response', response)), map(response => { if (response.Responses && Object.keys(response.Responses).length && response.Responses[this.tableName]) { return response.Responses![this.tableName].map(attributeMap => @@ -76,7 +84,8 @@ export class BatchGetSingleTableRequest { } else { return [] } - }) + }), + tap(items => this.logger.debug('mapped items', items)) ) } diff --git a/src/dynamo/request/batchwritesingletable/batch-write-single-table.request.ts b/src/dynamo/request/batchwritesingletable/batch-write-single-table.request.ts index ca0899a44..760e8baa3 100644 --- a/src/dynamo/request/batchwritesingletable/batch-write-single-table.request.ts +++ b/src/dynamo/request/batchwritesingletable/batch-write-single-table.request.ts @@ -3,6 +3,7 @@ import { Observable, of } from 'rxjs' import { delay, map, mergeMap, tap } from 'rxjs/operators' import { DynamoRx } from '../../../dynamo/dynamo-rx' import { randomExponentialBackoffTimer } from '../../../helper' +import { createLogger, Logger } from '../../../logger/logger' import { Mapper } from '../../../mapper' import { Attributes } from '../../../mapper/type/attribute.type' import { ModelConstructor } from '../../../model/model-constructor' @@ -11,6 +12,8 @@ import { BatchWriteSingleTableResponse } from './batch-write-single-table.respon const MAX_BATCH_WRITE_ITEMS = 25 export class BatchWriteSingleTableRequest { + private readonly logger: Logger + private get toKey(): (item: T) => Attributes { if (!this._keyFn) { this._keyFn = Mapper.createToKeyFn(this.modelClazz) @@ -26,6 +29,7 @@ export class BatchWriteSingleTableRequest { private _keyFn: any constructor(dynamoRx: DynamoRx, modelClazz: ModelConstructor, tableName: string) { + this.logger = createLogger('dynamo.request.BatchWriteSingleTableRequest', modelClazz) this.dynamoRx = dynamoRx if (modelClazz === null || modelClazz === undefined) { @@ -39,6 +43,7 @@ export class BatchWriteSingleTableRequest { delete(items: T[]): BatchWriteSingleTableRequest { this.itemsToProcess.push(...items.map(item => ({ DeleteRequest: { Key: this.toKey(item) } }))) + this.logger.debug(`${items.length} items added for DeleteRequest`) return this } @@ -46,6 +51,7 @@ export class BatchWriteSingleTableRequest { this.itemsToProcess.push( ...items.map(item => ({ PutRequest: { Item: Mapper.toDb(item, this.modelClazz) } })) ) + this.logger.debug(`${items.length} items added for PutRequest`) return this } @@ -56,8 +62,10 @@ export class BatchWriteSingleTableRequest { [this.tableName]: batch, }, } + this.logger.debug('request', batchWriteItemInput) return this.dynamoRx.batchWriteItem(batchWriteItemInput).pipe( + tap(response => this.logger.debug('response', response)), tap((batchWriteManyResponse: BatchWriteItemOutput) => { if (batchWriteManyResponse.UnprocessedItems && batchWriteManyResponse.UnprocessedItems[this.tableName]) { this.itemsToProcess.unshift(...batchWriteManyResponse.UnprocessedItems[this.tableName]) @@ -69,7 +77,12 @@ export class BatchWriteSingleTableRequest { batchWriteManyResponse.UnprocessedItems && batchWriteManyResponse.UnprocessedItems[this.tableName] ), consumedCapacity: batchWriteManyResponse.ConsumedCapacity, - })) + })), + tap(response => { + if (response.capacityExceeded) { + this.logger.info('capacity exceeded', response.consumedCapacity) + } + }) ) } @@ -79,6 +92,7 @@ export class BatchWriteSingleTableRequest { * @param throttleTimeSlot defines how long one timeSlot is for throttling, default 1 second */ exec(backoffTimer = randomExponentialBackoffTimer, throttleTimeSlot = 1000): Observable { + this.logger.debug('starting batchWriteItem') let rBoT = backoffTimer() let backoffTime = 0 return this.execNextBatch().pipe( @@ -87,9 +101,10 @@ export class BatchWriteSingleTableRequest { rBoT = backoffTimer() backoffTime = 0 } else { - backoffTime = rBoT.next().value + backoffTime = rBoT.next().value * throttleTimeSlot + this.logger.info(`wait ${backoffTime} ms until next request`, { backoffTime }) } - return of(r).pipe(delay(backoffTime * throttleTimeSlot)) + return of(r).pipe(delay(backoffTime)) }), mergeMap((r: BatchWriteSingleTableResponse) => { if (r.remainingItems > 0) { diff --git a/src/dynamo/request/delete/delete.request.ts b/src/dynamo/request/delete/delete.request.ts index a419f3d4b..cec72c368 100644 --- a/src/dynamo/request/delete/delete.request.ts +++ b/src/dynamo/request/delete/delete.request.ts @@ -5,7 +5,8 @@ import { ReturnItemCollectionMetrics, } from 'aws-sdk/clients/dynamodb' import { Observable } from 'rxjs' -import { map } from 'rxjs/operators' +import { map, tap } from 'rxjs/operators' +import { createLogger, Logger } from '../../../logger/logger' import { Mapper } from '../../../mapper/mapper' import { Attributes } from '../../../mapper/type/attribute.type' import { ModelConstructor } from '../../../model/model-constructor' @@ -18,6 +19,8 @@ import { RequestConditionFunction } from '../../expression/type/request-conditio import { BaseRequest } from '../base.request' export class DeleteRequest extends BaseRequest { + private readonly logger: Logger + constructor( dynamoRx: DynamoRx, modelClazz: ModelConstructor, @@ -26,6 +29,7 @@ export class DeleteRequest extends BaseRequest { sortKey?: any ) { super(dynamoRx, modelClazz, tableName) + this.logger = createLogger('dynamo.request.DeleteRequest', modelClazz) const hasSortKey: boolean = this.metaData.getSortKey() !== null @@ -79,20 +83,21 @@ export class DeleteRequest extends BaseRequest { } /* - * The ReturnValues parameter is used by several DynamoDB operations; however, - * DeleteItem does not recognize any values other than NONE or ALL_OLD. - */ + * The ReturnValues parameter is used by several DynamoDB operations; however, + * DeleteItem does not recognize any values other than NONE or ALL_OLD. + */ returnValues(returnValues: 'NONE' | 'ALL_OLD'): DeleteRequest { this.params.ReturnValues = returnValues return this } execFullResponse(): Observable { - return this.dynamoRx.deleteItem(this.params) + this.logger.debug('request', this.params) + return this.dynamoRx.deleteItem(this.params).pipe(tap(response => this.logger.debug('response', response))) } exec(): Observable { - return this.dynamoRx.deleteItem(this.params).pipe( + return this.execFullResponse().pipe( map(response => { return }) diff --git a/src/dynamo/request/get/get.request.ts b/src/dynamo/request/get/get.request.ts index ab828e59b..59375153f 100644 --- a/src/dynamo/request/get/get.request.ts +++ b/src/dynamo/request/get/get.request.ts @@ -1,7 +1,8 @@ import { ReturnConsumedCapacity } from 'aws-sdk/clients/dynamodb' import { values as objValues } from 'lodash' import { Observable } from 'rxjs' -import { map } from 'rxjs/operators' +import { map, tap } from 'rxjs/operators' +import { createLogger, Logger } from '../../../logger/logger' import { Mapper } from '../../../mapper/mapper' import { Attributes } from '../../../mapper/type/attribute.type' import { ModelConstructor } from '../../../model/model-constructor' @@ -11,6 +12,8 @@ import { BaseRequest } from '../base.request' import { GetResponse } from './get.response' export class GetRequest extends BaseRequest { + private readonly logger: Logger + constructor( dynamoRx: DynamoRx, modelClazz: ModelConstructor, @@ -19,6 +22,7 @@ export class GetRequest extends BaseRequest { sortKey?: any ) { super(dynamoRx, modelClazz, tableName) + this.logger = createLogger('dynamo.request.GetRequest', modelClazz) const hasSortKey: boolean = this.metaData.getSortKey() !== null @@ -71,7 +75,9 @@ export class GetRequest extends BaseRequest { } execFullResponse(): Observable> { + this.logger.debug('request', this.params) return this.dynamoRx.getItem(this.params).pipe( + tap(response => this.logger.debug('response', response)), map(getItemResponse => { const response: GetResponse = { ...getItemResponse } @@ -82,19 +88,23 @@ export class GetRequest extends BaseRequest { } return response - }) + }), + tap(response => this.logger.debug('mapped item', response.Item)) ) } exec(): Observable { + this.logger.debug('request', this.params) return this.dynamoRx.getItem(this.params).pipe( + tap(response => this.logger.debug('response', response)), map(response => { if (response.Item) { return Mapper.fromDb(response.Item, this.modelClazz) } else { return null } - }) + }), + tap(item => this.logger.debug('mapped item', item)) ) } } diff --git a/src/dynamo/request/put/put.request.ts b/src/dynamo/request/put/put.request.ts index 7472ccd30..5f0cbbabb 100644 --- a/src/dynamo/request/put/put.request.ts +++ b/src/dynamo/request/put/put.request.ts @@ -1,6 +1,7 @@ import { PutItemOutput, ReturnConsumedCapacity, ReturnItemCollectionMetrics } from 'aws-sdk/clients/dynamodb' import { Observable } from 'rxjs' -import { map } from 'rxjs/operators' +import { map, tap } from 'rxjs/operators' +import { createLogger, Logger } from '../../../logger/logger' import { Mapper } from '../../../mapper/mapper' import { ModelConstructor } from '../../../model/model-constructor' import { DynamoRx } from '../../dynamo-rx' @@ -13,8 +14,11 @@ import { RequestConditionFunction } from '../../expression/type/request-conditio import { BaseRequest } from '../base.request' export class PutRequest extends BaseRequest { + private readonly logger: Logger + constructor(dynamoRx: DynamoRx, modelClazz: ModelConstructor, tableName: string, item: T) { super(dynamoRx, modelClazz, tableName) + this.logger = createLogger('dynamo.request.PutRequest', modelClazz) this.params.Item = Mapper.toDb(item, this.modelClazz) } @@ -69,11 +73,12 @@ export class PutRequest extends BaseRequest { } execFullResponse(): Observable { - return this.dynamoRx.putItem(this.params) + this.logger.debug('request', this.params) + return this.dynamoRx.putItem(this.params).pipe(tap(response => this.logger.debug('response', response))) } exec(): Observable { - return this.dynamoRx.putItem(this.params).pipe( + return this.execFullResponse().pipe( map(response => { return }) diff --git a/src/dynamo/request/query/query.request.ts b/src/dynamo/request/query/query.request.ts index 033fb735b..d668e1ef3 100644 --- a/src/dynamo/request/query/query.request.ts +++ b/src/dynamo/request/query/query.request.ts @@ -1,7 +1,8 @@ import { QueryInput, QueryOutput } from 'aws-sdk/clients/dynamodb' import { Observable } from 'rxjs' -import { map } from 'rxjs/operators' +import { map, tap } from 'rxjs/operators' import { fetchAll } from '../../../helper' +import { createLogger, Logger } from '../../../logger/logger' import { Mapper } from '../../../mapper/mapper' import { Attributes } from '../../../mapper/type/attribute.type' import { ModelConstructor } from '../../../model/model-constructor' @@ -18,8 +19,11 @@ import { QueryResponse } from './query.response' export class QueryRequest extends Request, QueryInput, QueryResponse> implements Pageable, QueryResponse> { + private readonly logger: Logger + constructor(dynamoRx: DynamoRx, modelClazz: ModelConstructor, tableName: string) { super(dynamoRx, modelClazz, tableName) + this.logger = createLogger('dynamo.request.QueryRequest', modelClazz) } wherePartitionKey(partitionKeyValue: any): QueryRequest { @@ -97,41 +101,55 @@ export class QueryRequest extends Request, QueryInput, Que const params = { ...this.params } params.Select = 'COUNT' - return this.dynamoRx.query(params).pipe(map(response => response.Count!)) + this.logger.debug('count request', params) + return this.dynamoRx.query(params).pipe( + tap(response => this.logger.debug('response', response)), + map(response => response.Count!), + tap(count => this.logger.debug('count', count)) + ) } execFullResponse(): Observable> { + this.logger.debug('request', this.params) return this.dynamoRx.query(this.params).pipe( + tap(response => this.logger.debug('response', response)), map(queryResponse => { const response: QueryResponse = { ...queryResponse } response.Items = queryResponse.Items!.map(item => Mapper.fromDb(item, this.modelClazz)) return response - }) + }), + tap(response => this.logger.debug('mapped items', response.Items)) ) } exec(): Observable { - return this.dynamoRx - .query(this.params) - .pipe(map(response => response.Items!.map(item => Mapper.fromDb(item, this.modelClazz)))) + this.logger.debug('request', this.params) + return this.dynamoRx.query(this.params).pipe( + tap(response => this.logger.debug('response', response)), + map(response => response.Items!.map(item => Mapper.fromDb(item, this.modelClazz))), + tap(items => this.logger.debug('mapped items', items)) + ) } execNoMap(): Observable { - return this.dynamoRx.query(this.params) + this.logger.debug('request (noMap)', this.params) + return this.dynamoRx.query(this.params).pipe(tap(response => this.logger.debug('response', response))) } execSingle(): Observable { this.limit(1) - + this.logger.debug('single request', this.params) return this.dynamoRx.query(this.params).pipe( + tap(response => this.logger.debug('response', response)), map(response => { if (response.Count) { return Mapper.fromDb(response.Items![0], this.modelClazz) } else { return null } - }) + }), + tap(item => this.logger.debug('mapped item', item)) ) } diff --git a/src/dynamo/request/scan/scan.request.ts b/src/dynamo/request/scan/scan.request.ts index 6ea2b1c82..a57451f35 100644 --- a/src/dynamo/request/scan/scan.request.ts +++ b/src/dynamo/request/scan/scan.request.ts @@ -1,7 +1,8 @@ import { ScanInput, ScanOutput } from 'aws-sdk/clients/dynamodb' import { Observable } from 'rxjs' -import { map } from 'rxjs/operators' +import { map, tap } from 'rxjs/operators' import { fetchAll } from '../../../helper' +import { createLogger, Logger } from '../../../logger/logger' import { Mapper } from '../../../mapper/mapper' import { Attributes } from '../../../mapper/type/attribute.type' import { ModelConstructor } from '../../../model/model-constructor' @@ -17,8 +18,11 @@ import { ScanResponse } from './scan.response' export class ScanRequest extends Request, ScanInput, ScanResponse> implements Pageable, ScanResponse> { + private readonly logger: Logger + constructor(dynamoRx: DynamoRx, modelClazz: ModelConstructor, tableName: string) { super(dynamoRx, modelClazz, tableName) + this.logger = createLogger('dynamo.request.ScanRequest', modelClazz) } whereAttribute(attributePath: keyof T): RequestConditionFunction> { @@ -34,41 +38,54 @@ export class ScanRequest extends Request, ScanInput, ScanRe execFullResponse(): Observable> { delete this.params.Select + this.logger.debug('request', this.params) return this.dynamoRx.scan(this.params).pipe( + tap(response => this.logger.debug('response', response)), map(queryResponse => { const response: ScanResponse = { ...queryResponse } response.Items = queryResponse.Items!.map(item => Mapper.fromDb(item, this.modelClazz)) return response - }) + }), + tap(response => this.logger.debug('mapped items', response.Items)) ) } execNoMap(): Observable { - return this.dynamoRx.scan(this.params) + this.logger.debug('request (noMap)', this.params) + return this.dynamoRx.scan(this.params).pipe(tap(response => this.logger.debug('response', response))) } exec(): Observable { delete this.params.Select - - return this.dynamoRx - .scan(this.params) - .pipe(map(response => response.Items!.map(item => Mapper.fromDb(item, this.modelClazz)))) + this.logger.debug('request', this.params) + return this.dynamoRx.scan(this.params).pipe( + tap(response => this.logger.debug('response', response)), + map(response => response.Items!.map(item => Mapper.fromDb(item, this.modelClazz))), + tap(items => this.logger.debug('mapped items', items)) + ) } execSingle(): Observable { delete this.params.Select - - return this.dynamoRx - .scan(this.params) - .pipe(map(response => Mapper.fromDb(response.Items![0], this.modelClazz))) + this.logger.debug('single request', this.params) + return this.dynamoRx.scan(this.params).pipe( + tap(response => this.logger.debug('response', response)), + map(response => Mapper.fromDb(response.Items![0], this.modelClazz)), + tap(item => this.logger.debug('mapped item', item)) + ) } execCount(): Observable { const params = { ...this.params } params.Select = 'COUNT' - return this.dynamoRx.scan(params).pipe(map(response => response.Count!)) + this.logger.debug('count request', params) + return this.dynamoRx.scan(params).pipe( + tap(response => this.logger.debug('response', response)), + map(response => response.Count!), + tap(count => this.logger.debug('count', count)) + ) } /** diff --git a/src/dynamo/request/update/update.request.ts b/src/dynamo/request/update/update.request.ts index e44c44294..0bd5908c2 100644 --- a/src/dynamo/request/update/update.request.ts +++ b/src/dynamo/request/update/update.request.ts @@ -1,7 +1,8 @@ import { ReturnConsumedCapacity, ReturnItemCollectionMetrics, UpdateItemOutput } from 'aws-sdk/clients/dynamodb' import { forEach } from 'lodash' import { Observable } from 'rxjs' -import { map } from 'rxjs/operators' +import { map, tap } from 'rxjs/operators' +import { createLogger, Logger } from '../../../logger/logger' import { Mapper } from '../../../mapper/mapper' import { Attributes } from '../../../mapper/type/attribute.type' import { ModelConstructor } from '../../../model/model-constructor' @@ -20,6 +21,8 @@ import { BaseRequest } from '../base.request' export type SortedUpdateExpressions = { [key in UpdateActionKeyword]: UpdateExpression[] } export class UpdateRequest extends BaseRequest { + private readonly logger: Logger + constructor( dynamoRx: DynamoRx, modelClazz: ModelConstructor, @@ -28,6 +31,7 @@ export class UpdateRequest extends BaseRequest { sortKey?: any ) { super(dynamoRx, modelClazz, tableName) + this.logger = createLogger('dynamo.request.UpdateRequest', modelClazz) const hasSortKey: boolean = this.metaData.getSortKey() !== null @@ -137,11 +141,12 @@ export class UpdateRequest extends BaseRequest { } execFullResponse(): Observable { - return this.dynamoRx.updateItem(this.params) + this.logger.debug('request', this.params) + return this.dynamoRx.updateItem(this.params).pipe(tap(response => this.logger.debug('response', response))) } exec(): Observable { - return this.dynamoRx.updateItem(this.params).pipe( + return this.execFullResponse().pipe( map(response => { return }) diff --git a/src/logger/index.ts b/src/logger/index.ts new file mode 100644 index 000000000..8ce3913e1 --- /dev/null +++ b/src/logger/index.ts @@ -0,0 +1,4 @@ +// only export types for public api +export * from './log-info.type' +export * from './log-level.type' +export * from './log-receiver.type' diff --git a/src/logger/log-info.type.ts b/src/logger/log-info.type.ts new file mode 100644 index 000000000..856314237 --- /dev/null +++ b/src/logger/log-info.type.ts @@ -0,0 +1,10 @@ +import { LogLevel } from './log-level.type' + +export interface LogInfo { + className: string + modelClass: string + level: LogLevel + message: string + timestamp: number + data?: any +} diff --git a/src/logger/log-level.type.ts b/src/logger/log-level.type.ts new file mode 100644 index 000000000..dd5cf661a --- /dev/null +++ b/src/logger/log-level.type.ts @@ -0,0 +1 @@ +export type LogLevel = 'warning' | 'info' | 'debug' diff --git a/src/logger/log-receiver.type.ts b/src/logger/log-receiver.type.ts new file mode 100644 index 000000000..a75cde77c --- /dev/null +++ b/src/logger/log-receiver.type.ts @@ -0,0 +1,3 @@ +import { LogInfo } from './log-info.type' + +export type LogReceiver = (logInfo: LogInfo) => any | void diff --git a/src/logger/logger.spec.ts b/src/logger/logger.spec.ts new file mode 100644 index 000000000..b940653e0 --- /dev/null +++ b/src/logger/logger.spec.ts @@ -0,0 +1,23 @@ +import { Employee } from '../../test/models/employee.model' +import { DynamoEasyConfig } from '../config' +import { DynamoStore } from '../dynamo' +import { LogInfo } from './log-info.type' +import { LogReceiver } from './log-receiver.type' + +describe('log receiver', () => { + let logs: LogInfo[] = [] + const logReceiver: LogReceiver = logInfo => logs.push(logInfo) + DynamoEasyConfig.updateConfig({ logReceiver }) + + beforeEach(() => (logs = [])) + + it('receives logs', () => { + const ts = Date.now() + const store = new DynamoStore(Employee) + + expect(store).toBeDefined() + expect(logs.length).toBe(1) + expect(logs[0].timestamp).toBeGreaterThanOrEqual(ts) + expect(logs[0].modelClass).toBe(Employee.name) + }) +}) diff --git a/src/logger/logger.ts b/src/logger/logger.ts new file mode 100644 index 000000000..ce26f4668 --- /dev/null +++ b/src/logger/logger.ts @@ -0,0 +1,32 @@ +import { DynamoEasyConfig } from '../config' +import { ModelConstructor } from '../model/model-constructor' +import { LogLevel } from './log-level.type' + +export type LogFn = (message: string, data?: any) => void + +export interface Logger { + warn: LogFn + info: LogFn + debug: LogFn +} + +function getLogFn(className: string, modelClass: string, level: LogLevel): LogFn { + return (message: string, data?: any) => { + DynamoEasyConfig.config.logReceiver({ + className, + modelClass, + level, + message, + data, + timestamp: Date.now(), + }) + } +} + +export function createLogger(className: string, modelClass: ModelConstructor): Logger { + return { + warn: getLogFn(className, modelClass.name, 'warning'), + info: getLogFn(className, modelClass.name, 'info'), + debug: getLogFn(className, modelClass.name, 'debug'), + } +}