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
base: master
Are you sure you want to change the base?
Feature/readsecrets + brief readme #6
Conversation
sdk/test/tutorials/clientBuilder.ts
Outdated
/* | ||
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'] | ||
) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yep. will do.
There was a problem hiding this 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.
sdk/client/client.ts
Outdated
// The path to the secrets file which may be used to store credentials | ||
secretsFilePath: string | ||
secretsFilePath: string = process.cwd() + '/secrets.json' |
There was a problem hiding this comment.
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.
sdk/client/client.ts
Outdated
// The refresh limit in seconds before token expiry to trigger a refresh | ||
refreshLimit: number = refreshLimit | ||
refreshLimit: number = 3580 |
There was a problem hiding this comment.
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?
sdk/client/client.ts
Outdated
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: {} }; | ||
} |
There was a problem hiding this comment.
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
sdk/client/client.ts
Outdated
{ | ||
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'] | ||
} |
There was a problem hiding this comment.
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?
sdk/client/client.ts
Outdated
this.tokenUrlDetails = !!tokenUrlDetails ? tokenUrlDetails : ( | ||
// [Source.Environment, 'FBN_TOKEN_URL'] | ||
[Source.Secrets, 'tokenUrl'] | ||
); |
There was a problem hiding this comment.
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}`) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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}` ); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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?
portfolioObject: this.portfolioObject, | ||
instrumentsObject: this.instrumentsObject, |
There was a problem hiding this comment.
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
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)) | ||
|
||
} ); | ||
} |
There was a problem hiding this comment.
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
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)); | ||
|
||
} ); | ||
|
||
} |
There was a problem hiding this comment.
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 => { |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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?
configurable: true, | ||
enumerable: true, | ||
writable: true, |
There was a problem hiding this comment.
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?
sdk/client/client.ts
Outdated
|
||
secretsFileContent: Object | ||
|
||
private configurationMapping: Array<Array<number|Array<string>>> = [ |
There was a problem hiding this comment.
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.
sdk/client/client.ts
Outdated
|
||
try | ||
{ | ||
return this.secretsFileContent = require( !!secretsFilePath ? secretsFilePath : this.configuration.secretsFilePath ).api; |
There was a problem hiding this comment.
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?
if( | ||
typeof this.api[apiName][prop] !== 'function' | ||
|| ['setDefaultAuthentication', 'setApiKey'].includes( prop ) | ||
) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice, much neater :)
this.configuration.tokenUrlDetails = !!tokenUrlDetails | ||
? this.fetchConfigurationItem( Source.Raw, tokenUrlDetails, true ) | ||
: this.fetchConfigurationItem( Source.Secrets, 'tokenUrl', false ); |
There was a problem hiding this comment.
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?
Pull Request Checklist
develop
branchDescription of the PR
Describe the code changes for the reviewers, explain the solution you have provided and how it fixes the issue