Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/readsecrets + brief readme #6

Open
wants to merge 41 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
e2bd89c
quick fix
Apr 3, 2020
c71e92f
added comments
Apr 3, 2020
033358e
added absolute path
Apr 3, 2020
c9f9732
added some usage docs
Apr 3, 2020
e6c7ac4
cascading secrets
Apr 3, 2020
8e895b8
optional client constructor parameters
Apr 3, 2020
3220f70
better error reporting
Apr 3, 2020
923bc00
removed superflous commentary
Apr 3, 2020
c37c1d8
added portfolio delete test + holdings test inception
Apr 3, 2020
158f1c8
ES6 upgrades + nicer logging
Apr 3, 2020
ba45d72
holdings interim
Apr 3, 2020
dc50d15
holdings interim
Apr 5, 2020
0e890ba
holdings assertions alpha
Apr 5, 2020
970cc0f
removal of unused reference
Apr 5, 2020
6144dd0
lusidTools inception + defineBuyRequest
Apr 6, 2020
7ff33c9
getHoldings beta
Apr 6, 2020
6656321
adjust holdings beta
Apr 6, 2020
bc466bd
delete back in test
Apr 6, 2020
84c4f30
portfolios beta
Apr 6, 2020
11b624a
better title
Apr 6, 2020
ade1a80
made error reporting broader
Apr 6, 2020
d25862c
broader error reporting
Apr 6, 2020
7ccd1f3
removed unused libraries
Apr 6, 2020
90ab2fc
defaults update
Apr 6, 2020
1986cd1
fallback beta
Apr 7, 2020
581abff
configuration
Apr 7, 2020
de7e7b7
removed comments
Apr 7, 2020
5a144d8
+comments
Apr 7, 2020
63a7fb0
explicit fallbacks
Apr 7, 2020
2e7a030
oauth2
Apr 7, 2020
d31c5ab
lusidUtilities
Apr 7, 2020
b310b89
elegant update
Apr 7, 2020
38b4024
fix docs
Apr 7, 2020
c44409f
one transaction test
Apr 7, 2020
abefcf1
improvements
Apr 8, 2020
d872778
createTransactions beta
Apr 8, 2020
bbb0e73
bitemporal alpha
Apr 8, 2020
a226e38
bitemporal with JS patch
Apr 8, 2020
3ef9303
cut labels alpha
Apr 8, 2020
c71e870
cut labels alpha
Apr 9, 2020
7da1325
cut labels alpha
Apr 9, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,23 @@ If you would prefer to generate the Javsscript SDK locally from the FINBOURNE Op
- download the latest swagger.json file from https://api.lusid.com/swagger/index.html
- save it in this directory as lusid.json
- run `$ docker-compose up && docker-compose rm -f`

# Working with the SDK

The relevant tests are in sdk/test/tutorials

You need to either create a sdk/secrets.json file, or set environment variables, based on your lusid installation. The configurations go into sdk/test/tutorials/clientBuilder.ts

To generate the JS based on TS

```
$ cd sdk/
$ npm i
$ npm run-script build
```

To run all scripts in sdk/test/tutorials

```
$ npm test
```
2 changes: 1 addition & 1 deletion sdk/api/instrumentsApi.ts

Large diffs are not rendered by default.

245 changes: 145 additions & 100 deletions sdk/client/client.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// Require the libraries
import localVarRequest from 'request';
import querystring from 'querystring'

import querystring from 'querystring';

// Import a list of the LUSID APIs
const lusid = require('../api');
Expand All @@ -15,12 +14,6 @@ It contains a property for each of the LUSID APIs.
*/
import {Api} from "../apis";


// Set the default path to the secrets file
const secretsPath = './secrets.json'
// Set the default amount of seconds before token expiry to call a refresh
const refreshLimit = 3580

