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

Finish TypeScript Migration #512

Merged
merged 42 commits into from Feb 22, 2019
Merged

Conversation

schmidt-sebastian
Copy link
Contributor

@schmidt-sebastian schmidt-sebastian commented Dec 29, 2018

This PR finishes the Typescript migration of the Firestore Server SDK. As part of this:

  • I changed the Validator class to use free-standing functions. This gets rid of the dynamic method creation, which doesn't really work with strong typing
  • I changed the client to use unknown instead of any for the user-facing types (only internally, not in the types we shipped)
  • I brought the client more in line with the Web SDK and only support the direction/query operator spelling that is outlined in the type definition (for example, "ASC" no longer works). This lets me use a common property validation function (and is the only breaking change)
  • I removed the dependency on is to be able to use TypeScript's typechecking.

This is the PR that I am planning on merging, and it's also the PR that I am would like to address all feedback in (to reduce merge conflicts). Since it is not very easy to review, I split this out into a series of PRs that are more tightly scoped:

The individual PRs are not meant to compile/pass tests. There should not be anything in this larger PR that is not listed above.

This is fairly low priority, so please take your time to review.

Fixes: b/117464450 and b/119350730

@googlebot googlebot added the cla: yes This human has signed the Contributor License Agreement. label Dec 29, 2018
@schmidt-sebastian schmidt-sebastian changed the title Finish TypeScript Migration (BREAKING CHANGE Finish TypeScript Migration (BREAKING CHANGE) Dec 29, 2018
@schmidt-sebastian
Copy link
Contributor Author

schmidt-sebastian commented Dec 29, 2018

@JustinBeckwith / Node team:

This PR fails CI with the following errors (re unknown[] usage):

/Users/mrschmidt/GitHub/googleapis/nodejs-firestore/dev/src/serializer.ts Array type using 'T[]' is forbidden for non-simple types. Use 'Array<T>' instead. (array-type)

Note that gts check passes (gts --version reports 0.8.0, even though 0.9.0 is installed), but npm run gts check -- as run by the CI -- does not (npm rum gts --version reports 6.2.0). gts fix actually converts Array<unknown> to unknown[].

@JustinBeckwith JustinBeckwith added the 🚨 This issue needs some love. label Feb 7, 2019
@yoshi-automation yoshi-automation added 🚨 This issue needs some love. and removed 🚨 This issue needs some love. labels Feb 7, 2019
@JustinBeckwith JustinBeckwith added the needs work This is a pull request that needs a little love. label Feb 9, 2019
@yoshi-automation yoshi-automation removed the 🚨 This issue needs some love. label Feb 9, 2019
Copy link
Contributor

@mikelehen mikelehen left a comment

Choose a reason for hiding this comment

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

Sorry for the slight delay. I looked over the individual PRs and left a few minor comments. I think mostly this looks good but we need to fix the merge conflicts and revert the breaking changes (sorry).

throw new Error(`${
invalidArgumentMessage(
arg, 'precondition')} Input contains more than one condition.`);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this be checking for invalid preconditions? E.g. if I typo and do {exist: true} instead of {exists: true}

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 filed b/125363185 to tackle this at a later point.

* @private
* @param arg The argument name or argument index (for varargs methods).
* @param value The object to validate.
* @param options Optional validation options specifying whether the valued can
Copy link
Contributor

Choose a reason for hiding this comment

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

"valued" => "value" here and elsewhere.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed

*
* @private
*/
function formatPlural(num, str) {
return `${num} ${str}` + (num === 1 ? '' : 's');
export interface AllowOptional {
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: I don't love AllowOptional and AllowRange as names. Normally I like to name classes / interfaces as nouns. I think maybe OptionalOption and RangeOptions would be better, though admittedly OptionalOption is a bit awkward. :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Renamed to RequiredArgumentOptions and NumericRangeOptions. Not sure if better but it is different!

* @param allowExist Whether to allow the 'exists' preconditions.
*/
function validatePrecondition(
arg: string|number, value: unknown, allowExist: boolean): void {
Copy link
Contributor

Choose a reason for hiding this comment

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

allowExist => allowExists ? (you use "/allowExists=/" elsewhere)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

if (conditions > 1) {
throw new Error(`${
invalidArgumentMessage(
arg, 'precondition')} Input contains more than one condition.`);
Copy link
Contributor

Choose a reason for hiding this comment

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

This error and above use "condition" instead of "precondition". Is that intentional? We should probably use whatever leads to the most consistency across our APIs / docs / errors.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It was, and the argument for that is that a precondition can only contain one condition. I can also see that someone might treat { exists: true } and { lastUpdateTime: Date.now() } as single preconditions, in which case { exists: true, lastUpdateTime: Date.now() } would make two preconditions. I don't have a strong preference here, but since I managed to confuse you, let's err on the side of less confusion and use the term precondition.

* @param value The object to validate.
* @param options Optional validation options specifying whether the valued can
* be omitted.
* @returns 'true' if the input is a valid SetOptions object.
Copy link
Contributor

Choose a reason for hiding this comment

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

Should be @throws .... I think several other methods are wrong too.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed, but I only found this one instance. There are still some internal helpers that do return a boolean.

@@ -957,29 +958,29 @@ describe('set document', () => {
firestore.doc('collectionId/documentId').set({foo: 'bar'}, 'foo');
})
.to.throw(
'Argument "options" is not a valid SetOptions. Input is not an object.');
'Argument "options" is not a valid set() option. Input is not an object.');
Copy link
Contributor

Choose a reason for hiding this comment

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

This reads a bit weird. Maybe "is not a valid set() options object." ? though admittedly that's a bit verbose.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Changed to "is not a valid set() options argument".

@@ -349,7 +348,7 @@ export class DocumentSnapshot {
* console.log(`Retrieved data: ${JSON.stringify(data)}`);
* });
*/
data(): DocumentData|undefined {
data(): {[field: string]: any}|undefined { // tslint:disable-line no-any
Copy link
Contributor

Choose a reason for hiding this comment

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

Are all of these remaining instances of "any" expected to stay forever? Or do you intend further cleanup passes? In my ideal world we'd either remove them or comment them. :-) This one may be intentional so that our users don't have to do instanceof checks on the data they get back? I'm not sure 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.

Yes, I think that we do need to use any here. We don't have a way to control what we are giving back, and by using any as a return type we indicate that the user has to do some type checking of their own. I added a comment.

Copy link
Contributor

Choose a reason for hiding this comment

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

by using any as a return type we indicate that the user has to do some type checking of their own

Sorry for the nit, but that's not actually what any does. 😀 any makes it so you don't have to do any type checking (you can do anything you want with it and TypeScript won't complain). If we want to force the user to do type-checking, then we should return unknown (new in TS 3.x) instead, which you can't do anything with unless you first do type-checking on it.

That said, using any here is consistent with the mobile SDK (https://github.com/firebase/firebase-js-sdk/blob/master/packages/firestore-types/index.d.ts#L24), so I'm fine with this.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Uhm, that's what I meant (kind of, but not really).

Changed to

  // We deliberately use `any` in the external API to not impose type-checking
  // on end users.


return true;
export function validateQueryOrder(arg: string|number, op: unknown): void {
validateEnumValue(arg, op, Object.keys(directionOperators), {optional: true});
Copy link
Contributor

Choose a reason for hiding this comment

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

Per discussions with Gil, etc. during the push to GA, I think we want to unroll the breaking change we made here (and elsewhere). And in fact we may want to relax the mobile SDK to match.

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 brought back the old behavior by pre-preprocessing op.

@@ -60,7 +58,6 @@ const comparisonOperators:
{[k: string]: api.StructuredQuery.FieldFilter.Operator} = {
'<': 'LESS_THAN',
'<=': 'LESS_THAN_OR_EQUAL',
'=': 'EQUAL',
'==': 'EQUAL',
Copy link
Contributor

Choose a reason for hiding this comment

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

Per discussions with Gil, etc. during the push to GA, I think we want to continue to allow '=' to avoid the breaking change.

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 changed the validation function to accept both = and ==. The function returns the sanitized value.

@codecov
Copy link

codecov bot commented Feb 21, 2019

Codecov Report

Merging #512 into master will decrease coverage by 0.08%.
The diff coverage is 96.98%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master     #512      +/-   ##
==========================================
- Coverage   95.93%   95.85%   -0.09%     
==========================================
  Files          24       24              
  Lines        1943     1928      -15     
  Branches      161      166       +5     
==========================================
- Hits         1864     1848      -16     
  Misses         57       57              
- Partials       22       23       +1
Impacted Files Coverage Δ
dev/src/types.ts 100% <ø> (ø) ⬆️
dev/src/document-change.ts 100% <ø> (ø) ⬆️
dev/src/timestamp.ts 100% <100%> (ø) ⬆️
dev/src/watch.ts 99.04% <100%> (ø) ⬆️
dev/src/order.ts 95.4% <100%> (-0.06%) ⬇️
dev/src/field-value.ts 93.75% <100%> (+1.29%) ⬆️
dev/src/logger.ts 100% <100%> (ø) ⬆️
dev/src/geo-point.ts 91.66% <100%> (-0.65%) ⬇️
dev/src/write-batch.ts 100% <100%> (ø) ⬆️
dev/src/index.ts 98.17% <100%> (+0.96%) ⬆️
... and 8 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 3f960ca...f681862. Read the comment docs.

@codecov
Copy link

codecov bot commented Feb 21, 2019

Codecov Report

Merging #512 into master will decrease coverage by 0.07%.
The diff coverage is 97.03%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master     #512      +/-   ##
==========================================
- Coverage   95.93%   95.85%   -0.08%     
==========================================
  Files          24       24              
  Lines        1943     1932      -11     
  Branches      161      168       +7     
==========================================
- Hits         1864     1852      -12     
  Misses         57       57              
- Partials       22       23       +1
Impacted Files Coverage Δ
dev/src/types.ts 100% <ø> (ø) ⬆️
dev/src/document-change.ts 100% <ø> (ø) ⬆️
dev/src/timestamp.ts 100% <100%> (ø) ⬆️
dev/src/watch.ts 99.04% <100%> (ø) ⬆️
dev/src/order.ts 95.4% <100%> (-0.06%) ⬇️
dev/src/field-value.ts 93.75% <100%> (+1.29%) ⬆️
dev/src/logger.ts 100% <100%> (ø) ⬆️
dev/src/geo-point.ts 91.66% <100%> (-0.65%) ⬇️
dev/src/write-batch.ts 100% <100%> (ø) ⬆️
dev/src/index.ts 98.17% <100%> (+0.96%) ⬆️
... and 8 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 6b5bd1e...4515566. Read the comment docs.

@schmidt-sebastian schmidt-sebastian changed the title Finish TypeScript Migration (BREAKING CHANGE) Finish TypeScript Migration Feb 21, 2019
Copy link
Contributor

@mikelehen mikelehen left a comment

Choose a reason for hiding this comment

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

LGTM with nits.

@@ -349,7 +348,7 @@ export class DocumentSnapshot {
* console.log(`Retrieved data: ${JSON.stringify(data)}`);
* });
*/
data(): DocumentData|undefined {
data(): {[field: string]: any}|undefined { // tslint:disable-line no-any
Copy link
Contributor

Choose a reason for hiding this comment

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

by using any as a return type we indicate that the user has to do some type checking of their own

Sorry for the nit, but that's not actually what any does. 😀 any makes it so you don't have to do any type checking (you can do anything you want with it and TypeScript won't complain). If we want to force the user to do type-checking, then we should return unknown (new in TS 3.x) instead, which you can't do anything with unless you first do type-checking on it.

That said, using any here is consistent with the mobile SDK (https://github.com/firebase/firebase-js-sdk/blob/master/packages/firestore-types/index.d.ts#L24), so I'm fine with this.

@@ -785,6 +786,8 @@ export class QuerySnapshot {
* });
* });
*/
// We deliberately use `any` to allow users to use arbitrary objects as the
// callback's context.
Copy link
Contributor

Choose a reason for hiding this comment

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

I think you can use unknown here since we don't care what the type is, we just pass it through.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, that works. Not sure how .call() is defined, but the linter doesn't complain.

@@ -165,7 +169,7 @@ export function validateBoolean(
*/
export function validateNumber(
arg: string|number, value: unknown,
options?: AllowOptional&AllowRange): void {
options?: RequiredArgumentOptions&NumericRangeOptions): void {
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this really how clang-format formats this? Yuck. 😒

@@ -14,20 +14,22 @@
* limitations under the License.
*/

const v1beta1 = require('../../src/v1beta1');
Copy link
Contributor

Choose a reason for hiding this comment

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

unused?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Removed. Bye bye v1beta1

@schmidt-sebastian schmidt-sebastian merged commit b1f6c75 into master Feb 22, 2019
@schmidt-sebastian schmidt-sebastian deleted the mrschmidt-mergedcleanup branch March 11, 2019 03:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
cla: yes This human has signed the Contributor License Agreement. needs work This is a pull request that needs a little love.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants