Skip to content

Commit

Permalink
Http client list iteration (#4)
Browse files Browse the repository at this point in the history
* wip on list iteration

* add prepublishOnly

* wip resource list

* use resource list for accounts

* working resource list
  • Loading branch information
allenan authored Apr 16, 2020
1 parent dfece21 commit 488f640
Show file tree
Hide file tree
Showing 33 changed files with 889 additions and 92 deletions.
38 changes: 22 additions & 16 deletions integration_tests/tests/create_and_submit_payment.spec.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
import axios from '@helium/http/node_modules/axios'
import nock from 'nock'
import { Keypair, Address } from '@helium/crypto'
import { Client } from '@helium/http'
import { PaymentV1, PaymentV2 } from '@helium/transactions'
import { bobWords, aliceB58 } from '../fixtures/users'

jest.mock('@helium/http/node_modules/axios')
const mockedAxios = axios as jest.Mocked<typeof axios>

test('create and submit a payment txn', async () => {

const bob = await Keypair.fromWords(bobWords)
const aliceAddress = Address.fromB58(aliceB58)

Expand All @@ -24,14 +22,18 @@ test('create and submit a payment txn', async () => {
'QowBCiEBNRpxwi/v7CIxk2rSgmshfs452fd/xsSWOZJimcOGkpUSIQGcZZ1yPMHoEKcuePfer0c2qH8Q74/PyAEAtTMn5+5JpBgKKAEyQMsS6J6SCN1nTYjYPfVmkZUuttx85MSKjzO1b46+tibRZ2a0j+M7nSL8+rz/1PWq1yJQ24srzuZ34jTOrT5rswc=',
)

nock('https://api.helium.io')
.post('/v1/pending_transactions', { txn: serializedTxn })
.reply(200, {
data: {
hash: 'txn hash',
},
})

const client = new Client()
client.transactions.submit(serializedTxn)
const pendingTxn = await client.transactions.submit(serializedTxn)

expect(
mockedAxios.post,
).toHaveBeenCalledWith('https://api.helium.io/v1/pending_transactions', {
txn: serializedTxn,
})
expect(pendingTxn.hash).toBe('txn hash')
})

test('create and submit a PaymentV2 txn', async () => {
Expand All @@ -58,12 +60,16 @@ test('create and submit a PaymentV2 txn', async () => {
'wgGOAQohATUaccIv7+wiMZNq0oJrIX7OOdn3f8bEljmSYpnDhpKVEiUKIQGcZZ1yPMHoEKcuePfer0c2qH8Q74/PyAEAtTMn5+5JpBAKIAEqQK88GjmG9CrESHVdcL//ZfWD+KsBnbKmZqKlx8oD89FUms7OjZNcL5NiQ4o0jREg+ahkjc2jX4SgKBBniM+QoAA='
)

nock('https://api.helium.io')
.post('/v1/pending_transactions', { txn: serializedTxn })
.reply(200, {
data: {
hash: 'txn hash',
},
})

const client = new Client()
client.transactions.submit(serializedTxn)
const pendingTxn = await client.transactions.submit(serializedTxn)

expect(
mockedAxios.post,
).toHaveBeenCalledWith('https://api.helium.io/v1/pending_transactions', {
txn: serializedTxn,
})
expect(pendingTxn.hash).toBe('txn hash')
})
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"eslint-plugin-import": "2.20.2",
"jest": "25.3.0",
"lerna": "3.20.2",
"nock": "^12.0.3",
"prettier-standard": "16.2.1",
"rimraf": "3.0.2",
"standard": "14.3.3",
Expand Down
3 changes: 2 additions & 1 deletion packages/crypto/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@
"scripts": {
"test": "echo \"Error: run tests from root\" && exit 1",
"clean": "rimraf build",
"build": "yarn run clean && tsc"
"build": "yarn run clean && tsc",
"prepublishOnly": "yarn run build"
},
"dependencies": {
"bs58": "^4.0.1",
Expand Down
69 changes: 69 additions & 0 deletions packages/http/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# `@helium/crypto`
![npm](https://img.shields.io/npm/v/@helium/crypto)

## Installation

```shell
$ yarn add @helium/http
# or
$ npm install @helium/http
```

## Usage

### Initializing the Client

By default, the client will be initialized with the production network: `https://api.helium.io`
```js
import { Client } from '@helium/http'
const client = new Client()
client.endpoint //= https://api.helium.io/v1
```

To specify a specific network, such as staging, the client can be initialized with a `Network` instance
```js
import { Client, Network } from '@helium/http'
const client = new Client(Network.staging)
client.endpoint //= https://api.helium.wtf/v1
```

##### Networks

| Network | Base URL | Version |
|----------------------|--------------------------|---------|
| `Network.production` | https://api.helium.io | v1 |
| `Network.staging` | https://api.helium.wtf | v1 |

### Paginating Results

#### Automatic Pagination

```js
for await (const account of client.accounts.list()) {
// do something with account

// after some condition is met, stop iterating
if (someConditionMet)
break
}
```

#### Manual Pagination

```js
const firstPage = await client.accounts.list()
firstPage.data //= [Account, Account, ...]
firstPage.hasMore //= true

const nextPage = await firstPage.nextPage()
firstPage.data //= [Account, Account, ...]
firstPage.hasMore //= false
```

### Resources

#### Accounts

#### Blocks

#### Transactions
9 changes: 7 additions & 2 deletions packages/http/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,14 @@
"scripts": {
"test": "echo \"Error: run tests from root\" && exit 1",
"clean": "rimraf build",
"build": "yarn run clean && tsc"
"build": "yarn run clean && tsc",
"prepublishOnly": "yarn run build"
},
"dependencies": {
"axios": "^0.19.2"
"axios": "^0.19.2",
"qs": "^6.9.3"
},
"devDependencies": {
"@types/qs": "^6.9.1"
}
}
49 changes: 29 additions & 20 deletions packages/http/src/Client.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,41 @@
import axios from 'axios'
import Transactions from './Transactions'

interface ClientOptions {
endpoint?: string
version?: number
}
import axios, { AxiosInstance } from 'axios'
import qs from 'qs'
import Network from './Network'
import Transactions from './resources/Transactions'
import Blocks from './resources/Blocks'
import Accounts from './resources/Accounts'
import { getUserAgent } from './util/instrument'

export default class Client {
public endpoint!: string
public version!: number
public network!: Network
private axios!: AxiosInstance

public transactions!: Transactions
public blocks!: Blocks
public accounts!: Accounts

constructor({
endpoint = 'https://api.helium.io',
version = 1,
}: ClientOptions = {}) {
this.endpoint = endpoint
this.version = version
constructor(network: Network = Network.production) {
this.network = network

this.axios = axios.create({
baseURL: this.network.endpoint,
headers: {
'User-Agent': getUserAgent(),
},
})

this.transactions = new Transactions(this)
this.blocks = new Blocks(this)
this.accounts = new Accounts(this)
}

async post(path: string, params: Object = {}) {
const url = this.toUrl(path)
return axios.post(url, params)
async get(path: string, params: Object = {}) {
const query = qs.stringify(params)
const url = query.length > 0 ? [path, query].join('?') : path
return this.axios.get(url)
}

private toUrl(path: string): string {
return [this.endpoint, `v${1}`, path.replace(/^\/+/, '')].join('/')
async post(path: string, params: Object = {}) {
return this.axios.post(path, params)
}
}
Empty file added packages/http/src/ErrorCode.ts
Empty file.
28 changes: 28 additions & 0 deletions packages/http/src/Network.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
interface NetworkOptions {
baseURL: string
version: number
}

export default class Network {
static production = new Network({
baseURL: 'https://api.helium.io',
version: 1,
})

static staging = new Network({
baseURL: 'https://api.helium.wtf',
version: 1,
})

public baseURL: string
public version: number

constructor({ baseURL, version }: NetworkOptions) {
this.baseURL = baseURL
this.version = version
}

get endpoint(): string {
return [this.baseURL, `v${this.version}`].join('/')
}
}
45 changes: 45 additions & 0 deletions packages/http/src/ResourceList.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
export default class ResourceList {
public data: Array<any>
private fetchMore?: any
private cursor?: string
private takeIterator?: AsyncGenerator<any, void, any>

constructor(data: Array<any>, fetchMore?: any, cursor?: string) {
this.data = data
this.fetchMore = fetchMore
this.cursor = cursor
}

async nextPage(): Promise<ResourceList> {
return this.fetchMore({ cursor: this.cursor })
}

public get hasMore(): boolean {
return !!this.cursor && !!this.fetchMore
}

async *[Symbol.asyncIterator]() {
for (const item of this.data) {
yield item
}
if (!this.hasMore) return
yield* await this.fetchMore({ cursor: this.cursor })
}

async take(count: number): Promise<Array<any>> {
if (!this.takeIterator) {
this.takeIterator = this[Symbol.asyncIterator]()
}
const values = []
while (values.length < count) {
const { value, done } = await this.takeIterator.next()
if (value !== undefined) values.push(value)
if (done) return values
}
return values
}

takeReset() {
this.takeIterator = undefined
}
}
13 changes: 0 additions & 13 deletions packages/http/src/Transactions.ts

This file was deleted.

46 changes: 28 additions & 18 deletions packages/http/src/__tests__/Client.spec.ts
Original file line number Diff line number Diff line change
@@ -1,36 +1,46 @@
import axios from 'axios'
import nock from 'nock'
import Client from '..'

jest.mock('axios')
const mockedAxios = axios as jest.Mocked<typeof axios>
import { Network } from '../'

test('exposes a client instance with default options', () => {
const prodUrl = 'https://api.helium.io'
const client = new Client()
expect(client.endpoint).toBe(prodUrl)
expect(client.network.baseURL).toBe(prodUrl)
})

test('configure client with different endpoint', () => {
const stagingUrl = 'https://api.helium.wtf'
const client = new Client({ endpoint: stagingUrl })
expect(client.endpoint).toBe(stagingUrl)
const client = new Client(Network.staging)
expect(client.network.baseURL).toBe(stagingUrl)
})

test('configure client with different version', () => {
const client = new Client({ version: 2 })
expect(client.version).toBe(2)
describe('get', () => {
it('creates a GET request to the full url', async () => {
nock('https://api.helium.io')
.get('/v1/greeting')
.reply(200, {
greeting: 'hello',
})

const client = new Client()

const { data } = await client.get('/greeting')

expect(data.greeting).toBe('hello')
})
})

describe('http methods', () => {
it('posts requests to the full url', async () => {
describe('post', () => {
it('creates a POST request to the full url', async () => {
nock('https://api.helium.io')
.post('/v1/greeting', { greeting: 'hello' })
.reply(200, {
response: 'hey there!',
})
const client = new Client()
const params = { greeting: 'hello' }

await client.post('/greeting', params)

expect(mockedAxios.post).toHaveBeenCalledWith(
'https://api.helium.io/v1/greeting',
params,
)
const { data } = await client.post('/greeting', params)
expect(data.response).toBe('hey there!')
})
})
Loading

0 comments on commit 488f640

Please sign in to comment.