-
Notifications
You must be signed in to change notification settings - Fork 11
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 2257e2a
Showing
62 changed files
with
12,088 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
{ | ||
"source": "./lib", | ||
"destination": "./docs", | ||
"plugins": [{"name": "esdoc-standard-plugin"}] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
node_modules/ | ||
docs/ | ||
coverage/ | ||
.nyc_output/ | ||
console.js |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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') | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
Oops, something went wrong.