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

feat(types): ship automatic TypeScript typings #794

Merged
merged 40 commits into from
Jul 21, 2018

Conversation

cdaringe
Copy link
Contributor

@cdaringe cdaringe commented Jul 7, 2018

problem statment

  • TS typings are out of date
  • TS typings don't ship by default

partially address #520

solution

  • have this TS project auto-build its typing artifcacts
    • achieve this theoretically via a 3 LOC code change in the tsconfig.json
    • address the cascading set of small issues associated with ^

package.json Outdated
@@ -34,6 +34,9 @@
"prepack": "./scripts/build"
},
"dependencies": {
"@types/pg": "^7.4.10",
"@types/graphql": "^0.8.2",
"@types/jsonwebtoken": "<7.2.1",
Copy link
Contributor Author

Choose a reason for hiding this comment

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

because our interfaces expose types that use these types, the Typescript documentation states to bundle them as dependencies. i was mixed feelings on this!

@@ -75,8 +76,8 @@
"source-map-support": "^0.4.6",
"supertest": "^2.0.1",
"ts-node": "^2.0.0",
"tslint": "^4.2.0",
"typescript": "2.1.5"
"tslint": "^5.10.0",
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 disable new rules. will discuss later

@@ -4,4 +4,4 @@ npm_bin=$(npm bin)

set -e

$npm_bin/tslint $(find src -name "*.[tj]s" -a \! -wholename '*/node_modules/*') $@
$npm_bin/tslint --format verbose $(find src -name "*.[tj]s" -a \! -wholename '*/node_modules/*') $@
Copy link
Contributor Author

Choose a reason for hiding this comment

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

adds rule names to output, essentially.

import jwt = require('jsonwebtoken')
import { GraphQLError } from 'graphql/error'

export namespace Postgraphile {
Copy link
Contributor Author

@cdaringe cdaringe Jul 7, 2018

Choose a reason for hiding this comment

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

types that were used in various places i moved into a namespace. for instance, PostGraphileOptions was almost fully duped in 2 places. i defined it once, imported it back two places, and extended it as needed

Copy link
Member

Choose a reason for hiding this comment

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

Capital g please 🤗

Copy link
Contributor Author

@cdaringe cdaringe Jul 8, 2018

Choose a reason for hiding this comment

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


const debugCli = debugFactory('postgraphile:cli')

// tslint:disable no-console
// tslint:disable no-console no-shadowed-variable
// no-shadowed-variable is erroroneously reporting
Copy link
Contributor Author

Choose a reason for hiding this comment

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

im betting the older tslint didnt do this :/ it was claiming that plugins in a typedef was shadowed. infact, it was just annotating the type. bonkers AST parse. as you can see, i didn't modify anything to do w/ a variable called plugins in this file.

Copy link
Member

@benjie benjie Jul 20, 2018

Choose a reason for hiding this comment

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

Nope; this was a legit error (my bad) 5599c82

@@ -1,101 +0,0 @@
import { IncomingMessage, ServerResponse } from 'http'
Copy link
Contributor Author

Choose a reason for hiding this comment

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

all of this content was trashed or moved into interfaces.d.ts, as referenced above

@@ -1,30 +1,49 @@
/* eslint-disable */// Because we use tslint
/* tslint:disable:no-any no-var-requires */
Copy link
Contributor Author

Choose a reason for hiding this comment

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

and here's the real only significant offender of this PR. to get the build to auto-populate typings, allowJs must be set to false. this was the lone .js in the lib.

this file could use have used some deep, deep tidy before i put my fingers on it. do to the size and complexity of it, rather than put proper types on everything in the true TS spirit, I declared many of the internals as any. this was to prevent any major refactor or create even more change than we already want for this PR.

* - `express`.
* - `koa` (2.0).
*/
export interface ICreateRequestHandler extends Postgraphile.PostGraphileOptions {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

boom! that huge red-file (createPostGraphileHttpRequestHandler.d.ts), gets compacted down to these 9 LOC. mo betta, much betta.

if (options.showErrorStack)
formattedError.stack =
options.showErrorStack === 'json'
options.showErrorStack
Copy link
Contributor Author

Choose a reason for hiding this comment

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

👀 needs discussion. showErrorStack is a bool everywhere else, and the lib wouldn't compile with the above conditional. is this really a supported feature? it looks like in both places, the stack makes it through.

im not sure of the intent.

Copy link
Member

Choose a reason for hiding this comment

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

This was before my time, but I believe there used to be an option where you could have the stack return an array rather than a string. I don't think this is currently possible, but a review of the code would be needed to assert this is the case.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ya. it logically doesn't make sense that showErrorStack would affect the return type of the stack anyway. i'm prone to change this whole logic entirely to:

formattedError.stack = option.showErrorStack ? (error.stack || '') : ''

Copy link
Member

Choose a reason for hiding this comment

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

So it turns out that this is actually a documented feature:

showErrorStack: Enables adding a stack field to the error response. Can be either the boolean true (which results in a single stack string) or the string 'json' (which causes the stack to become an array with elements for each line of the stack). Recommended in development, not recommended in production.
-- https://www.graphile.org/postgraphile/usage-library/#api-postgraphilepgconfig-schemaname-options

Copy link
Contributor Author

Choose a reason for hiding this comment

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

in the original ternary, either way, a stack was assigned. this is a bug with the original impl. let's file a bug and address it independently.

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 #804 for addressing this

if (error) {
return next(error)
}
fn(req, res, next)
})
}
},
(req, res, next) => next(),
(req: any, res: any, next: any) => next(),
Copy link
Contributor Author

