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

Conversation

victorfinbourne
Copy link

Pull Request Checklist

Description of the PR

Describe the code changes for the reviewers, explain the solution you have provided and how it fixes the issue

Comment on lines 3 to 27
/*
Read from Environment Variables
*/

// export var client = new Client(
// [Source.Environment, 'FBN_TOKEN_URL'],
// [Source.Environment, 'FBN_USERNAME'],
// [Source.Environment, 'FBN_PASSWORD'],
// [Source.Environment, 'FBN_CLIENT_ID'],
// [Source.Environment, 'FBN_CLIENT_SECRET'],
// [Source.Environment, 'FBN_LUSID_API_URL'],
// )

/*
Read from secrets.json
*/

export var client = new Client(
[Source.Environment, 'FBN_TOKEN_URL'],
[Source.Environment, 'FBN_USERNAME'],
[Source.Environment, 'FBN_PASSWORD'],
[Source.Environment, 'FBN_CLIENT_ID'],
[Source.Environment, 'FBN_CLIENT_SECRET'],
[Source.Environment, 'FBN_LUSID_API_URL'],
)
[Source.Secrets, 'tokenUrl'],
[Source.Secrets, 'username'],
[Source.Secrets, 'password'],
[Source.Secrets, 'clientId'],
[Source.Secrets, 'clientSecret'],
[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.

As this client runs the tests we need some way to ensure that we can run them locally using a secrets file and in our continuous integration pipeline which will use environment variables.

The logic in the Python SDK is to use a secrets file if provided, falling back to environment variables if it can't find a secret file. Do you think we could implement similar logic here?

https://github.com/finbourne/lusid-sdk-python-preview/tree/master/sdk/lusid/utilities

I would suggest that this lives in the sdk/client/client.ts but then you can delete completely the commented out code which references environment variables.

Copy link
Author

Choose a reason for hiding this comment

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

yep. will do.

sdk/client/client.ts Outdated Show resolved Hide resolved
sdk/client/client.ts Outdated Show resolved Hide resolved
Copy link
Member

@MikeMcGarry MikeMcGarry left a comment

Choose a reason for hiding this comment

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

Hey Victor,

Great work, looking much more loved than before!

Overall we should have a chat about the semantics of Node.JS Typescript vs Python to think about the best way to achieve parity while maintaining the language specific flows.

I've made some comments in-line as well.

// 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.

// 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?

Comment on lines 137 to 146
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

Comment on lines 154 to 175
{
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?

Comment on lines 182 to 185
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?


Object.keys( res.values ).forEach( ( insertedInstrument ) => {

mlog.log( `Upserted instrument ${insertedInstrument} -> ${res.values[ insertedInstrument ].lusidInstrumentId}`)
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 assert that the response contains the instruments that we expect?

Copy link
Member

Choose a reason for hiding this comment

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

Or is that outside the scope of this test as it is just something we need to run the holding tests? If so, should the functions for creating these live in a utilities folder instead of inside this test file?


this.portfolioObject = res;

mlog.log( `Created portfolio ${res.displayName}` );
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 assert that the displayName and other values are as we expect?

Copy link
Member

Choose a reason for hiding this comment

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

Or is that outside the scope of this test as it is just something we need to run the holding tests? If so, should the functions for creating these live in a utilities folder instead of inside this test file?

Comment on lines 439 to 440
portfolioObject: this.portfolioObject,
instrumentsObject: this.instrumentsObject,
Copy link
Member

Choose a reason for hiding this comment

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

Nice how you grab these from the testcase using this

Comment on lines 62 to 96
const createTransactionPortfolio = (
{
scope
}: {
scope: string,
}
) :Promise<{
portfolio:Portfolio;
createRequest:CreateTransactionPortfolioRequest}
> => {

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

const
createRequest = Object.assign(
new CreateTransactionPortfolioRequest(),
{
displayName: "UK Equities",
code: "UKEQTY",
baseCurrency: "GBP",
created: moment([2018, 1, 1, 0, 0, 0]).utc()
}
);

client.api.transactionPortfolios.createPortfolio(
scope,
createRequest
)
.then((res: {response: IncomingMessage; body: Portfolio}) => {
resolve( { portfolio: res.body, createRequest } )
})
.catch((err: {response: IncomingMessage; body: LusidProblemDetails}) => reject(err))

} );
}
Copy link
Member

Choose a reason for hiding this comment

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

Seeing some duplication here between this function and the one in the getholdings tests

Comment on lines 166 to 202
const upsertInstruments = (
identifiers: Object = {}
) :Promise<UpsertInstrumentsResponse> => {

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

client.api.instruments.upsertInstruments(

Object.keys( identifiers ).reduce( ( accumulator, currentValue ) => {

accumulator[ currentValue ] = Object.assign(
new InstrumentDefinition(),
{
name: identifiers[ currentValue ].Name,
identifiers: {
Figi: Object.assign(
new InstrumentIdValue(),
{
value: currentValue
}
)
}
}
);

return accumulator;

}, {} )
)
.then((res: {response: IncomingMessage; body: UpsertInstrumentsResponse}) => {
resolve( res.body )
})
.catch((err: {response: IncomingMessage; body: LusidProblemDetails}) => reject(err));

} );

}
Copy link
Member

