-
Notifications
You must be signed in to change notification settings - Fork 65
feat: add server.custom api #382
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
Conversation
6ca1a15
to
11063e2
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.
Excited to get this out the door! Found it hard to write my feedback for the API, its tricky! No blocking comments!
src/framework/server.ts
Outdated
|
||
interface CustomServerHookInput { | ||
schema: GraphQL.GraphQLSchema | ||
defaultServer: ServerWithInstance<Express> |
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.
Mutually exclusive comments:
👏
One thing, wonder if we should go with originalServer
. Thinking of how I played with this on the Settings
API. But we never talked about it! Maybe the Settings
API should change to be settings.defaults
instead.
But either way, I think probably using consistent terminology is the most important thing.
I do kind of like softening the terminology a bit where we can to make it a little more humane/literate. For example settings
instead of config
.
Thoughts?
Since we're strongly typing this as Express
what's the point of this not just being an Express
reference called express
?
I guess plugins could one day supply custom server and then user wants to to tap into it just for side-effects (e.g. add middleware).
Then, I see value, because then, plugins could turn defaultServer
from Express
to e.g. Fastify
.
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 was indeed the original goal. But your comment made me realize that nothing was ready for plugins to override the type anyway so it might be a bit premature. I reverted and "hardcoded" express into its own type for now!
Regarding original
vs default
, I think original
is probably better. That's usually how people name their variable when overriding functions. FWIW, I have an example in mind, taken from the TS compiler API documentation
Notice how both variables are called orig<something>
, where orig
is most likely referring to original
.
TLDR: I renamed to property to originalServer
👍
src/framework/server.ts
Outdated
interface CustomServerHookInput { | ||
schema: GraphQL.GraphQLSchema | ||
defaultServer: ServerWithInstance<Express> | ||
settings: App.SettingsData['server'] |
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.
Calling this serverSettings
would be more consistent with other name of defaultServer
?
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.
Renamed too! I agree with you, I was initially passing the whole settings
object. If I may be really neat-picking here, I don't like the fact that we have <server>Settings
and default<Server>
(server is used as prefix and as suffix).
Makes me wonder if it'd be better to scope everything under a single server
property.
server: {
original: Express
settings: SettingsData['server']
}
Either server.originalInstance
or server.original
. That'd be even more consistent with settings.original
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, just saw your comment below. Yeah, you're right, let's just go with express
and let them take the settings the import
src/framework/server.ts
Outdated
instance: T | ||
} | ||
|
||
interface CustomServerHookInput { |
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 feedback invalidates some of the below. Actually a bit tricky of an API/naming here, so pardon the multiple takes I'm giving...
-
I don't think we need to pass
settings
reference because user can get it from lexical scope. -
I think it might be simplest to just do:
server.custom(({ express }) => { }
something else, not sure...
// simple case
server.custom(express => {
express.use(...)
return {
express.listen(settings.current.server.port)
}
})
// advanced case
server.custom(express => {
express.use(...)
return {
start(){
// user can lexically access settings, should be fine!
express.listen(settings.current.server.port)
}
}
})
// most advanced case
server.custom((_, { schema }) => {
// User can get at schema, may ignore originalServer instance if they wish
// I don't think we actually need the `settings` here, should be
// good enough with lexical scope access.
})
or...
// most advanced case
server.replace(({ schema }) => {
// User can get at schema, may ignore originalServer instance if they wish
// I don't think we actually need the `settings` here, should be
// good enough with lexical scope access.
})
- It supports (by way of simplest api/syntax) a primary use-case of configuring express, not swapping the server completely.
- It allows user to name the server as it actually is
.replace
can improve boot performance because we can skip creatingexpress
instance completely...- Then we need to make it a runtime error if user calls both
replace
andcustom
... - Basically back to raw 🙈
- Then we need to make it a runtime error if user calls both
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.
Actually, I'm reconsidering your suggestion. I'm fine with calling it express
, but I think I'd prefer keeping express of type Server
and not the raw Express
. It makes the usage way simpler in case you just want to add middlewares and other kind of stuff to the express instance. All you need to do is mutate the express
(previously originalServer
) property, and return it.
As to your suggestion for replace/custom
, I personally find it easier and more generic the way it is now. There's no need to wonder what's the difference between replace
and custom
, no possibility of errors if you actually use both, and, with my suggestion just above, it makes it very simple to just configure express
server.custom(({ originalServer }) => {
originalServer.instance.use(/*some custom middleware*/)
return originalServer
})
Even cleaner pattern IMO if you have too much express related stuff:
import { customizeExpress } from './custom-express'
server.custom(({ originalServer }) => {
return customizeExpress(originalServer.instance)
})
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 about we make it so returning is optional and if you don’t it means do not override the server? I think this is most clear. It does create more room for user error though. If we had replace and custom then we could make more precise return type. But not sure it’s worth the extra dpi surface. Think the return-to-replace is the sweet spot.
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'd prefer keeping express of type Server and not the raw Express. It makes the usage way simpler in case you just want to add middlewares and other kind of stuff to the express instance.
Then user should never need access to server interface. Simplest of all?
Hey @nhuesmann, your 0.02 on this if any would be welcome. |
Hey @jasonkuhrt I just saw your mention. I took a look at this, so far I like where it's headed. To clarify:
So far this feels good! |
Hey @nhuesmann, Yeah, this is an attempt at reconciling the The idea of the raw api where you could access the instance via I'm not sure my explanation is clear, sorry if it's not 🙈 |
Note: I added a missing feature to pass the nexus generated context to a custom server. The I updated the example at the top to reflect the changes. EDIT: @jasonkuhrt Made me think about something though: That feature is error-prone. It's very tempting to do: server.custom(({ schema, createContext }) => {
const customServer = new GraphQLServer({
schema,
context: req => ({
...createContext(req),
additionalProperty: 'foo' // <== Wrong usage. If people do that, they won't have type-safety in their resolvers because the only way should be through `schema.addToContext()`
})
})
}) If people add any additional property "manually", they won't have type-safety in their resolvers because the only way should be through Also makes me wonder if It also makes it less awkward for the problem above. Having EDIT2: Continuing train of thoughts, worth wondering then if it should live under the EDIT3: Here's my position after thinking more: Under these assumptions, having a slightly error-prone API when dealing with custom stuff doesn't bother me that much, especially because you're not exposed to the context stuff when you're just willing to add middlewares to the express server. Therefore, I'd say we can properly document the limitation (that context should exclusively be contributed through the I'm still preferring |
@Weakky I just got caught up.
Anyway, that's what I've got for now, hope that helps. Lemme know if any of it needs clarification. |
@nhuesmann thanks for the input! Really nice to have another angle in the discussion 🙏 import Fastify from 'fastify'
import GQL from 'fastify-gql'
import { server } from 'nexus-future'
type Server = any
type ServerLens = any
type ServerReplacer = (lens: ServerLens) => Server
type ServerAugmenter = (lens: ServerLens) => void
// server.custom
// server.customize
// server.tap
// augment
// server.custom(({ express }) => {
// express.use(...)
// // no return, does not replace
// })
// // ???
// server.customize((express, { schema }) => {
// expreess.use() //
// })
// // ???
// server.tap((express, { schema }) => {
// expreess.use() //
// })
// // ???
// server.customize(({ express, schema }) => {
// express.use()
// })
// replace pattern
server.custom(({ express }) => {
// inside server
// ...
// -->
express.use(...)
// return to outside server
// ...
// <--
return {
start() {},
stop() {}
}
})
// tap pattern
server.custom(({ express }) => {
express.use(...)
})
const app: any
const schema: any
const settings: any
// replace
server.custom(({ context, schema }) => {
const app = Fastify()
return {
async start() {
app.register(GQL, {
schema,
context,
ide: 'graphiql',
})
await app.listen(settings.current.server.port)
console.log(
`Server listening at http://localhost:${e.settings.port}/graphiql`
)
},
stop() {
return app.close()
},
}
})
// the new addTocontext
schema.context(req => {})
schema.context<Request>(req => {})
app.schema
app.server
server.start
server.stop
server.addToSchemaContext(req => {})
server.addToContext
server.addContext
server.context
server.custom // ize
schema.context(req => {
}) //
// server.addContext({
// // ...
// })
// schema.addContext({
// // ..
// })
// schema.addContext(() => {
// })
// add to context ????
// integration concern
// future no-magic api
// aka. explicit integration
// server.on('request', (req) => {
// schema.addContext({
// user: {
// id: req.id
// }
// // ...
// })
// }) Summary:
|
@jasonkuhrt changes implemented on latest commit. I also made it zero-cost at runtime in case a custom server is provided. The implementation is a bit tricky but I think it's safe and fairly nice. Last round of review possible if you want! Updated usage examples: Using a custom fastify server import { settings, server, schema } from 'nexus-future'
import Fastify, { FastifyRequest } from 'fastify'
import GQL from 'fastify-gql'
server.custom(async e => {
const app = Fastify()
return {
async start() {
app.register(GQL, {
schema: e.schema,
context: e.context,
ide: 'playground',
})
await app.listen(settings.current.server.port)
console.log(
`Server listening at http://localhost:${settings.current.server.port}/graphiql`
)
},
stop() {
return app.close()
},
}
})
// Output:
// ○ server:Custom server used Simply accessing express instance server.custom(e => {
e.express.use(/* ... */)
})
// Output:
// ○ server:Augmented Express server used |
As Jason mentioned above, we went with an api that makes it easier/less awkward to access the express instance. You can see an example on my comment above
Yes. But you can easily just move your code into a separate function to keep things clean. Any reason to be worried about everything being setup there? |
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.
Looks great!
Question about the example code, why is the argument named e
?
No particular reason, I initially called it server.custom(lens => {
lens.express.*
}) I'm currently writing an example for fastify. What would you suggest as naming? EDIT: Decided not to give it a name and destructure it 😄 |
@Weakky I dig the change to Excited about this progress! Thanks for letting me be involved. |
@Weakky @jasonkuhrt could you guys explain the thought behind the name |
It’s not particularly deep. The idea there being a kind of mini API/handle into a part/this part of the system. Nothing to do with Functional programming lenses... heh. What would you call this kind of thing? Sent with GitHawk |
@nhuesmann agreed about multiple calls ambiguity. In a sense this arguably is more like a setting dressed up as an API. I cannot think of a good reason there would be multiple calls to custom for server replacement. I kind of could see, more, multiple calls for adding middleware if for some reason that was spread through the codebase. But seems exotic. It’s a quirk for sure, but, somehow don’t mind it right now. Sent with GitHawk |
@jasonkuhrt Re: multiple calls, cool, glad you think so. Re: lens- Gotcha. I think I just brought it up bc Prisma just renamed Photon/Lift for greater clarity, and |
@jasonkuhrt Ideas, though not great: Last 2 cents- I still feel like the This isn't critical, I just was reading through the code changes in the PR as well as @Weakky's updated example and it stuck out to me again. Ultimately go with whatever you guys feel best about. Also full disclosure- my degree is in English, so I sometimes fret about variable names and their varying degrees of clarity a wee bit too much 🙃 |
closes #368, #369
Description
server.custom
api to provide custom serverUsage examples:
Using fastify
Accessing the default express instance to do some stuff
TODO