Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement transactions by address endpoint #35

Merged
merged 1 commit into from
Aug 19, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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[]> => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you want to keep it type safe, you can combine keyof and Pick there:

field: (keyof Pick<Transaction, 'sender' | 'receiver'>) | null

Pick<Transaction, 'sender' | 'receiver'> gives you a type:

{ sender: .., receiver: .. }

and using keyof on this gives you 'sender' | 'receiver'

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
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kpudlik I want to sort by ordinal which is nested lastTransactionRef.ordinal
I set there also a string type but maybe you have a better idea?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typescript doesn't allow that :<

You can technically use something like lenses/path from Ramda: https://ramdajs.com/docs/#path

here you can read more: microsoft/TypeScript#12290

Copy link
Contributor

@kpudlik kpudlik Aug 18, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general you could potentially make Sort<Transaction | Transaction["lastTransactionRef"]>

But it wont give you such string:

lastTransactionRef.ordinal :p

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