Choose a reason for hiding this comment

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

Duplication of the function between this file and getholdings?

public clientId: string
public clientSecret: string
// polyfill for Object.fromEntries until Typescript has ES2019 included
const _ObjectFromEntries = ( iter ): Object => {
Copy link
Member

Choose a reason for hiding this comment

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

I understand that this is used to convert an array of key, value pairs into an object.

Some questions that someone reading this function might have and could be worth answering with a JSDoc compliant docstring:

  • What is the input exactly for this function?
  • What happens with nested objects?
  • What are you checking with the equality check if (Object(pair) !== pair)?

// Consistency with Map: contract is that entry has "0" and "1" keys, not
// that it is an array or iterable.

const { '0': key, '1': val } = pair;
Copy link
Member

Choose a reason for hiding this comment

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

What is the significance of this line?

Comment on lines +55 to +57
configurable: true,
enumerable: true,
writable: true,
Copy link
Member

Choose a reason for hiding this comment

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

What would happen if you didn't set these to true?


secretsFileContent: Object

private configurationMapping: Array<Array<number|Array<string>>> = [
Copy link
Member

Choose a reason for hiding this comment

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

When would the nested array be an array of numbers? Is that in the case of the Enum? If so why not Array<Array<Source|Array<string>>>

Also is this saying that the nested array will either be a flat Array of numbers or another nested array which contains strings? Or is it saying that it will be a flat array of either numbers or strings.


try
{
return this.secretsFileContent = require( !!secretsFilePath ? secretsFilePath : this.configuration.secretsFilePath ).api;
Copy link
Member

Choose a reason for hiding this comment

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

Is falling back to this.configuration.secretsFilePath redundant as you do this on lines 221 to 223? Or do you expect this to be used in another context inside this class where you aren't confident that secretsFilePath will have already been populated with a sensible default?

Comment on lines +278 to +281
if(
typeof this.api[apiName][prop] !== 'function'
|| ['setDefaultAuthentication', 'setApiKey'].includes( prop )
) {
Copy link
Member

Choose a reason for hiding this comment

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

Nice, much neater :)

sdk/client/client.ts Show resolved Hide resolved
sdk/client/client.ts Show resolved Hide resolved
Comment on lines +229 to +231
this.configuration.tokenUrlDetails = !!tokenUrlDetails
? this.fetchConfigurationItem( Source.Raw, tokenUrlDetails, true )
: this.fetchConfigurationItem( Source.Secrets, 'tokenUrl', false );
Copy link
Member

Choose a reason for hiding this comment

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

In this instance, if I provide in tokenUrlDetails when creating my client 'tokenUrl' then won't this actually use the value of 'tokenUrl' as my tokenUrl?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants