From 701a10b3e327227aceaa55cf7a650abfa7765f1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Wado=C5=84?= Date: Tue, 18 Aug 2020 18:01:45 +0200 Subject: [PATCH] Implement transactions by address endpoint --- api/elastic.ts | 68 ++++++++++++++++++++++++++++++++++++++-------- api/handler.ts | 13 ++++++++- api/model.ts | 2 +- api/serverless.yml | 27 ++++++++++++++++++ api/service.ts | 58 ++++++++++++++++++++++++++++++++++----- api/validation.ts | 12 ++++++-- 6 files changed, 156 insertions(+), 24 deletions(-) diff --git a/api/elastic.ts b/api/elastic.ts index a13dc25..ff6fe36 100644 --- a/api/elastic.ts +++ b/api/elastic.ts @@ -1,5 +1,5 @@ import {ApiResponse, Client} from '@elastic/elasticsearch' -import {chain, left, map, right, TaskEither, tryCatch} from 'fp-ts/lib/TaskEither' +import {chain, left, right, TaskEither, tryCatch} from 'fp-ts/lib/TaskEither' import {ApplicationError, StatusCodes} from './http' import {CheckpointBlock, Snapshot, Sort, SortOrder, Transaction, WithTimestamp} from './model' import {pipe} from 'fp-ts/lib/pipeable' @@ -21,15 +21,13 @@ const getByFieldQuery = ( field: keyof T, value: string, size: number = 1, - sort: Sort = {field: 'timestamp', order: SortOrder.Desc} + sort: Sort[] = [{field: 'timestamp', order: SortOrder.Desc}] ) => (es: Client) => es.search({ index, body: { size, - sort: [ - {[sort.field]: sort.order} - ], + sort: sort.map(s => ({ [s.field]: s.order })), query: { match: { [field]: { @@ -40,6 +38,27 @@ const getByFieldQuery = ( } }) +const getMultiQuery = ( + index: string, + fields: (keyof T)[], + value: string, + size: number = 1, + sort: Sort[] = [{field: 'timestamp', order: SortOrder.Desc}] +) => (es: Client) => + es.search({ + index, + body: { + size, + sort: sort.map(s => ({ [s.field]: s.order })), + query: { + multi_match: { + query: value, + fields + } + } + } + }) + const getLatestQuery = (index: string) => (es: Client) => es.search({ index, @@ -82,13 +101,24 @@ export const getTransactionBySnapshot = (es: Client) => (term: string): TaskEith return findAll(getByFieldQuery(ESIndex.Transactions, 'snapshotHash', term, -1)(es)) } -const findOne = (search: TransportRequestPromise) => - pipe( - findAll(search), - map(s => s[0]) - ) +export const getTransactionByAddress = (es: Client) => (term: string, field: 'receiver' | 'sender' | null = null): TaskEither => { + const sortByTimestamp: Sort = { field: 'timestamp', order: SortOrder.Desc } + const sortByOrdinal: Sort = { field: 'lastTransactionRef.ordinal', order: SortOrder.Desc } -const findAll = (search: TransportRequestPromise) => pipe( + if (!field) { + return findAll(getMultiQuery(ESIndex.Transactions, ['receiver', 'sender'], term, -1, [sortByTimestamp, sortByOrdinal])(es)) + } + + return findAll(getByFieldQuery(ESIndex.Transactions, field, term, -1, [sortByOrdinal])(es)) +} + +export const getTransactionBySender = (es: Client) => (term: string): TaskEither => + getTransactionByAddress(es)(term, 'sender') + +export const getTransactionByReceiver = (es: Client) => (term: string): TaskEither => + getTransactionByAddress(es)(term, 'receiver') + +const findOne = (search: TransportRequestPromise) => pipe( tryCatch( () => search.then(r => { return r.body.hits.hits @@ -101,7 +131,7 @@ const findAll = (search: TransportRequestPromise) => pipe( ), chain(hits => { if (hits.length > 0) { - return right(hits.map(h => h._source)) + return right(hits[0]._source) } return left( @@ -112,4 +142,18 @@ const findAll = (search: TransportRequestPromise) => pipe( ) ) }), +) + +const findAll = (search: TransportRequestPromise) => pipe( + tryCatch( + () => search.then(r => { + return r.body.hits.hits + }), + err => new ApplicationError( + 'ElasticSearch error', + [err as string], + StatusCodes.SERVER_ERROR + ) + ), + chain(hits => right(hits.map(h => h._source))), ) \ No newline at end of file diff --git a/api/handler.ts b/api/handler.ts index 294c6d5..8489467 100644 --- a/api/handler.ts +++ b/api/handler.ts @@ -1,5 +1,13 @@ import {getClient} from './elastic' -import {getCheckpointBlocksHandler, getSnapshotHandler, getTransactionsBySnapshotHandler, getTransactionsHandler} from './service' +import { + getCheckpointBlocksHandler, + getSnapshotHandler, + getTransactionsByAddressHandler, + getTransactionsByReceiverHandler, + getTransactionsBySenderHandler, + getTransactionsBySnapshotHandler, + getTransactionsHandler +} from './service' const client = getClient() @@ -7,3 +15,6 @@ export const snapshots = async event => getSnapshotHandler(event, client)() export const checkpointBlocks = async event => getCheckpointBlocksHandler(event, client)() export const transactions = async event => getTransactionsHandler(event, client)() export const transactionsBySnapshot = async event => getTransactionsBySnapshotHandler(event, client)() +export const transactionsByAddress = async event => getTransactionsByAddressHandler(event, client)() +export const transactionsBySender = async event => getTransactionsBySenderHandler(event, client)() +export const transactionsByReceiver = async event => getTransactionsByReceiverHandler(event, client)() diff --git a/api/model.ts b/api/model.ts index 29bf5a9..38d59c5 100644 --- a/api/model.ts +++ b/api/model.ts @@ -8,7 +8,7 @@ export enum SortOrder { } export type Sort = { - field: keyof T + field: keyof T | string order: SortOrder } diff --git a/api/serverless.yml b/api/serverless.yml index c09d784..f74c304 100644 --- a/api/serverless.yml +++ b/api/serverless.yml @@ -45,6 +45,33 @@ functions: parameters: paths: term: true + transactionsByAddress: + handler: handler.transactionsByAddress + events: + - http: + path: address/{term}/transaction + method: GET + parameters: + paths: + term: true + transactionsBySender: + handler: handler.transactionsBySender + events: + - http: + path: address/{term}/transaction/sent + method: GET + parameters: + paths: + term: true + transactionsByReceiver: + handler: handler.transactionsByReceiver + events: + - http: + path: address/{term}/transaction/received + method: GET + parameters: + paths: + term: true plugins: - serverless-plugin-typescript \ No newline at end of file diff --git a/api/service.ts b/api/service.ts index 0d6f3f8..96cd97e 100644 --- a/api/service.ts +++ b/api/service.ts @@ -1,15 +1,23 @@ import {Client} from '@elastic/elasticsearch' import {ApplicationError, errorResponse, StatusCodes, successResponse} from './http' import {APIGatewayEvent} from 'aws-lambda' -import {chain, fold, taskEither, map} from 'fp-ts/lib/TaskEither' +import {chain, fold, map, taskEither} from 'fp-ts/lib/TaskEither' import {pipe} from 'fp-ts/lib/pipeable' -import {getSnapshot, getCheckpointBlock, getTransaction, getTransactionBySnapshot} from './elastic' -import {validateListCheckpointBlocksEvent, validateListSnapshotsEvent, validateListTransactionsEvent} from './validation' +import { + getCheckpointBlock, + getSnapshot, + getTransaction, + getTransactionByAddress, + getTransactionByReceiver, + getTransactionBySender, + getTransactionBySnapshot +} from './elastic' +import {validateAddressesEvent, validateCheckpointBlocksEvent, validateSnapshotsEvent, validateTransactionsEvent} from './validation' export const getSnapshotHandler = (event: APIGatewayEvent, es: Client) => pipe( taskEither.of(event), - chain(validateListSnapshotsEvent), + chain(validateSnapshotsEvent), map(event => event.pathParameters!.term), chain(getSnapshot(es)), fold( @@ -21,7 +29,7 @@ export const getSnapshotHandler = (event: APIGatewayEvent, es: Client) => export const getCheckpointBlocksHandler = (event: APIGatewayEvent, es: Client) => pipe( taskEither.of(event), - chain(validateListCheckpointBlocksEvent), + chain(validateCheckpointBlocksEvent), map(event => event.pathParameters!.term), chain(getCheckpointBlock(es)), fold( @@ -33,7 +41,7 @@ export const getCheckpointBlocksHandler = (event: APIGatewayEvent, es: Client) = export const getTransactionsHandler = (event: APIGatewayEvent, es: Client) => pipe( taskEither.of(event), - chain(validateListTransactionsEvent), + chain(validateTransactionsEvent), map(event => event.pathParameters!.term), chain(getTransaction(es)), fold( @@ -45,11 +53,47 @@ export const getTransactionsHandler = (event: APIGatewayEvent, es: Client) => export const getTransactionsBySnapshotHandler = (event: APIGatewayEvent, es: Client) => pipe( taskEither.of(event), - chain(validateListTransactionsEvent), + chain(validateTransactionsEvent), map(event => event.pathParameters!.term), chain(getTransactionBySnapshot(es)), fold( reason => taskEither.of(errorResponse(reason)), value => taskEither.of(successResponse(StatusCodes.OK)(value)) ) + ) + +export const getTransactionsByAddressHandler = (event: APIGatewayEvent, es: Client) => + pipe( + taskEither.of(event), + chain(validateAddressesEvent), + map(event => event.pathParameters!.term), + chain(getTransactionByAddress(es)), + fold( + reason => taskEither.of(errorResponse(reason)), + value => taskEither.of(successResponse(StatusCodes.OK)(value)) + ) + ) + +export const getTransactionsBySenderHandler = (event: APIGatewayEvent, es: Client) => + pipe( + taskEither.of(event), + chain(validateAddressesEvent), + map(event => event.pathParameters!.term), + chain(getTransactionBySender(es)), + fold( + reason => taskEither.of(errorResponse(reason)), + value => taskEither.of(successResponse(StatusCodes.OK)(value)) + ) + ) + +export const getTransactionsByReceiverHandler = (event: APIGatewayEvent, es: Client) => + pipe( + taskEither.of(event), + chain(validateAddressesEvent), + map(event => event.pathParameters!.term), + chain(getTransactionByReceiver(es)), + fold( + reason => taskEither.of(errorResponse(reason)), + value => taskEither.of(successResponse(StatusCodes.OK)(value)) + ) ) \ No newline at end of file diff --git a/api/validation.ts b/api/validation.ts index 81efb05..a06a16f 100644 --- a/api/validation.ts +++ b/api/validation.ts @@ -54,19 +54,25 @@ const termIsNotNull = (event: APIGatewayEvent) => pipe( : right(event)) ) -export const validateListSnapshotsEvent = (event: APIGatewayEvent) => +export const validateSnapshotsEvent = (event: APIGatewayEvent) => pipe( of(event), chain(termIsNotNull) ) -export const validateListCheckpointBlocksEvent = (event: APIGatewayEvent) => +export const validateCheckpointBlocksEvent = (event: APIGatewayEvent) => pipe( of(event), chain(termIsNotNull) ) -export const validateListTransactionsEvent = (event: APIGatewayEvent) => +export const validateTransactionsEvent = (event: APIGatewayEvent) => + pipe( + of(event), + chain(termIsNotNull) + ) + +export const validateAddressesEvent = (event: APIGatewayEvent) => pipe( of(event), chain(termIsNotNull)