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

Add provider and status APIs #25

Merged
merged 5 commits into from
May 29, 2019
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
3 changes: 3 additions & 0 deletions index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export { AuthAPIClient } from "./src/v1/AuthAPIClient";
export { DataAPIClient } from "./src/v1/DataAPIClient";
export { StatusAPIClient } from "./src/v1/StatusAPIClient";
export { Constants } from "./src/v1/Constants";
export { IAccount } from "./src/v1/interfaces/data/IAccount";
export { IBalance } from "./src/v1/interfaces/data/IBalance";
Expand All @@ -13,3 +14,5 @@ export { ITransaction } from "./src/v1/interfaces/data/ITransaction";
export { ICard } from "./src/v1/interfaces/data/ICard";
export { ICardBalance } from "./src/v1/interfaces/data/ICardBalance";
export { ICardTransaction } from "./src/v1/interfaces/data/ICardTransaction";
export { IProviderInfo } from "./src/v1/interfaces/auth/IProviderInfo";
export { IStatusInfo } from "./src/v1/interfaces/status/IStatusInfo";
25 changes: 24 additions & 1 deletion src/v1/AuthAPIClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import { ApiError } from "./APIError";
import { Constants } from "./Constants";
import { IAuthResponse } from "./interfaces/auth/IAuthResponse";
import { IOptions } from "./interfaces/auth/IOptions";
import { IProviderInfo } from './interfaces/auth/IProviderInfo';
import { ITokenResponse } from "./interfaces/auth/ITokenResponse";
import * as moment from "moment";
import * as request from "request-promise";

/**
Expand Down Expand Up @@ -169,4 +169,27 @@ export class AuthAPIClient {
throw new ApiError(error);
}
}

/**
* Gets info about available providers
* Docs: https://docs.truelayer.com/#list-of-supported-providers
*
* @param {string} [type] when provided, returns only providers of the given type
*/
public static async getProviderInfos(
type?: "credentialssharing" | "oauth" | "oauth/openbanking",
): Promise<IProviderInfo[]> {

const requestOptions: request.Options = {
uri: `${Constants.AUTH_URL}/api/providers/${type || ""}`,
}

try {
const response: string = await request.get(requestOptions);
const parsedResponse: IProviderInfo[] = JSON.parse(response);
return parsedResponse;
} catch (error) {
throw new ApiError(error);
}
}
}
1 change: 1 addition & 0 deletions src/v1/Constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@ export class Constants {
// Constants
public static readonly AUTH_URL: string = "https://auth.truelayer.com";
public static readonly API_URL: string = "https://api.truelayer.com";
public static readonly STATUS_URL: string = "https://status-api.truelayer.com";
public static readonly API_TIMEOUT: number = 60000;
}
83 changes: 83 additions & 0 deletions src/v1/StatusAPIClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { ApiError } from "./APIError";
import { Constants } from "./Constants";
import { IResult } from "./interfaces/data/IResponse";
import * as request from "request-promise";

import { IStatusInfo } from "./interfaces/status/IStatusInfo";
import moment = require("moment");