Choose a reason for hiding this comment

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

honestly, i dont know how this is working at all right now (before i touched it, or after). i can't find next on 245 in scope? our initial value is a function which calls next, which i dont see defined anywhere.

Copy link
Member

@benjie benjie Jul 8, 2018

Choose a reason for hiding this comment

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

I know how that feels. It’s one of the function arguments: req, res, NEXT 🙃

Basically this is a no-op middleware we use as a base so we can efficiently combine the other middlewares into one middleware. It helps the JIT.

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, next isnt really used. we may as well have literally used a no-op on 245. 237-241 provides a next implementation for us. doh! thx :)

Copy link
Member

Choose a reason for hiding this comment

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

Yeah this reduce is super confusing; the next is actually called, it's just the inner most "no-op" middleware; each middleware wraps the previous one and we have this be the initial one.

@@ -430,7 +438,7 @@ export default function createPostGraphileHttpRequestHandler(options) {
req._koaCtx.compress = false
}
await new Promise((resolve, reject) => {
const stream = sendFile(req, assetPathRelative, {
sendFile(req, assetPathRelative, {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

ya ya ya. the linter made me do all of this pedantic junk. im sorry!

@@ -742,7 +749,7 @@ export default function createPostGraphileHttpRequestHandler(options) {

res.setHeader('Content-Type', 'application/json; charset=utf-8')

res.end(JSON.stringify(returnArray ? results : results[0]))
res.end(JSON.stringify(returnArray ? results : results![0]))
Copy link
Contributor Author

Choose a reason for hiding this comment

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

this is a new TS feature i learned recently. assert that <preceding_var> is defined!

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 nice! This has been annoying me for a while 👍

@@ -1,9 +1,10 @@
/* eslint-disable */// Because we use tslint
Copy link
Contributor Author

Choose a reason for hiding this comment

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

only change below is changing where types are imported from

@@ -1,188 +1,18 @@
import { Pool, PoolConfig } from 'pg'
Copy link
Contributor Author

Choose a reason for hiding this comment

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

only changes below are changing where types are imported from, and migrating these types to a common interfaces file

tsconfig.json Outdated
@@ -14,10 +16,9 @@
"suppressImplicitAnyIndexErrors": true,
"strictNullChecks": true,
"noFallthroughCasesInSwitch": true,
"noUnusedParameters": true,
"noUnusedParameters": false,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

because so often we have (req, res, next) where res isn't used

Copy link
Member

Choose a reason for hiding this comment

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

Can we configure it like ESLint using these rules rather than disabling it outright? I'm not sure how flexible it is in this regard.

  1. we don't care if parameters start with underscores (e.g. _res would be ignored)
  2. we don't care if one of the parameters after this one is used (e.g. (req, res, next) => next() shouldn't raise)

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, perfect. i wasn't aware that that was a thing. looks like ts honors it as well. 2396b0e

@@ -1,6 +1,8 @@
{
"compilerOptions": {
"allowJs": true,
"declaration": 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.

🎉

@@ -1,18 +0,0 @@
/**
Copy link
Contributor Author

Choose a reason for hiding this comment

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

👋 bye bye

@@ -1,4 +0,0 @@
/**
Copy link
Contributor Author

Choose a reason for hiding this comment

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

👋 adddddiiioos!

@cdaringe
Copy link
Contributor Author

cdaringe commented Jul 7, 2018

a net -1K LOC! i say that's a win :)

@cdaringe cdaringe changed the title wip: feat(typings): ship automatic ts typings by default feat(typings): ship automatic ts typings by default Jul 8, 2018
import jwt = require('jsonwebtoken')
import { GraphQLError } from 'graphql/error'

export namespace Postgraphile {
Copy link
Member

Choose a reason for hiding this comment

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

Capital g please 🤗

(ctx: { req: IncomingMessage, res: ServerResponse }, next: () => void): Promise<void>
getGraphQLSchema: () => Promise<GraphQLSchema>
formatError: (e: GraphQLError) => GraphQLError
pgPool: Pool
Copy link
Member

Choose a reason for hiding this comment

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

I think this has more stuff on it now, I should check.

Copy link
Member

Choose a reason for hiding this comment

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

20a50fe

this one was a bit of a drag, inducing a little more typing refactor than I had wanted. ill see if i can annotate it. i bundled your remark from https://github.com/graphile/postgraphile/pull/794/files#r200834248 in this commit too.

if (error) {
return next(error)
}
fn(req, res, next)
})
}
},
(req, res, next) => next(),
(req: any, res: any, next: any) => next(),
Copy link
Member

@benjie benjie Jul 8, 2018

Choose a reason for hiding this comment

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

I know how that feels. It’s one of the function arguments: req, res, NEXT 🙃

Basically this is a no-op middleware we use as a base so we can efficiently combine the other middlewares into one middleware. It helps the JIT.

Copy link
Member

@benjie benjie left a comment

Choose a reason for hiding this comment

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

Excellent start! Here's some feedback and although it took me a solid 45 minutes to go through this I've not reviewed it in enough detail to be confident I've not missed anything.

Please make sure any changes from now are performed as additional commits (please do not rebase!) to make further reviews easier.

(ctx: { req: IncomingMessage, res: ServerResponse }, next: () => void): Promise<void>
getGraphQLSchema: () => Promise<GraphQLSchema>
formatError: (e: GraphQLError) => GraphQLError
pgPool: Pool
Copy link
Member

Choose a reason for hiding this comment

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

).toBeInstanceOf(http.ServerResponse)
})
})
}
Copy link
Member

Choose a reason for hiding this comment

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

I don't think you've replaced this test file after deleting it?

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 possibly where 855 of the removed 1K LOC came from 😳 )

Copy link
Contributor Author

Choose a reason for hiding this comment

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

oh brother. 🤦‍♂️

0ab968a

@@ -36,12 +55,12 @@ const sendFile = require('send')
const LRU = require('lru-cache')
const crypto = require('crypto')

const calculateQueryHash = queryString => crypto.createHash('sha1').update(queryString).digest('base64')
const calculateQueryHash = (query: string) => crypto.createHash('sha1').update(query).digest('base64')
Copy link
Member

Choose a reason for hiding this comment

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

I prefer queryString: string since "query string" is what I would call it aloud. "query" has a different meaning to me.

@@ -94,15 +111,14 @@ const origGraphiqlHtml = new Promise((resolve, reject) => {
* We need to be able to share the withPostGraphileContext logic between HTTP
* and websockets
*/
const withPostGraphileContextFromReqResGenerator = options => {
function withPostGraphileContextFromReqResGenerator(options: ICreateRequestHandler): any {
Copy link
Member

Choose a reason for hiding this comment

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

Rather than any, consider using something like:

function withPostGraphileContextFromReqResGenerator<T>(options: ICreateRequestHandler): (req: IncomingMessage, res: ServerResponse, moreOptions: any, fn: (pgClient: PoolClient) => T) => T

You can use any instead of T if you like. Effectively whatever is returned by the callback function fn will be returned by the function returned from withPostGraphileContextFromReqResGenerator

// "omit" (default) - relay connections only,
// "only" - simple collections only (no Relay connections),
// "both" - both
simpleCollections?: 'omit' | 'both' | 'only',
Copy link
Member

Choose a reason for hiding this comment

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

We should consider adding [propName: string]: any here because PostGraphile plugins can add additional options to this object (e.g. the supporter plugin adds simpleSubscriptions as an option)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@@ -850,6 +857,13 @@ function addCORSHeaders(res) {
)
}

function createBadAuthorizationHeaderError(): void {
throw httpError(
Copy link
Member

Choose a reason for hiding this comment

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

Don't throw here, we throw down below (and the name is just create)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

whoops! great catch. thx. fb7430b

const sse = str => {
if (isKoa) {
const sse = (str: string) => {
if (isKoa && stream) {
Copy link
Member

Choose a reason for hiding this comment

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

Ugh. This is one of the things I hate about typed JS. The logic here would be confusing to the reader as they'd be "if it's koa but there is no stream then do res.write? Why would there not be a stream...?" and then read the code to find it's impossible to not have a stream if it's Koa.

Can you use stream!.write(str) below instead so that it's easier to read and doesn't change the compiled code?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

tsconfig.json Outdated
@@ -14,10 +16,9 @@
"suppressImplicitAnyIndexErrors": true,
"strictNullChecks": true,
"noFallthroughCasesInSwitch": true,
"noUnusedParameters": true,
"noUnusedParameters": false,
Copy link
Member

Choose a reason for hiding this comment

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

Can we configure it like ESLint using these rules rather than disabling it outright? I'm not sure how flexible it is in this regard.

  1. we don't care if parameters start with underscores (e.g. _res would be ignored)
  2. we don't care if one of the parameters after this one is used (e.g. (req, res, next) => next() shouldn't raise)

tsconfig.json Outdated
"noUnusedLocals": true
},
"include": ["src", "typings"],
Copy link
Member

Choose a reason for hiding this comment

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

Why have you removed this? I store a lot of extra stuff (tests for user issues, WIP code, local test scripts, etc) that I wouldn't want to be affected by tsconfig

Copy link
Contributor Author

Choose a reason for hiding this comment

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

mmm. i think i see what you mean. flipping tsconfig to allowJs: false prevents .js files from being copied into the build, so those files would be omitted anyway if we auto gen the declarations. if that content needs to ship, we can do it in a post processing step. does that work? are you certain those need to be shipped?

@@ -6,6 +6,6 @@ declare module 'pg-sql2' {

export function compile(query: SQLQuery): QueryConfig
export function query(strings: TemplateStringsArray, ...values: Array<SQLNode | SQLQuery>): SQLQuery
export function value(val: mixed): SQLNode
export function value(val: any): SQLNode
Copy link
Member

Choose a reason for hiding this comment

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

Why was this change required? Specifically this should only receive JSON-like values, I think...

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 confess. laziness as i wasn't not sure how to import a locally defined interface :/ ../src/interfaces

@benjie
Copy link
Member

benjie commented Jul 20, 2018

Hour and a half into reviewing this, few minor changes but looking good so far. It was too intense to review on it's own so I've checked out the src directory as of master and then re-implemented the changes myself over the top. As my diff gets smaller and smaller we get closer to approving this PR 👍

All the minor files are now done; just the major one createPostGraphileHttpRequestHandler and new companion koaMiddleware to go. 😅

@cdaringe
Copy link
Contributor Author

Cool! Ya, sorry it blew up. allowJs being false really took this from an easy win to beast.

@benjie
Copy link
Member

benjie commented Jul 20, 2018

😅 Okay; I've reviewed it fully and made a load of changes and I'm pretty much ready to merge it. I need to give it a last check over (and fix any CI errors) but I'm already running super late for my evening plans so I'm going to head out.

I've reverted a few things to their previous behaviour.

@benjie
Copy link
Member

benjie commented Jul 21, 2018

I just went through the compiled bundle and inspected every d.ts file; the external requires that we need to bundle types for are:

    "@types/graphql": "^0.8.2",
    "@types/jsonwebtoken": "<7.2.1",
    "@types/koa": "2.0.44",
    "@types/pg": "^7.4.10",

And postgraphile-core which doesn't currently have types. We have a declaration in typings/postgraphile-core.d.ts but I don't think that's being referenced by the build... How do we solve this @cdaringe?

@benjie benjie changed the title feat(typings): ship automatic ts typings by default feat(types): ship automatic TypeScript typings Jul 21, 2018
@benjie benjie merged commit 89b0900 into graphile:master Jul 21, 2018
@cdaringe cdaringe deleted the fix/typings branch July 21, 2018 18:07
@cdaringe
Copy link
Contributor Author

but I don't think that's being referenced by the build

it's being referenced by the build, but not being included in it. based on your remark in #520, i think you already figured it out :)

thanks for taking to the time to get back over all of this!

@benjie
Copy link
Member

benjie commented Jul 21, 2018

Thank you for doing the work! Being TypeScript through and through should make a big difference to a lot of users, and greater confidence in PRs.

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.

3 participants