Skip to content

Commit

Permalink
Initial Commit
Browse files Browse the repository at this point in the history
  • Loading branch information
bhelx committed Mar 14, 2019
0 parents commit 2257e2a
Show file tree
Hide file tree
Showing 62 changed files with 12,088 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .esdoc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"source": "./lib",
"destination": "./docs",
"plugins": [{"name": "esdoc-standard-plugin"}]
}
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
node_modules/
docs/
coverage/
.nyc_output/
console.js
136 changes: 136 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
# Recurly

Warning:
This library is not meant to be used with the V2 API. If you are attempting to build an integration with V2, please see [https://dev.recurly.com/](https://dev.recurly.com/).

This repository contains the node client for Recurly's V3 API (or "partner api").
It's currently Beta software and is not yet an official release. Documentation for the HTTP API can be found [here](https://partner-docs.recurly.com/).

## Getting Started

### Documentation

This library uses [documentation](http://documentation.js.org/) to generate jsdocs, but it's not published anywhere yet.
To view documentation locally run:

```
./scripts/build && open docs/index.html
```

To view the HTTP API documentation along with the node example code, see [https://partner-docs.recurly.com/](https://partner-docs.recurly.com/).

### Installing

This library is published on npm under the name `recurly`.

We recommend manually inserting the dependency into the `dependencies` section of your `package.json`:

```
{
// ...
"recurly" : "3.0.0.beta.1"
// ...
}
```


Install via the command line:
```
npm install [email protected] --save-prod
```

### Creating a client

A client object represents a connection to the Recurly API. The client implements
each `operation` that can be performed in the API as a method.

To initialize a client, give it an API key and a subdomain:

```js
const recurly = require('recurly')
// You should store your api key somewhere safe
// and not in plain text if possible
const myApiKey = '<myapikey>'
const mySubdomain = '<mysubdomain>'
const client = new recurly.Client(myApiKey, `subdomain-${mySubdomain}`)
```

### Operations

All operations are `async` and return promises (except the `list*` methods which return `Pager`s).
You can handle the promises directly with `then` and `catch` or use await:

```js
client.getAccount('code-benjamin')
.then(account => console.log(account.id))
.catch(err => console.log(err.msg))
```

```js
async myFunc () {
try {
let account = await client.getAccount('code-benjamin')
} catch (err) {
// handle err from client
}
}
```

### Creating Resources

For creating or updating resources, pass a json object to one of the create* or update* methods.
Keep in mind that the api accepts snake-cased keys but this library expects camel-cased keys.
We do the translation for you so this library can conform to js style standards.

```js
client.createAccount({
code: 'new-account-code',
firstName: 'Benjamin',
lastName: 'Du Monde'
})
.then(account => console.log(account.id))
.catch(console.log)
```

### Pagination

All `list*` methods on the client return a `Pager`. They
are not `async` because they are lazy and do not make any
network requests until they are iterated over. There are
two methods on `Pager` that return async iterators `each` and `eachPage`:

* `each` will give you an iterator over each item that matches your query.
* `eachPage` will give you an iterator over each page that is returned. The result is an array of resources.

TODO: Need to fully test and document error handling

```js
async function eachAccount (accounts) {
try {
for await (const acct of accounts.each()) {
console.log(acct.id)
}
} catch (err) {
// err is bubbled up from recurly client
}
}

async function eachPageOfAccounts (accounts) {
try {
for await (const page of accounts.eachPage()) {
page.forEach(acct => console.log(acct.id))
}
} catch (err) {
// err is bubbled up from recurly client
}
}

let accounts = client.listAccounts({
beginTime: '2018-12-01T00:00:00Z',
sort: 'updated_at'
})

eachAccount(accounts)
// or
eachPageOfAccounts(accounts)
```
6 changes: 6 additions & 0 deletions lib/recurly.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
'use strict'

let resources = require('./recurly/resources')
module.exports = Object.assign(resources, {
'Client': require('./recurly/Client')
})
10 changes: 10 additions & 0 deletions lib/recurly/ApiError.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
class ApiError extends Error {
constructor (message, type, params) {
super(message)
this.name = 'RecurlyApiError'
this.type = type
this.params = params || []
}
}

module.exports = ApiError
112 changes: 112 additions & 0 deletions lib/recurly/BaseClient.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
'use strict'

const https = require('https')

const pkg = require('../../package')

const ApiError = require('./ApiError')

const casters = require('./Caster')

const querystring = require('querystring')

/**
* This is the base functionality for the Recurly.Client.
* @private
* @param {string} apiKey - The private api key for the site.
* @param {string} siteId - The site id (if you only have the subdomain, use the subdomain- prefix: `subdomain-${mySubdomain}`).
*/
class BaseClient {
constructor (apiKey, siteId) {
this.siteId = siteId

// API key should not be instance variable.
// This way it's not accidentally logged.
this._getApiKey = () => apiKey

this._httpOptions = {
host: 'partner-api.recurly.com',
port: 443,
agent: new https.Agent({
keepAlive: true
})
}
}

_interpolatePath (path, parameters = {}) {
parameters['site_id'] = this.siteId
Object.keys(parameters).forEach(name => {
path = path.replace(`{${name}}`, parameters[name])
})
return path
}

_makeRequest (method, path, request, params) {
if (params) {
path = path + '?' + querystring.stringify(casters.castRequest(params))
}

const options = this._getDefaultOptions(method, path)

let requestBody
if (request) {
requestBody = JSON.stringify(casters.castRequest(request))
options.headers['Content-Length'] = Buffer.byteLength(requestBody)
}

return new Promise((resolve, reject) => {
// console.log('Sending Request: ', options)
const request = https.request(options, (response) => {
const body = []
response.setEncoding('utf8')
response.on('data', (chunk) => body.push(chunk))
response.on('end', () => {
let jsonBody
if (body.length > 0) {
jsonBody = JSON.parse(body.join(''))
// console.log('Got Response: ', jsonBody)
}

let deprecated = response.headers['Recurly-Deprecated'] || ''
if (!this['_ignore_deprecation_warning'] && deprecated.toUpperCase() === 'TRUE') {
let sunset = response.headers['Recurly-Sunset-Date']
console.log(`[recurly-client-node] WARNING: Your current API version "${this.apiVersion()}" is deprecated and will be sunset on ${sunset}`)
}

if (response.statusCode < 200 || response.statusCode > 299) {
const err = jsonBody.error
reject(new ApiError(err.message, err.type, err.params))
} else {
resolve(casters.castResponse(jsonBody))
}
})
})
request.on('error', reject)
if (requestBody) {
request.write(requestBody)
}
request.end()
})
}

_getDefaultOptions (method, path) {
// Create a copy to ensure that this._httpOptions is not modified
const options = Object.assign({}, this._httpOptions)
options.method = method
options.path = path

// The headers can't be stored in _httpOptions because this object will
// be directly modified for certain requests. Object.assign() does not allow
// for deep cloning
options.headers = {
'Accept': `application/vnd.recurly.${this.apiVersion()}`,
'User-Agent': `Recurly/${pkg.version}; ${pkg.name}`,
'Authorization': 'Basic ' + Buffer.from(this._getApiKey() + ':', 'ascii').toString('base64'),
'Content-Type': 'application/json'
}

return options
}
}

module.exports = BaseClient
96 changes: 96 additions & 0 deletions lib/recurly/Caster.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
const resources = require('./resources')

const dtRegex = /^(-?(?:[1-9][0-9]*)?[0-9]{4})-(1[0-2]|0[1-9])-(3[01]|0[1-9]|[12][0-9])T(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])(\\.[0-9]+)?(Z)?$/

function className (obj) {
const objName = obj && obj.object
if (!objName) return null
return objName
.replace(/_([a-z])/g, g => g[1].toUpperCase())
.replace(/^\w/, g => g.toUpperCase())
}

function camelize (name) {
return name.replace(/(_\w)/g, m => m[1].toUpperCase())
}

function snakeify (name) {
return name.split(/(?=[A-Z])/).join('_').toLowerCase()
}

function construct (Klass, data) {
return Object.assign(new Klass(), data)
}

/**
* Turns a plain javascript object into Recurly domain objects.
* This involves camelizing the snake-cased keys and converting the
* untyped objects into typed objects. It also casts special types like
* datetimes.
*
* @private
* @param {Object} obj - The plain js object to cast to Recurly object
* @param {Object} resourceDefs - The optional resource definitions used when casting. Only used for testing.
* @return {Object}
*/
function castResponse (obj, resourceDefs = resources) {
const name = className(obj)
const Klass = resourceDefs[name]

// either create a Klass object or just a plain js object
const resource = !(name && Klass) ? {} : construct(Klass, {})

for (let key in obj) {
const value = obj[key]
const newKey = camelize(key)

if (value) {
// if it's a resource, cast the item
if (value.object) {
resource[newKey] = castResponse(value, resourceDefs)
// if it's an array, cast each element
} else if (Array.isArray(value) && value.length > 0 && value[0].object) {
resource[newKey] = value.map(v => castResponse(v, resourceDefs))
// cast other special types like datetimes
// TODO check the performance of this approach
} else if (typeof value === 'string' && value.match(dtRegex)) {
resource[newKey] = new Date(value)
} else {
resource[newKey] = value
}
}
}

// I assume we don't need this, maybe we
// want to preserve it for some reason
delete resource['object']

return resource
}

/**
* Turns a plain javascript object into Recurly request.
* This involves snakeifying the camel-cased keys.
*
* @private
* @param {Object} obj - The plain js object to cast to Recurly request
* @return {Object}
*/
function castRequest (obj) {
const newObj = {}
for (let key in obj) {
const value = obj[key]
// the order of these conditionals are important
if (Array.isArray(value) && value.length > 0 && typeof value[0] === 'object') {
newObj[snakeify(key)] = value.map(v => castRequest(v))
} else if (typeof value === 'object') {
newObj[snakeify(key)] = castRequest(value)
} else {
newObj[snakeify(key)] = value
}
}
return newObj
}

module.exports.castResponse = castResponse
module.exports.castRequest = castRequest
Loading

0 comments on commit 2257e2a

Please sign in to comment.