prisma-labs / graphql-framework-experiment

Code-First Type-Safe GraphQL Framework
https://nexusjs.org
MIT License
673 stars 66 forks source link

Lazy app initialization code #758

Closed jasonkuhrt closed 4 years ago

jasonkuhrt commented 4 years ago

Current Workaround

Please see this https://github.com/graphql-nexus/nexus/issues/732#issuecomment-626586244

Perceived Problem

Ideas / Proposed Solution(s)

Raw sketches (with @Weakky) ```ts import { log, settings, state, use } from 'nexus'; import { prisma } from 'nexus-plugin-prisma'; lifecycle.on('dev|build|prod', () => {}) lifecycle.is('dev|build|prod') env interface State { } const app = App.create({ isDev: process.env.NEXUS_STAGE === 'dev', isReflectionTime: process.env.NEXUS_IS_CONFIGURATION_TIME }) if (!state.isReflectionTime) { // heavy db setup } if (state.isDev && !state.isReflectionTime) { // add schema middleware for dev only } // process.env.NODE_ENV === 'production' // env.stage === 'reflection' (env.stage === 'build' || env.stage === 'dev') !== state.reflectionTime (env.stage === 'build') === state.reflectionTime // concepts // deployment stage: development / prodution (/ qa / testing / ...) // execution phase: run / reflection // if (state.executionPhase === 'run|reflection') {} // if (state.deploymentStage === 'development|production|qa|testing') {} // isReflectionTime -> isRun // run | reflection // run // if (state.stage === 'development|production|qa|testing|reflection') { // } if (state.lifecycle === 'start') let state = { prismaClient: PrismaClient } // making the logic lazy on.start(() => { prismaClient = new prismaClient() }) // making the logic lazy on.start(() => { state.prismaClient = new prismaClient() }) export { state }; // nexus.config.ts -> plugins and settings // schema.ts // schema.ts -> only importing the schema types -> ts-node file state.prismaClient in('dev', () => { on('start', (e) => { }) }) // deployment stage => development | qa | production | testing // _________________________ production / not development // build stage => development | production const __dev__ = NODE_ENV.env === 'development' if (__dev__) { } // why not just import { stage } from 'nexus' on('start', (event, stage) => { if (stage === 'development') { } }) on.boot(() => { }) on('start').in('development').run(() => { }) if (state.stage === '') lifecycle.start = (() => { }) if (state.executionPhase === 'run') { require('./on.start.ts') } on('run', () => { }) // on('start', (runtime) => { // const heavy // runtime.use.prisma() // runtime // }) use(prisma({ client: { instance: prismaClient } )) settings.change({ logger: { level: 'trace', }, schema: { connections: { foobar: {}, toto: {}, }, }, server: { startMessage: info => { settings.original.server.startMessage(info) log.warn('piggy back message!') }, }, }) const schema = schema.make() await server.start({ schema }) ```

References

Instances

jasonkuhrt commented 4 years ago

Another use-case came up recently: https://github.com/graphql-nexus/nexus/issues/523#issuecomment-622451621

The reason we can consider it a use-case is that we can tap into the lifecycle to solve it, for example:

on.before.start((data) => {
  server.express.get('*', () => {
    // ...
  })
})
jasonkuhrt commented 4 years ago

New idea for an API.

Also, we now have better definition of the runtime hooks.


// style 1

on(async ({ before, after }) => {
  let data

  data = await before.assembly
  data = await before.assembly.loadPlugins
  data = await before.assembly.makeSchema
  data = await before.assembly.makeServer 
  data = await before.assembly.checks
  data = await before.start
  data = await before.start.serverListening

  data = await after.assembly
  data = await after.assembly.loadPlugins // [1] reflect on used plugins short circuit here
  data = await after.assembly.makeSchema // [2] reflect on schema short circuit here
  data = await after.assembly.makeServer 
  data = await after.assembly.checks
  data = await after.start
  data = await after.start.serverListening
})
// style 2

on.before.start((data) => {})
...
// Example style 1 v 2

on(async ({ before }) => {
  const data = await before.start

  server.express.get('*', () => {
    // ...
  })
})

on.before.start((data) => {
  server.express.get('*', () => {
    // ...
  })
})

Style 1 may pose too challenging to implement but I'm interested in trying. It will require some trickery but so far doesn't look impossible.

