Skip to content
This repository has been archived by the owner on Jan 12, 2021. It is now read-only.

Commit

Permalink
Merge pull request #1 from Foxy/release/1.0.0-beta.5
Browse files Browse the repository at this point in the history
1.0.0-beta.5
  • Loading branch information
brettflorio authored Apr 7, 2020
2 parents 8c4be08 + 0a52c79 commit 3e4af10
Show file tree
Hide file tree
Showing 18 changed files with 889 additions and 79 deletions.
24 changes: 23 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ const foxy = new FoxyApi({
```ts
const foxy = new FoxyApi({
// ...
silence: true, // don't log errors and such to console at all
silent: true, // don't log errors and such to console at all
});
```

Expand Down Expand Up @@ -148,3 +148,25 @@ await store.fetch({
```ts
await store.fetch({ method: "DELETE" });
```

## Development

To get started, clone this repo locally and install the dependencies:

```bash
git clone https://github.com/foxy/foxy-node-api.git
npm install
```

Running tests:

```bash
npm run test # runs all tests and exits
npm run test:watch # looks for changes and re-runs tests as you code
```

Committing changes with [commitizen](https://github.com/commitizen/cz-cli):

```bash
git commit # precommit hooks will lint the staged files and help you format your message correctly
```
3 changes: 2 additions & 1 deletion jest.config.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"collectCoverage": true,
"coverageDirectory": ".coverage",
"coveragePathIgnorePatterns": ["/node_modules/", "<rootDir>/test/mocks/"],
"testEnvironment": "node"
}
}
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@foxy.io/node-api",
"version": "1.0.0-beta.4",
"version": "1.0.0-beta.5",
"description": "FoxyCart hAPI client for Node",
"main": "dist/index.js",
"types": "dist/index.ts",
Expand All @@ -9,6 +9,7 @@
],
"scripts": {
"test": "jest --config ./jest.config.json",
"test:watch": "jest --watch --config ./jest.config.json",
"lint": "tsc --noEmit && eslint \"{src,test}/**/*.ts\" --quiet --fix",
"build": "tsc"
},
Expand Down
106 changes: 90 additions & 16 deletions src/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as winston from "winston";
import * as logform from "logform";
import fetch from "node-fetch";
import { Cache, MemoryCache } from "./utils/cache";
import { Props } from "./types/props";

import {
FOXY_API_CLIENT_ID,
Expand All @@ -13,24 +14,60 @@ import {
type Version = "1";

type AuthInit = {
/**
* OAuth2 client ID for your integration.
* If omitted from the config, the value of the `FOXY_API_CLIENT_ID` env var will be used.
*
* @see https://api.foxycart.com/docs/authentication
* @tutorial https://api.foxycart.com/docs/authentication/client_creation
*/
clientId?: string;

/**
* OAuth2 client secret for your integration.
* If omitted from the config, the value of the `FOXY_API_CLIENT_SECRET` env var will be used.
*
* @see https://api.foxycart.com/docs/authentication
* @tutorial https://api.foxycart.com/docs/authentication/client_creation
*/
clientSecret?: string;

/**
* OAuth2 long-term refresh token for your integration.
* If omitted from the config, the value of the `FOXY_API_REFRESH_TOKEN` env var will be used.
*
* @see https://api.foxycart.com/docs/authentication
* @tutorial https://api.foxycart.com/docs/authentication/client_creation
*/
refreshToken?: string;

/**
* API version to use when making requests.
* So far we have just one ("1") and it's used by default.
*/
version?: Version;

/**
* Cache provider to store access token and other temporary values with.
* See the available built-in options under `FoxyApi.cache` or supply your own.
*/
cache?: Cache;

/**
* Determines how verbose our client will be when logging.
* By default, only errors are logged. To log all messages, set this option to `silly`.
*/
logLevel?: "error" | "warn" | "info" | "http" | "verbose" | "debug" | "silly";
silent?: boolean;
};

type PostInit = {
refreshToken?: string;
clientSecret?: string;
clientId?: string;
};
/** Pass `true` to completely disable logging (`false` by default). */
silent?: boolean;

type PostResponse = {
access_token: string;
expires_in: number;
/**
* Allows changing the API endpoint. You'll most likely never need to use this option.
* A value of the `FOXY_API_URL` env var will be used if found.
* Default value is `https://api.foxycart.com`.
*/
endpoint?: string;
};

type StoredToken = {
Expand All @@ -41,10 +78,22 @@ type StoredToken = {
export class Auth {
private _logger: winston.Logger;

/** OAuth2 client ID for your integration (readonly).*/
readonly clientId: string;

/** OAuth2 client secret for your integration (readonly). */
readonly clientSecret: string;

/** OAuth2 refresh token for your integration (readonly). */
readonly refreshToken: string;

/** API endpoint that requests are made to (readonly). */
readonly endpoint: string;

/** API version used when making requests (readonly). */
readonly version: Version;

/** Cache implementation used with this instance (readonly). */
readonly cache: Cache;

constructor(config?: AuthInit) {
Expand All @@ -62,6 +111,7 @@ export class Auth {
this.refreshToken = refreshToken;
this.version = config?.version ?? "1";
this.cache = config?.cache ?? new MemoryCache();
this.endpoint = config?.endpoint ?? FOXY_API_URL;

this._logger = winston.createLogger({
level: config?.logLevel,
Expand All @@ -76,32 +126,56 @@ export class Auth {
});
}

/**
* Formats and logs a message if `logLevel` param value allows it.
*
* @example
*
* foxy.log({
* level: "http",
* message: "Sending a GET request..."
* });
*
* @param entry the {@link https://www.npmjs.com/package/winston winston} logger options
*/
log(entry: winston.LogEntry) {
this._logger.log(entry);
}

async getAccessToken(init?: PostInit): Promise<string> {
/**
* Fetches and caches the access token for this integration.
* Will return a cached value if there is one and it's still valid, otherwise
* will make an API request and update cache before returning the fresh token.
*
* @example
*
* const token = await foxy.getAccessToken();
*
* @see https://api.foxycart.com/rels/token
* @tutorial https://api.foxycart.com/docs/authentication
*/
async getAccessToken(): Promise<string> {
const token = await this.cache.get("fx_auth_access_token");
if (this._validateToken(token)) return (JSON.parse(token) as StoredToken).value;

const response = await fetch(`${FOXY_API_URL}/token`, {
const response = await fetch(`${this.endpoint}/token`, {
method: "POST",
headers: {
"FOXY-API-VERSION": this.version,
"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({
grant_type: "refresh_token",
refresh_token: init?.refreshToken ?? this.refreshToken,
client_secret: init?.clientSecret ?? this.clientSecret,
client_id: init?.clientId ?? this.clientId,
refresh_token: this.refreshToken,
client_secret: this.clientSecret,
client_id: this.clientId,
}),
});

const text = await response.text();

if (response.ok) {
const json = JSON.parse(text) as PostResponse;
const json = JSON.parse(text) as Props["fx:token"];
const storedToken: StoredToken = {
expiresAt: Date.now() + json.expires_in * 1000,
value: json.access_token,
Expand Down
15 changes: 13 additions & 2 deletions src/follower.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,21 @@
import { PathMember, ApiGraph } from "./types/utils";
import { Sender } from "./sender";

/**
* Part of the API functionality that provides a URL builder
* with IDE autocompletion features powered by TS and JSDoc.
*
* **IMPORTANT:** this class is internal; using it in consumers code is not recommended.
*/
export class Follower<Graph extends ApiGraph, Host extends PathMember> extends Sender<Host> {
/**
* Navigate to a nested resource, building a request query.
* Calling this method will not fetch your data immediately.
* Navigates to the nested resource, building a request query.
* Calling this method will not fetch your data immediately. For the list of relations please refer to the
* {@link https://api.foxycart.com/hal-browser/index.html link relationships} page.
*
* @example
*
* const link = foxy.follow("fx:stores").follow(8);
*
* @param key Nested relation (link) or a numeric id.
*/
Expand Down
80 changes: 72 additions & 8 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,35 +2,99 @@ import { Auth } from "./auth";
import { FOXY_API_URL } from "./env";
import { Follower } from "./follower";
import { Sender, SendRawInit } from "./sender";
import { ApiGraph, Followable } from "./types/utils";
import { Graph } from "./types/graph";
import { Props } from "./types/props";
import * as cache from "./utils/cache";
import * as sanitize from "./utils/sanitize";
import * as sso from "./utils/sso";
import * as webhook from "./utils/webhook";

export class FoxyApi extends Auth {
/**
* Foxy Hypermedia API client for Node.
* To start working with our API, create an instance of this class.
*
* @example
*
* const foxy = new FoxyApi({
* clientId: "...", // or set process.env.FOXY_API_CLIENT_ID
* clientSecret: "...", // or set process.env.FOXY_API_CLIENT_SECRET
* refreshToken: "..." // or set process.env.FOXY_API_REFRESH_TOKEN
* });
*
* @see https://api.foxycart.com/docs
* @tutorial https://github.com/foxy/foxy-node-api
*/
class Api extends Auth {
/** The default API endpoint, also a value of `process.env.FOXY_API_URL` if it's set. */
static readonly endpoint = FOXY_API_URL;

/** A set of useful {@link https://npmjs.com/package/traverse traverse} utils for removing private data from response objects. */
static readonly sanitize = sanitize;

/** A set of utilities for working with our {@link https://docs.foxycart.com/v/2.0/webhooks webhooks}. */
static readonly webhook = webhook;

/** A set of basic cache providers to choose from when creating an instance of this class. */
static readonly cache = cache;

/** A set of utilities for using our {@link https://docs.foxycart.com/v/2.0/sso sso} functionality with your website. */
static readonly sso = sso;

/**
* Makes JSON object received with `.fetch()` followable.
* Makes JSON response object followable.
*
* @example
* const store = await foxy.follow("fx:store").fetch();
* const attributes = await foxy.from(store).follow("fx:attributes");
*
* const store = { _links: { "fx:attributes": { href: "https://api.foxy..." } } };
* const link = foxy.from(store).follow("fx:attributes");
*
* // typescript users: specify resource location in the graph for better autocompletion
* const link = foxy.from<FoxyApiGraph["fx:store"]>(...);
*
* @param resource partial response object with the `_links` property containing relations you'd like to follow
*/
from(snapshot: any) {
return new Follower(this, [], snapshot._links.self.href);
from<G extends ApiGraph, R extends Followable>(resource: R) {
return new Follower<G, any>(this, [], resource._links.self.href);
}

/**
* Starts building a resource URL from the root level. For the list of relations please refer to the
* {@link https://api.foxycart.com/hal-browser/index.html link relationships} page.
*
* @example
*
* const link = foxy.follow("fx:store").follow("fx:attributes");
*
* @param key any root relation
*/
follow<K extends keyof Graph>(key: K) {
return new Follower<Graph[K], K>(this, [key]);
return new Follower<Graph[K], K>(this, [key], this.endpoint);
}

/**
* Makes an API request to the specified URL, skipping the path construction
* and resolution. This is what `.fetch()` uses under the hood. Before calling
* this method, consider using a combination of `foxy.from(resource).fetch()`
* or `foxy.follow(...).fetch()` instead.
*
* @example
*
* const response = await foxy.fetchRaw({
* url: "https://api.foxycart.com/stores",
* method: "POST",
* body: { ... }
* });
*
* // typescript users: provide relation name to get a better response type
* const response = await foxy.fetchRaw<"fx:stores">(...)
*
* @param init fetch-like request initializer supporting url, method and body params
*/
fetchRaw<Host extends keyof Props = any>(init: SendRawInit<Host>) {
return new Sender<Host>(this).fetchRaw(init);
return new Sender<Host>(this, [], this.endpoint).fetchRaw(init);
}
}

export { Api as FoxyApi };
export { Graph as FoxyApiGraph } from "./types/graph";
Loading

0 comments on commit 3e4af10

Please sign in to comment.