Skip to content

Commit

Permalink
Implement transactions by address endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
mwadon committed Aug 19, 2020
1 parent 2417e50 commit 701a10b
Show file tree
Hide file tree
Showing 6 changed files with 156 additions and 24 deletions.
68 changes: 56 additions & 12 deletions api/elastic.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -21,15 +21,13 @@ const getByFieldQuery = <T extends WithTimestamp>(
field: keyof T,
value: string,
size: number = 1,
sort: Sort<T> = {field: 'timestamp', order: SortOrder.Desc}
sort: Sort<T>[] = [{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]: {
Expand All @@ -40,6 +38,27 @@ const getByFieldQuery = <T extends WithTimestamp>(
}
})

const getMultiQuery = <T extends WithTimestamp>(
index: string,
fields: (keyof T)[],
value: string,
size: number = 1,
sort: Sort<T>[] = [{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,
Expand Down Expand Up @@ -82,13 +101,24 @@ export const getTransactionBySnapshot = (es: Client) => (term: string): TaskEith
return findAll(getByFieldQuery<Transaction>(ESIndex.Transactions, 'snapshotHash', term, -1)(es))
}

const findOne = (search: TransportRequestPromise<ApiResponse>) =>
pipe(
findAll(search),
map(s => s[0])
)
export const getTransactionByAddress = (es: Client) => (term: string, field: 'receiver' | 'sender' | null = null): TaskEither<ApplicationError, Transaction[]> => {
const sortByTimestamp: Sort<Transaction> = { field: 'timestamp', order: SortOrder.Desc }
const sortByOrdinal: Sort<Transaction> = { field: 'lastTransactionRef.ordinal', order: SortOrder.Desc }

const findAll = (search: TransportRequestPromise<ApiResponse>) => pipe(
if (!field) {
return findAll(getMultiQuery<Transaction>(ESIndex.Transactions, ['receiver', 'sender'], term, -1, [sortByTimestamp, sortByOrdinal])(es))
}

return findAll(getByFieldQuery<Transaction>(ESIndex.Transactions, field, term, -1, [sortByOrdinal])(es))
}

export const getTransactionBySender = (es: Client) => (term: string): TaskEither<ApplicationError, Transaction[]> =>
getTransactionByAddress(es)(term, 'sender')

export const getTransactionByReceiver = (es: Client) => (term: string): TaskEither<ApplicationError, Transaction[]> =>
getTransactionByAddress(es)(term, 'receiver')

const findOne = (search: TransportRequestPromise<ApiResponse>) => pipe(
tryCatch<ApplicationError, any>(
() => search.then(r => {
return r.body.hits.hits
Expand All @@ -101,7 +131,7 @@ const findAll = (search: TransportRequestPromise<ApiResponse>) => pipe(
),
chain(hits => {
if (hits.length > 0) {
return right(hits.map(h => h._source))
return right(hits[0]._source)
}

return left(
Expand All @@ -112,4 +142,18 @@ const findAll = (search: TransportRequestPromise<ApiResponse>) => pipe(
)
)
}),
)

const findAll = (search: TransportRequestPromise<ApiResponse>) => pipe(
tryCatch<ApplicationError, any>(
() => 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))),
)
13 changes: 12 additions & 1 deletion api/handler.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,20 @@
import {getClient} from './elastic'
import {getCheckpointBlocksHandler, getSnapshotHandler, getTransactionsBySnapshotHandler, getTransactionsHandler} from './service'
import {
getCheckpointBlocksHandler,
getSnapshotHandler,
getTransactionsByAddressHandler,
getTransactionsByReceiverHandler,
getTransactionsBySenderHandler,
getTransactionsBySnapshotHandler,
getTransactionsHandler
} from './service'

const client = getClient()

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)()
2 changes: 1 addition & 1 deletion api/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export enum SortOrder {
}

export type Sort<T> = {
field: keyof T
field: keyof T | string
order: SortOrder
}

Expand Down
27 changes: 27 additions & 0 deletions api/serverless.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
58 changes: 51 additions & 7 deletions api/service.ts
Original file line number Diff line number Diff line change
@@ -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<ApplicationError, APIGatewayEvent>(event),
chain(validateListSnapshotsEvent),
chain(validateSnapshotsEvent),
map(event => event.pathParameters!.term),
chain(getSnapshot(es)),
fold(
Expand All @@ -21,7 +29,7 @@ export const getSnapshotHandler = (event: APIGatewayEvent, es: Client) =>
export const getCheckpointBlocksHandler = (event: APIGatewayEvent, es: Client) =>
pipe(
taskEither.of<ApplicationError, APIGatewayEvent>(event),
chain(validateListCheckpointBlocksEvent),
chain(validateCheckpointBlocksEvent),
map(event => event.pathParameters!.term),
chain(getCheckpointBlock(es)),
fold(
Expand All @@ -33,7 +41,7 @@ export const getCheckpointBlocksHandler = (event: APIGatewayEvent, es: Client) =
export const getTransactionsHandler = (event: APIGatewayEvent, es: Client) =>
pipe(
taskEither.of<ApplicationError, APIGatewayEvent>(event),
chain(validateListTransactionsEvent),
chain(validateTransactionsEvent),
map(event => event.pathParameters!.term),
chain(getTransaction(es)),
fold(
Expand All @@ -45,11 +53,47 @@ export const getTransactionsHandler = (event: APIGatewayEvent, es: Client) =>
export const getTransactionsBySnapshotHandler = (event: APIGatewayEvent, es: Client) =>
pipe(
taskEither.of<ApplicationError, APIGatewayEvent>(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<ApplicationError, APIGatewayEvent>(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<ApplicationError, APIGatewayEvent>(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<ApplicationError, APIGatewayEvent>(event),
chain(validateAddressesEvent),
map(event => event.pathParameters!.term),
chain(getTransactionByReceiver(es)),
fold(
reason => taskEither.of(errorResponse(reason)),
value => taskEither.of(successResponse(StatusCodes.OK)(value))
)
)
12 changes: 9 additions & 3 deletions api/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,19 +54,25 @@ const termIsNotNull = (event: APIGatewayEvent) => pipe(
: right<ApplicationError, APIGatewayEvent>(event))
)

export const validateListSnapshotsEvent = (event: APIGatewayEvent) =>
export const validateSnapshotsEvent = (event: APIGatewayEvent) =>
pipe(
of<ApplicationError, APIGatewayEvent>(event),
chain(termIsNotNull)
)

export const validateListCheckpointBlocksEvent = (event: APIGatewayEvent) =>
export const validateCheckpointBlocksEvent = (event: APIGatewayEvent) =>
pipe(
of<ApplicationError, APIGatewayEvent>(event),
chain(termIsNotNull)
)

export const validateListTransactionsEvent = (event: APIGatewayEvent) =>
export const validateTransactionsEvent = (event: APIGatewayEvent) =>
pipe(
of<ApplicationError, APIGatewayEvent>(event),
chain(termIsNotNull)
)

export const validateAddressesEvent = (event: APIGatewayEvent) =>
pipe(
of<ApplicationError, APIGatewayEvent>(event),
chain(termIsNotNull)
Expand Down

0 comments on commit 701a10b

Please sign in to comment.