/*
To authenticate with a third party identity provider, a number of credentials
are required e.g. username, password, clientId, clientSecret etc.
Expand Down Expand Up @@ -124,39 +117,98 @@ export class Client {
basePath: string
// The available API endpoints
api: Api

// The path to the secrets file which may be used to store credentials
secretsFilePath: string
secretsFilePath: string = process.cwd() + '/secrets.json'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that we are looking at this in detail, do we think the "secrets.json" path should live in the client class or should the file path be passed into the constructor? This allows any file path to be specified when creating a client.

secretsFileContent: { api: {} }

// The credential access details
private tokenUrlDetails: [Source, string]
private usernameDetails: [Source, string]
private passwordDetails: [Source, string]
private clientIdDetails: [Source, string]
private clientSecretDetails: [Source, string]

// The refresh limit in seconds before token expiry to trigger a refresh
refreshLimit: number = refreshLimit
refreshLimit: number = 3580
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we also allow providing a custom refreshLimit with this set as a sensible default?


private loadSecretsFile(): object | null {

try
{
this.secretsFileContent = require( this.secretsFilePath );
}
catch( e )
{
// not necessarily a problem. even if the format of the source file is bad, this will reset the internal validation to an empty configuration

this.secretsFileContent = { api: {} };
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What are your thoughts on try/catch handling vs if/then handling? We could check that the file exists and if it does not raise a warning rather than silently defaulting to a default empty set of values.

I'm not sure which is better suited to Typescript and more specifically Node.js


return this.secretsFileContent;

}

// Constructor method which takes the details on where to find the credentials
constructor(
tokenUrlDetails: [Source, string],
usernameDetails: [Source, string],
passwordDetails: [Source, string],
clientIdDetails: [Source, string],
clientSecretDetails: [Source, string],
apiUrlDetails: [Source, string],
{
tokenUrlDetails,
usernameDetails,
passwordDetails,
clientIdDetails,
clientSecretDetails,
apiUrlDetails
}: {
tokenUrlDetails?: [Source, string],
usernameDetails?: [Source, string],
passwordDetails?: [Source, string],
clientIdDetails?: [Source, string],
clientSecretDetails?: [Source, string],
apiUrlDetails?: [Source, string],
} = {
tokenUrlDetails: [Source.Secrets, 'tokenUrl'],
usernameDetails: [Source.Secrets, 'username'],
passwordDetails: [Source.Secrets, 'password'],
clientIdDetails: [Source.Secrets, 'clientId'],
clientSecretDetails: [Source.Secrets, 'clientSecret'],
apiUrlDetails: [Source.Secrets, 'apiUrl']
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is interesting! What are the benefits to this over having the types in the constructor as there were previously?

) {

// Set the path to the secrets file
this.secretsFilePath = secretsPath

// Set the credential details
this.tokenUrlDetails = tokenUrlDetails
this.usernameDetails = usernameDetails
this.passwordDetails = passwordDetails
this.clientIdDetails = clientIdDetails
this.clientSecretDetails = clientSecretDetails

// Set the base path for the API
this.basePath = this.fetchCredentials(apiUrlDetails[0], apiUrlDetails[1])
// attempt to get disk config by default. if it doesn't exist it is still fine
this.loadSecretsFile();

// provide init + default to environment vars below
this.tokenUrlDetails = !!tokenUrlDetails ? tokenUrlDetails : (
// [Source.Environment, 'FBN_TOKEN_URL']
[Source.Secrets, 'tokenUrl']
);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the defaults are already specified in the constructor above in what situation would !!tokenUrlDetails resolve to False? In that situation what is the value of providing the same details "[Source.Secrets, 'tokenUrl']" as the default?


this.usernameDetails = !!usernameDetails ? usernameDetails : (
// [Source.Environment, 'FBN_USERNAME']
[Source.Secrets, 'username']
);

this.passwordDetails = !!passwordDetails ? passwordDetails : (
// [Source.Environment, 'FBN_PASSWORD']
[Source.Secrets, 'password']
);

this.clientIdDetails = !!clientIdDetails ? clientIdDetails : (
// [Source.Environment, 'FBN_CLIENT_ID']
[Source.Secrets, 'clientId']
);

this.clientSecretDetails = !!clientSecretDetails ? clientSecretDetails : (
// [Source.Environment, 'FBN_CLIENT_SECRET']
[Source.Secrets, 'clientSecret']
);

// initialize base path at this moment, since it is needed below
this.basePath = !!apiUrlDetails ? this.fetchConfigurationItem( apiUrlDetails[0], apiUrlDetails[1] ) : (
// this.fetchConfigurationItem( Source.Environment, 'FBN_CLIENT_SECRET' )
this.fetchConfigurationItem( Source.Secrets, 'apiUrl' )
);

// Set the authentications to use oauth2
this.authentications = {'oauth2': new Oauth2(undefined, 0,0,0,0)}
Expand All @@ -183,8 +235,8 @@ export class Client {
// Wrap each method with token refresh logic
this.api[apiName][prop] = this.apiFunctionWrapper(
this.api[apiName][prop],
this.api[apiName],
this)
this.api[apiName]
)
}

}
Expand All @@ -196,91 +248,83 @@ export class Client {
The function below is a wrapper function which wraps the input function
'apiFunction' with token refresh logic to ensure uninterrupted access to LUSID.
*/
public apiFunctionWrapper(apiFunction, api, self) {

// Return a function
return function() {
// Collect the arguments passed into the wrapper to use later
var topLevelArguments = arguments
// Return a promise to ensure that the function remains 'thenable'
return new Promise(function(resolve, reject) {
private apiFunctionWrapper(apiFunction, api ) {

// Return a function, thus not immediately invoking

return ( ...args ) => {
MikeMcGarry marked this conversation as resolved.
Show resolved Hide resolved

// Return a promise to ensure that the function remains '.then()-able'
return new Promise( (resolve, reject) => {
MikeMcGarry marked this conversation as resolved.
Show resolved Hide resolved

// Trigger a token refresh
var oauthPopulated = self.refreshToken(
self.authentications['oauth2'],
self.refreshLimit,
self.tokenUrlDetails,
self.usernameDetails,
self.passwordDetails,
self.clientIdDetails,
self.clientSecretDetails
)
// Once this has completed pass the oauth2 details on
oauthPopulated.then(function(oauth2Details: Oauth2) {
this.refreshToken(
this.authentications.oauth2,
this.refreshLimit,
this.tokenUrlDetails,
this.usernameDetails,
this.passwordDetails,
this.clientIdDetails,
this.clientSecretDetails
).then( (oauth2Details: Oauth2) => {

// Update the clients oauth2 details
self.authentications.oauth2 = oauth2Details
this.authentications.oauth2 = oauth2Details
// Update the access token of the api being called
api['authentications']['oauth2']['accessToken'] = self.authentications.oauth2.accessToken
api.authentications.oauth2.accessToken = this.authentications.oauth2.accessToken

/*
Resolve the promise with the function that was wrapped
In this case api is the api that this function is a part of,
this is required to ensure that the function is called
in the right context. The second argument topLevelArguments
is the arguments passed into the Wrapper
*/
resolve(apiFunction.apply(api, topLevelArguments))

resolve( apiFunction.apply( api, args ) );

})
// Error handling
.catch((err) => reject(err))
})
}
}

/*
The fetchCredentials function gets the credentials from the specified source
*/
private fetchCredentials(source: Source, value: string): string {

// Environment source
if (source == Source.Environment) {
// Set the credential using an environment variable
var credential: string = process.env[value] || 'MISSING'
// If it the environment variable does not exist then throw error
if (credential == 'MISSING') {
throw "Environment variable " + value + " has not been specified"
}
private fetchConfigurationItem( sourceType: Source, itemName: string ): string {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice function! I like that it is not requiring the secrets file for each item as it was earlier.


// Raw or variable source
} else if (source == Source.Raw) {
// Set the credential directly
var credential: string = value

// Secrets source
} else if (source = Source.Secrets) {
// Try and import the credentials file
try {
var credentials: any = require(this.secretsFilePath)
// If there are any problems throw an error
} catch (e) {
throw "File " + this.secretsFilePath + " could not be found/read, ensure that it exists in the client folder"
}
// If the property exists in the secrets file set the credential
if (credentials.hasOwnProperty(value)) {
var credential: string = credentials[value]
} else {
throw "The specified value " + value + " does not exist in the file"
}
switch( sourceType )
{
case Source.Environment:

if( !!process.env[ itemName ] )
{
return process.env[ itemName ];
}

throw `Environment variable ${itemName} has not been specified`;

break;
case Source.Raw:

return itemName;

break;
case Source.Secrets:

if( !!this.secretsFileContent.api[ itemName ] )
{
return this.secretsFileContent.api[ itemName ];
}

throw `Configuration item ${itemName} not found in secrets file`;

break;
default:

throw `Source is not valid, must be one of ${Object.keys( Source ).join( ", " )}`;

// If the source is incorrectly specified
} else {
let availableSources: string[]
for (var availableSource in Source) {
availableSources.push(availableSource)
}
throw "Source is not valid, must be one of " + availableSources.toString()
}

// Return the credential
return credential
}

/*
Expand Down Expand Up @@ -313,11 +357,11 @@ export class Client {

// If so, populate the credentials
var credentials = new Credentials(
this.fetchCredentials(tokenUrlDetails[0], tokenUrlDetails[1]),
this.fetchCredentials(usernameDetails[0], usernameDetails[1]),
this.fetchCredentials(passwordDetails[0], passwordDetails[1]),
this.fetchCredentials(clientIdDetails[0], clientIdDetails[1]),
this.fetchCredentials(clientSecretDetails[0], clientSecretDetails[1])
this.fetchConfigurationItem(tokenUrlDetails[0], tokenUrlDetails[1]),
this.fetchConfigurationItem(usernameDetails[0], usernameDetails[1]),
this.fetchConfigurationItem(passwordDetails[0], passwordDetails[1]),
this.fetchConfigurationItem(clientIdDetails[0], clientIdDetails[1]),
this.fetchConfigurationItem(clientSecretDetails[0], clientSecretDetails[1])
)

// Get a new access token using these credentials
Expand Down Expand Up @@ -385,7 +429,8 @@ export class Client {
clientId: string,
clientSecret: string
): Promise<Oauth2> {
// Returns a rpomise

// Returns a promise

return new Promise((resolve, reject) => {

Expand Down