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

Translate source code to typescript #197

Merged
merged 18 commits into from
Sep 4, 2020

Conversation

mzgoddard
Copy link
Contributor

  • 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

@mzgoddard mzgoddard force-pushed the mzgoddard-typescriptIt branch 3 times, most recently from 6d753b0 to 97004bb Compare August 5, 2020 16:29
Copy link
Contributor

@rwaldron rwaldron left a 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;
Copy link
Contributor

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.

Copy link
Contributor Author

@mzgoddard mzgoddard Aug 10, 2020

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.

Copy link
Contributor

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 _

Copy link
Contributor Author

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?

Copy link
Contributor

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,
},
});
Copy link
Contributor

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
Copy link
Contributor

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 ;)

Copy link
Contributor Author

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('');
Copy link
Contributor

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}]` : ''}`;

?

Copy link
Contributor Author

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

message;
statusCode;

constructor(error, message, statusCode) {
Copy link
Contributor

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) {
Copy link
Contributor

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;
Copy link
Contributor

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) {
Copy link
Contributor

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?

@@ -1,31 +1,23 @@
'use strict';
import exponentialBackoffWithJitter from './exponential_backoff_with_jitter';
Copy link
Contributor

Choose a reason for hiding this comment

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

Space

Copy link
Contributor Author

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';
Copy link
Contributor

Choose a reason for hiding this comment

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

Array.isArray

@mzgoddard mzgoddard force-pushed the mzgoddard-typescriptIt branch from 7a18194 to a4626f6 Compare August 10, 2020 19:15
@mzgoddard
Copy link
Contributor Author

Rebased to fixup the gitignore conflict.

Copy link
Contributor

@SomeHats SomeHats left a 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.

package.json Outdated Show resolved Hide resolved
src/airtable_error.ts Outdated Show resolved Hide resolved
Comment on lines +28 to +39
readonly _airtable: Airtable;
readonly _id: string;
Copy link
Contributor

Choose a reason for hiding this comment

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

could these be private?

Copy link
Contributor Author

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.

Copy link
Contributor Author

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.

Copy link
Contributor Author

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.

#197 (comment)

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?

Copy link
Contributor

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!

Copy link
Contributor Author

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();
Copy link
Contributor

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 @@
{
Copy link
Contributor

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

Copy link
Contributor Author

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.

Copy link
Contributor Author

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.

Copy link
Contributor

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';
Copy link
Contributor

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?

Copy link
Contributor

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"

Copy link
Contributor

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.

Copy link
Contributor

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 👍

Comment on lines +37 to +39
readonly firstPage: RecordCollectionRequestMethod;
readonly eachPage: RecordPageIteratationMethod;
readonly all: RecordCollectionRequestMethod;
Copy link
Contributor

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?

Copy link
Contributor

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

Copy link
Contributor Author

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;
Copy link
Contributor

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;
Copy link
Contributor

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;
Copy link
Contributor

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 _

Copy link
Contributor

@SomeHats SomeHats left a 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)

@mzgoddard
Copy link
Contributor Author

  • Add "types" field to package json

Copy link
Contributor

@SomeHats SomeHats left a 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) {
Copy link
Contributor

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.

Copy link
Contributor

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.

Copy link
Contributor

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;
};

Copy link
Contributor

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 anys 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 on Base is it just undefined? should we remove it?

Copy link
Contributor

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;
};
Copy link
Contributor

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;
Copy link
Contributor

Choose a reason for hiding this comment

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

@SomeHats PTAL

rmeritz and others added 14 commits September 1, 2020 14:47
- 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
@rwaldron rwaldron force-pushed the mzgoddard-typescriptIt branch from cb8c3d4 to c7b74ca Compare September 1, 2020 18:48
@rwaldron
Copy link
Contributor

rwaldron commented Sep 1, 2020

I've just rebased this with master, so it will apply cleanly.

qs?: any;
headers?: any;
body?: any;
qs?: Record<string, any>;
Copy link
Contributor

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.

Copy link
Contributor

Choose a reason for hiding this comment

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

sounds good to me!

Copy link
Contributor

@SomeHats SomeHats left a 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

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.

4 participants