/**
* Class responsible for calling to the Data endpoints
*/
export class StatusAPIClient {
/**
* Generic API calling function
*
* @template T
* @param {string} accessToken
* @param {string} path
* @param {object} [qs]
* @returns {Promise<IResult<T>>}
*/
public static async callAPI<T>(
path: string,
qs?: object
): Promise<IResult<T>> {
const requestOptions = StatusAPIClient.buildRequestOptions(path, qs);
try {
const response = await request.get(requestOptions);
const parsedResponse: IResult<T> = JSON.parse(response);
return parsedResponse;
} catch (error) {
throw new ApiError(error);
}
}

/**
* Build Request options
*
* @private
* @param {string} accessToken
* @param {string} path
* @param {object} [qs]
* @returns {request.Options}
*/
public static buildRequestOptions(
path: string,
qs?: object
): request.Options {
const requestOptions: request.Options = {
uri: path,
timeout: Constants.API_TIMEOUT
};

if (qs) {
requestOptions.qs = qs;
requestOptions.useQuerystring = true;
}

return requestOptions;
}

/**
* Call to /data/status API.
*
* @returns {Promise<IResult<IStatusInfo>>}
*/
public static async getStatus(
from?: Date,
to?: Date,
providers?: string[],
endpoints?: string[]
) {
const dateFormat = "YYYY-MM-DD HH:MM:SS";
const url = `${Constants.STATUS_URL}/api/v1/data/status`;
const qs = {
from: moment(from).format(dateFormat),
to: moment(to).format(dateFormat),
providers: providers ? providers.join(",") : null,
endpoints: endpoints ? endpoints.join(",") : null,
};
return await StatusAPIClient.callAPI<IStatusInfo>(url, qs);
}
}
2 changes: 1 addition & 1 deletion src/v1/interfaces/auth/IJWT.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,4 @@ export interface IJWT {
*
* @type: Scope
*/
export type Scope = "info" | "accounts" | "transactions" | "balance" | "offline_access";
export type Scope = "info" | "accounts" | "transactions" | "balance" | "cards" | "offline_access";
21 changes: 21 additions & 0 deletions src/v1/interfaces/auth/IProviderInfo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { IProvider } from '../data/ICard';
import { Scope } from './IJWT';

/**
* Description of a data provider.
* Docs: https://docs.truelayer.com/#list-of-supported-providers
*
* @interface IProviderInfo
*/
export interface IProviderInfo {
// NOTE: this could extend IProvider adding only the `scopes` field, but the
// naming is inconsistent (`logo_uri` / `logo_url`)
/** Unique TrueLayer provider ID **/
provider_id: string;
/** Human friendly provider name */
display_name: string;
/** URL to the providers logo as used by TrueLayer */
logo_url: string;
/** List of Permissions supported by the provider */
scopes: Scope[];
}
24 changes: 24 additions & 0 deletions src/v1/interfaces/status/IStatusInfo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
export interface IStatusInfo {
/** A string containing the provider id */
providers: IProviderStatus[];
/** The start of a one-hour time bucket */
timestamp: string;
}

export interface IProviderStatus {
/** A string containing the provider id */
provider_id: string;
/** Array of availability data for requested endpoints * @type {IEndpoint} */
endpoints: IEndpoint[];
}

export interface IEndpoint {
/** Partial URL of the endpoint */
endpoint: string;
/** The percentage of successful requests for a provider's endpoint represented as a float value between 0-100 */
availability: number;
/** The percentage of unsuccessful requests (that we know are on the provider side) for a provider's endpoint represented as a float value between 0-100 */
provider_error: number;
/** The percentage of unsuccessful requests (due to TrueLayer) for a provider's endpoint represented as a float value between 0-100 */
truelayer_error: number;
}
22 changes: 22 additions & 0 deletions test/integration/integration.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { DataAPIClient } from "../../src/v1/DataAPIClient";
import { test } from "ava";
import * as moment from "moment";
import * as TrueLayer from "./../../index";
import { StatusAPIClient } from "../../src/v1/StatusAPIClient";

// Get access token from environment variable
const access_token: string = process.env.access_token;
Expand Down Expand Up @@ -258,3 +259,24 @@ if (DataAPIClient.validateToken(access_token)) {
} else {
test("No 'access_token' environment variable set. Integration test disabled.", (t) => t.pass());
}

const now = moment(new Date(Date.now())).startOf("hour");
const from = now.clone().subtract(2, "hours").toDate();
const to = now.clone().subtract(1, "hours").toDate();
const providers = ["hsbc", "oauth-monzo", "ob-barclays"];
const endpoints = ["accounts", "info"];

test.serial("Get /data/status returns success", async (t) => {
t.plan(1);
await t.notThrows(StatusAPIClient.getStatus(from, to, providers, endpoints));
});

test.serial("Get /data/status actually returns something", async (t) => {
t.plan(1);
const statuses = (await StatusAPIClient.getStatus(from, to, providers, endpoints)).results;
const receivedProviders: string[] = [];
for (const status of statuses) {
status.providers.map((p) => receivedProviders.push(p.provider_id));
}
t.deepEqual(receivedProviders, providers);
});
9 changes: 9 additions & 0 deletions test/unit/auth.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,12 @@ test("Exchange code for token", async (t) => {
t.deepEqual(actual.access_token, expected.access_token, "Access token not as expected");
t.deepEqual(actual.refresh_token, expected.refresh_token, "Refresh token not as expected");
});

test("Get providers", async (t) => {
t.plan(2);
const stub = sinon.stub(request, "get").returns(JSON.stringify(fixtures.providersResponse))
const actual = await AuthAPIClient.getProviderInfos("oauth");
const expectedGetUri = "https://auth.truelayer.com/api/providers/oauth";
t.is(stub.getCall(0).args[0].uri, expectedGetUri);
t.deepEqual(actual, fixtures.providersResponse);
});
Loading