-
Notifications
You must be signed in to change notification settings - Fork 407
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
Translate source code to typescript #197
Conversation
mzgoddard
commented
Jul 28, 2020
- Use grunt-ts to transpile typescript to /lib directory for publishing to npm
- Translate function syntax classes and commonjs modules to class syntax classes and es6 modules
- Add types to public interfaces
- Add types to parameters for Query class
- Add overloaded method types to bound methods with callbackToPromise
6d753b0
to
97004bb
Compare
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.
First review done! Looks great!
readonly _endpointUrl: string; | ||
readonly _apiVersion: string; | ||
readonly _apiVersionMajor: string; | ||
readonly _noRetryIfRateLimited: boolean; |
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 there a legacy requirement to keep these named with _
? This seems like a good use case for private fields, but I can understand not using them if some other code somewhere else is somehow relying on them.
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.
One can argue otherwise but I consider that a backwards incompatible change. I suppose it might be a minor breaking change instead of major breaking change. But I want to avoid any mechanical change in this changeset.
Beyond this PR, I agree, switching to private fields would be good here.
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.
we should mark these as private
, but our style guide still encourages prefixing private methods with _
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.
Marking these as private
creates typescript errors in base.ts
.
Property '_endpointUrl' is private and only accessible within class 'Airtable'. ts(2341)
So we can't mark these properties in airtable.ts
as private
without changing the executed relationship in base.ts
. That probably means some kind of API refactor?
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.
sounds good - we can leave these as-is for now and maybe address in a follow-up
Airtable.noRetryIfRateLimited || | ||
defaultConfig.noRetryIfRateLimited, | ||
}, | ||
}); |
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.
Continuing from my last question, if you chose to use private fields, this would be simplified.
apiVersion: '0.1.0', | ||
apiKey: process.env.AIRTABLE_API_KEY, | ||
noRetryIfRateLimited: false, | ||
requestTimeout: 300 * 1000, // 5 minutes |
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.
Does TS support numeric separators? Asking for a friend. 300_000
;)
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.
Hmm. Not sure. If I had changed this line (instead of the automated changes made by lebab and prettier) I'd probably make this 5 * 60 * 1000
. Or change the line to 300_000, // 300,000 milliseconds (5 minutes)
with your recommendation.
this.error, | ||
')', | ||
this.statusCode ? `[Http code ${this.statusCode}]` : '', | ||
].join(''); |
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.message} (${this.error}) ${this.statusCode ? `[Http code ${this.statusCode}]` : ''}`;
?
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 had that same thought when I was making changes. But I don't want to change behaviour in this PR. (I really wanted to change the fetch inverted promise chain in another file.)
src/airtable_error.ts
Outdated
message; | ||
statusCode; | ||
|
||
constructor(error, message, statusCode) { |
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 there are legacy reason for this parameter list?
throw new Error('The first parameter to `eachPage` must be a function'); | ||
} | ||
|
||
if (!isFunction(done) && done !== void 0) { |
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 has the same behavior as the parameter I pointed out previously.
src/query.ts
Outdated
throw new Error('The second parameter to `eachPage` must be a function or undefined'); | ||
} | ||
|
||
const that = this; |
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.
Unnecessary when all occurrences of this
are in arrow functions
var AbortController = require('./abort-controller'); | ||
|
||
var userAgent = 'Airtable.js/' + packageVersion; | ||
const userAgent = `Airtable.js/${packageVersion}`; | ||
|
||
function runAction(base, method, path, queryParams, bodyData, callback, numAttempts) { |
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.
Can this be an async function?
src/run_action.ts
Outdated
@@ -1,31 +1,23 @@ | |||
'use strict'; | |||
import exponentialBackoffWithJitter from './exponential_backoff_with_jitter'; |
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.
Space
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.
How'd that get there?
@@ -0,0 +1,24 @@ | |||
import includes from 'lodash/includes'; | |||
import isArray from 'lodash/isArray'; |
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.
Array.isArray
7a18194
to
a4626f6
Compare
Rebased to fixup the gitignore conflict. |
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.
Thanks so much for this! And thanks @rwaldron for the review too.
Generally this looks great! I've left a few comments and questions particularly on areas where we can make changes with confidence that we're not changing runtime semantics at all. For the other refactors & cleanups (inc. reducing our reliance on lodash where built-ins would suffice), i've left some suggestions but would rather we handle those in a follow-up PR as this diff is already pretty large.
readonly _airtable: Airtable; | ||
readonly _id: 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.
could these be private
?
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.
Making them private
produces a typescript error because code outside of the class modifies these values currently.
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.
Ah, wait. No. That's another file I'm thinking about. We can make these properties private.
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.
So we can't mark the _
properties in airtable.ts
private
without some kind of refactor in airtable.ts
and base.ts
.
But we can mark the _
prefixed properties of base.ts
private
without changing how any js executes. Personally I agree we should mark the members in base.ts
as private, however even without changing any executing source, I think that will make this change a major api breaking change for users using typescript and airtable.js if they are also accessing those members already. This seems like a small detail but should we add private
to these members as a part of the follow ups?
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.
given we don't currently provide typescript types, i don't know that just marking them private
constitutes a breaking change. happy to leave as-is and address in a follow-up though!
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.
happy to leave as-is and address in a follow-up though!
That's my thought.
|
||
options = options || {}; | ||
|
||
const method = get(options, 'method', 'GET').toUpperCase(); |
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.
potential follow-up: Do we still need lodash's get
here? Would options.method ?? 'GET'
work instead? ditto for other places i see it used in this project
@@ -0,0 +1,13 @@ | |||
{ |
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.
could we specify "lib": "ES5"
in here too? right now, we this includes dom types, which might not be available in some peoples typescript projects, and i'm worried that us referencing things like RequestInit
could cause type issues in other people's projects
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.
Hmm. We'll need to add import 'typescript/lib/lib.dom';
to any files in these packages using fetch types.
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'm not sure if thats the right way to do that. That may import dom types globally for all files. As in it may pollute the files of the user's files that are depending on airtable.js
.
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.
let's leave as-is for now, we can look at explicitly importing the types we need for fetch
in a follow-up. i'm not even sure this'll cause problems tbh
@@ -0,0 +1,6 @@ | |||
import nodeFetch from 'node-fetch'; |
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.
potential follow-up: what it we replaced this file with isomorphic-fetch
?
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.
@SomeHats while doing due diligence on this, I note that:
- isomorphic-fetch:
- was last published 5 years ago
- has > 4.5M downloads per week
- 16 open PRs (the newest is over a year old)
- 61 open issues and the newest is "Is this library still been maintained?" 🤣
- node-fetch
- was last published 22 days ago
- has >16M downloads per week
- PR queue is active
- Issue queue is active
I don't think it's in Airtable.js's best interest to migrate to what appears to be an unmaintained package, so I'd like to close this "won't fix"
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.
fwiw, isomorphic-fetch just re-exports node-fetch (in node) or a polyfill/browser-builtins (in the browser). node-fetch actually references isomorphic-fetch (and another maybe more suitable option) in its docs. i'm not fussed about the specific thing we use here though - the main thing i'm interested in is that for browser users, we don't include all of node-fetch
in the bundle unnecessarily.
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.
Gotcha! That should be satisfied by the browserify
operation that will omit node-fetch
since its value is false
in package.json's browser
property:
"browser": {
"node-fetch": false,
"abort-controller": false,
"./lib/package_version": "./lib/package_version_browser"
},
I checked airtable.browser.js
and verified that node-fetch
has an empty module entry 👍
readonly firstPage: RecordCollectionRequestMethod; | ||
readonly eachPage: RecordPageIteratationMethod; | ||
readonly all: RecordCollectionRequestMethod; |
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.
out of curiosity, why this approach vs. standard typescript method overloading?
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.
oh i guess to support callbackToPromise
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.
Yeah. I couldn't think of a more standard typescript approach without removing the use of callbackToPromise.
src/record.ts
Outdated
} | ||
|
||
function patchUpdate(this: Record, cellValuesByName, opts, done?: RecordCallback) { | ||
const that = this; |
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.
because these now use arrow functions, i believe that this 'that this' (????? these words are hard to talk about lol) is no longer needed(ditto for elsewhere in this file/project)
src/table.ts
Outdated
done: RecordCollectionCallback | ||
): void; | ||
_createRecords(recordsData, optionalParameters, done?) { | ||
const that = this; |
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.
ditto re: that
/this
readonly _endpointUrl: string; | ||
readonly _apiVersion: string; | ||
readonly _apiVersionMajor: string; | ||
readonly _noRetryIfRateLimited: boolean; |
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.
we should mark these as private
, but our style guide still encourages prefixing private methods with _
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.
thanks for the updates! this looks all-good to me (pending that package.json update for the type definitions)
|
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 folks! Finally got around to running the integration tests for this repo - sorry for the delay! There are a few issues with the types, but i didn't see any actual functional errors. I left an inline comment on the main type issue (a bad inferred return type) but could we potentially go through this diff and make sure that every method has its arguments and return value correctly typed? could also introduce some lint rules enforcing this as part of this diff
src/base.ts
Outdated
return this._id; | ||
} | ||
|
||
static createFunctor(airtable: Airtable, baseId: 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.
Can we define a return type for this that includes all the correct methods and properties? things like new Airtable().base(someId).table(someTable)
are errors otherwise.
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.
@SomeHats I'm working on this now, and something I ran into that I can see only one way around. When I define a type that includes the methods that are added below in the forEach(['table', 'makeRequest', 'runAction', 'getId'], ...
operation, they are not recognized. When I define them as standalone assignments (statically analyzable), then it all works out. The only issue is that it's not very nice to look at.
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.
Here's the type I defined:
type AirtableBase = {
(tableName: any): Table;
_base: Base;
getId(): string;
makeRequest(options: BaseRequestOptions): Promise<Record<string, any>>;
table(tableName: string): Table;
tables: any;
};
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 if you turn line 218 into return baseFn as AirtableBase
? that should let us keep the correct types for the return value and not have to worry too much about how it's constructed.
re: that AirtableBase
type, a few questions:
- can we replace all the
any
s with more specific types? - can we get rid of
_base
from the public type definition? if no that's fine - any idea what's up with
tables
? i don't see it defined onBase
is it justundefined
? should we remove it?
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 if you turn line 218 into return baseFn as AirtableBase? that should let us keep the correct types for the return value and not have to worry too much about how it's constructed.
Smart—that does the trick for the existing forEach(['table', 'makeRequest', 'runAction', 'getId'], ...
operation.
re: that AirtableBase type, a few questions:
can we replace all the anys with more specific types?
Sure can!
can we get rid of _base from the public type definition? if no that's fine
Unfortunately, when I remove that property it produces errors.
any idea what's up with tables? i don't see it defined on Base is it just undefined? should we remove it?
Digging around and I learned that there is no other reference to this property anywhere else in the codebase—I'm going to nuke it.
getId(): string; | ||
makeRequest(options: BaseRequestOptions): Promise<BaseResponse>; | ||
table(tableName: string): Table; | ||
}; |
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.
@SomeHats PTAL
@@ -214,8 +222,7 @@ class Base { | |||
baseFn[baseMethod] = base[baseMethod].bind(base); | |||
}); | |||
baseFn._base = base; | |||
baseFn.tables = base['tables']; | |||
return baseFn; | |||
return baseFn as AirtableBase; |
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.
@SomeHats PTAL
- Use grunt-ts with recommended minimal config: https://github.com/TypeStrong/grunt-ts#getting-started - Switch all filetypes to `ts` - Ignore all warnings and imply all types
- eslint only looks at ts files by default. Add ts files to files reviewed. - Add recommened tslint pluggin - Switch all linting errors to warnings for now. Will go back and switch them to warnings at a future commit.
- Change the path of the required files in the tests to test the build files - Change the test runner to compile the typescript prior to running the tests - Change the test runner to only run the tests once (coverage also runs the tests) - Pass --no-cache to coverage to ensure it works with typescript: kulshekhar/ts-jest#211 - Stop using `grunt-ts` because it doesn't actually just pass the config through tsc. This caused problems when the new resolveJsonModule config didn't get passed through. Instead just use `tsc` to compile directly. - Modify the `tsconfig` to ensure that json files are included in the compiled package. - One line of code is no longer coverage for mysterious reasons, punt on figuring out why and just ignore it so they test continue to pass.
Source the files from the compiled js instead of the source typescript. At this point grunt doesn't seem to be adding much value because I ended up adding the task in package.json directly but removing it is out of scope.
- This is a more traditional location and it fixes problems with the location of package.json
- type param validators and by extensions the params to Query - type bound methods with callbackToPromise with overloaded interfaces
/lib/ is tsc's configured output now. /lib/ is not longer committed to git.
- do not lint build directory lib/* - output source maps from typescript files for coverage testing - fix a istanbul ignore in fetch.ts
cb8c3d4
to
c7b74ca
Compare
I've just rebased this with master, so it will apply cleanly. |
qs?: any; | ||
headers?: any; | ||
body?: any; | ||
qs?: Record<string, any>; |
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.
@SomeHats in Airtable/Base: Pass custom headers to requests made through the client, I've introduced the ObjectMap
we discussed in Slack. Once this PR lands and then that PR lands, we can come back use ObjectMap
here as well.
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.
sounds good to me!
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.
great stuff! integration tests are all passing, i'll look at getting this landed soon :D