Also, quite interesting to think about DX of top-level await that we could explore in the future:

let data

data = await on.before.assembly
data = await on.before.assembly.loadPlugins
data = await on.before.assembly.makeSchema
data = await on.before.assembly.makeServer 
data = await on.before.assembly.checks
data = await on.before.start
data = await on.before.start.serverListening

data = await on.after.assembly
data = await on.after.assembly.loadPlugins // [1] reflect on used plugins short circuit here
data = await on.after.assembly.makeSchema // [2] reflect on schema short circuit here
data = await on.after.assembly.makeServer 
data = await on.after.assembly.checks
data = await on.after.start
data = await on.after.start.serverListening
jasonkuhrt commented 4 years ago

We could also entertain worktime hooks, but probably in a new issue, and later.

on.worktime.build(async ({ before, after }) => {
  // e.g.
  await before.reflection
  await after.typescript.checker.pass
  await before.typescript.emit
})
jasonkuhrt commented 4 years ago

Discussion with @Weakky the other day.

jasonkuhrt commented 4 years ago

Another instance https://github.com/graphql-nexus/nexus/discussions/983

gustawdaniel commented 4 years ago

Wow! Listening on events in booting process is grat feature.

What to you think about global event emitter with events:

before.assembly
after.assembly.checks

Accessible in:

app.emmiter

Node js Api: https://nodejs.org/api/events.html

jasonkuhrt commented 4 years ago

@gustawdaniel

What to you think about global event emitter with events:

I think that API is more than we need and would degrade the simplicity of the Nexus API. Open to hearing specific pros about it though.

jasonkuhrt commented 4 years ago

Another instance #1201

jasonkuhrt commented 4 years ago

Users should probably be able to tap into reflection as well. Doing so will require removing the code at build time, so that bundling doesn't drag in work time dependencies. This would make it easy for users to, for example, generate a genql client. While in that case Nexus might integrate it (genql) the point is any tool.

Some of the events we have:

reflection
reflection.loadPlugins
reflection.generateArtifacts
reflection.generateArtifacts.generateTypegen
reflection.generateArtifacts.generateSDL
jasonkuhrt commented 4 years ago

Another idea. Path and module name convention. I’m not convinced we should do this. But I want to document it.

[<path>]/on/<eventName>.ts

Examples:

on/start.ts
on/reflectionDone.ts

What’s nice about this is:

What’s not good about this:

jasonkuhrt commented 4 years ago

I would like to unify a few things: runtime events, worktime events, stage "events" (?). Stage is about what "environment" the app is running in: dev, preview, prod, etc.

Example, rewriting use-case of https://github.com/graphql-nexus/nexus/issues/1201:

import { on, schema, settings } from 'nexus'
import Redis from 'redis';

on.run.before(() => {
  schema.addToContext({
    redis: Redis.createClient(settings.current.redis)
  })
})

Note, this requires allowing objects to be passed to addToContext. In my mind @Weakky this use-case, lazy initialization, puts that debate to rest. We need it.

Example, generate genql:

import { generate } from 'genql-cli'
import { on } from 'nexus'

on.reflect.after(async (project) => {    // <-- this codeblock stripped at build time <3
  await generate({
    schema: project.data.schema,
    output: project.path('client'),
    scalarTypes: {
      MongoID: 'string',
    },
  })
})

Access to booleans:

if (on.stage.now === 'production') {

}

if (on.reflect.now) {

}

if (on.run.now) {

}
jasonkuhrt commented 4 years ago

Addressing another use-case https://prisma.slack.com/archives/CM2LEN7JL/p1594329877332200

Example, access built graphql schema

import { createServer } from 'http';
import { SubscriptionServer } from 'subscriptions-transport-ws';
import { execute, subscribe } from 'graphql';
import { schema } from './my-schema';

on.run.graphql.after(({ schema }) => {
  const websocketServer = createServer((request, response) => {
    response.writeHead(404)
    response.end()
  })

  const subscriptionServer = SubscriptionServer.create(
    {
      schema,
      execute,
      subscribe,
    },
    {
      server: websocketServer,
      path: '/graphql',
    },
  )

  return new Promise(res => { websocketServer.listen(5000, res) }).then(() => {
    console.log(
      `Websocket Server is now running on http://localhost:${WS_PORT}`
    )
  })
})