From 526c09dd315a8a41be8f544f889bc18199a9b01f Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Mon, 3 Feb 2020 20:49:03 -0500 Subject: [PATCH 1/4] feat(configurator): add new system component closes #297 --- src/framework/app.ts | 51 ++++++++++++++++++++++++----------------- src/framework/index.ts | 4 ++-- src/lib/logger/index.ts | 2 +- 3 files changed, 33 insertions(+), 24 deletions(-) diff --git a/src/framework/app.ts b/src/framework/app.ts index 7b3f03aba..768f0eb0e 100644 --- a/src/framework/app.ts +++ b/src/framework/app.ts @@ -27,13 +27,12 @@ type Request = HTTP.IncomingMessage & { log: Logger.Logger } type ContextContributor = (req: Request) => T export type App = { - use: (plugin: Plugin.Driver) => App /** * [API Reference](https://nexus-future.now.sh/#/references/api?id=logger) ⌁ [Guide](https://nexus-future.now.sh/#/guides/logging) * * ### todo */ - log: Logger.RootLogger + log: Logger.Logger /** * [API Reference](https://nexus-future.now.sh/#/references/api?id=server) ⌁ [Guide](todo) * @@ -44,6 +43,10 @@ export type App = { start: (config?: ServerOptions) => Promise stop: () => Promise } + /** + * todo + */ + settings: Settings /** * [API Reference](https://nexus-future.now.sh/#/references/api?id=appschema) // [Guide](todo) * @@ -62,12 +65,35 @@ export type App = { } } +type SettingsInput = { + logger?: Logger.SettingsInput +} + +type SettingsData = Readonly<{ + logger: Logger.SettingsData +}> + +type Settings = { + current: SettingsData + change(settings: SettingsInput): void +} + /** * Crate an app instance * TODO extract and improve config type */ export function create(appConfig?: { types?: any }): App { const plugins: Plugin.RuntimeContributions[] = [] + const settings: Settings = { + change(newSettings) { + if (newSettings.logger) { + log.settings(newSettings.logger) + } + }, + current: { + logger: log.settings, + }, + } // Automatically use all installed plugins // TODO during build step we should turn this into static imports, not unlike @@ -76,29 +102,12 @@ export function create(appConfig?: { types?: any }): App { const contextContributors: ContextContributor[] = [] - /** - * Auto-use all runtime plugins that are installed in the project - */ - let server: Server.Server const schema = Schema.create() + const api: App = { log, - // TODO bring this back pending future discussion - // installGlobally() { - // installGlobally(api) - // return api - // }, - // TODO think hard about this api... When/why would it be used with auto-use - // import system? "Inproject" plugins? What is the right place to expose - // this? app.plugins.use() ? - use(pluginDriver) { - const plugin = pluginDriver.loadRuntimePlugin() - if (plugin) { - plugins.push(plugin) - } - return api - }, + settings, schema: { addToContext(contextContributor) { contextContributors.push(contextContributor) diff --git a/src/framework/index.ts b/src/framework/index.ts index 43fd1de0c..b69afcf12 100644 --- a/src/framework/index.ts +++ b/src/framework/index.ts @@ -1,7 +1,7 @@ import * as App from './app' const app = App.create() -const { log, schema, server } = app +const { log, schema, server, settings } = app export default app -export { log, schema, server } +export { log, schema, server, settings } diff --git a/src/lib/logger/index.ts b/src/lib/logger/index.ts index dbb0c0cd6..9829866f2 100644 --- a/src/lib/logger/index.ts +++ b/src/lib/logger/index.ts @@ -1,3 +1,3 @@ export { demo } from './demo' export { Logger } from './logger' -export { create, RootLogger } from './root-logger' +export { create, RootLogger, SettingsData, SettingsInput } from './root-logger' From 7752d199d5617875dca0baf110341ca9a04cf043 Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Mon, 3 Feb 2020 21:10:43 -0500 Subject: [PATCH 2/4] schema settings --- src/framework/app.ts | 50 +++++++++++++++++++++++--------- src/framework/schema/index.ts | 2 +- src/framework/schema/schema.ts | 52 +++++++++++++++++++++------------- 3 files changed, 70 insertions(+), 34 deletions(-) diff --git a/src/framework/app.ts b/src/framework/app.ts index 768f0eb0e..d68c3b060 100644 --- a/src/framework/app.ts +++ b/src/framework/app.ts @@ -40,7 +40,13 @@ export type App = { * */ server: { + /** + * todo + */ start: (config?: ServerOptions) => Promise + /** + * todo + */ stop: () => Promise } /** @@ -67,15 +73,26 @@ export type App = { type SettingsInput = { logger?: Logger.SettingsInput + schema?: Schema.SettingsInput } type SettingsData = Readonly<{ logger: Logger.SettingsData + schema: Schema.SettingsData }> +/** + * todo + */ type Settings = { + /** + * todo + */ current: SettingsData - change(settings: SettingsInput): void + /** + * todo + */ + change(newSetting: SettingsInput): void } /** @@ -84,27 +101,32 @@ type Settings = { */ export function create(appConfig?: { types?: any }): App { const plugins: Plugin.RuntimeContributions[] = [] + // Automatically use all installed plugins + // TODO during build step we should turn this into static imports, not unlike + // the schema module imports system. + plugins.push(...Plugin.loadAllRuntimePluginsFromPackageJsonSync()) + + const contextContributors: ContextContributor[] = [] + + let server: Server.Server + + const schema = Schema.create() + const settings: Settings = { change(newSettings) { if (newSettings.logger) { log.settings(newSettings.logger) } + if (newSettings.schema) { + schema.private.settings.change(newSettings.schema) + } }, current: { logger: log.settings, + schema: schema.private.settings.data, }, } - // Automatically use all installed plugins - // TODO during build step we should turn this into static imports, not unlike - // the schema module imports system. - plugins.push(...Plugin.loadAllRuntimePluginsFromPackageJsonSync()) - - const contextContributors: ContextContributor[] = [] - - let server: Server.Server - const schema = Schema.create() - const api: App = { log, settings, @@ -113,7 +135,7 @@ export function create(appConfig?: { types?: any }): App { contextContributors.push(contextContributor) return api }, - ...schema.external, + ...schema.public, }, server: { /** @@ -246,14 +268,14 @@ export function create(appConfig?: { types?: any }): App { nexusConfig.types.push(...appConfig.types) } - if (schema.internal.types.length === 0) { + if (schema.private.types.length === 0) { log.warn( 'Your GraphQL schema is empty. Make sure your GraphQL schema lives in a `schema.ts` file or some `schema/` directories' ) } return Server.create({ - schema: await schema.internal.compile(nexusConfig), + schema: await schema.private.compile(nexusConfig), plugins, contextContributors, ...opts, diff --git a/src/framework/schema/index.ts b/src/framework/schema/index.ts index b67aef3e4..c75eb5efd 100644 --- a/src/framework/schema/index.ts +++ b/src/framework/schema/index.ts @@ -1,3 +1,3 @@ export { findDirOrModules, importModules, printStaticImports } from './modules' export { createInternalConfig } from './nexus' -export { create, Schema } from './schema' +export { create, Schema, SettingsData, SettingsInput } from './schema' diff --git a/src/framework/schema/schema.ts b/src/framework/schema/schema.ts index d846efdbe..ee53b6ce7 100644 --- a/src/framework/schema/schema.ts +++ b/src/framework/schema/schema.ts @@ -8,7 +8,7 @@ type ConnectionPluginConfig = NonNullable< type ConnectionConfig = Omit -type SettingsInput = { +export type SettingsInput = { /** * todo */ @@ -35,6 +35,8 @@ type SettingsInput = { } } +export type SettingsData = SettingsInput + export type Schema = { // addToContext: ( // contextContributor: ContextContributor @@ -54,15 +56,18 @@ export type Schema = { idArg: typeof NexusSchema.idArg extendType: typeof NexusSchema.extendType extendInputType: typeof NexusSchema.extendInputType - settings: (settingsInput: SettingsInput) => void } type SchemaInternal = { - internal: { + private: { types: any[] compile: any + settings: { + data: SettingsData + change: (newSettings: SettingsInput) => void + } } - external: Schema + public: Schema } export function create(): SchemaInternal { @@ -86,32 +91,39 @@ export function create(): SchemaInternal { __types, } = createNexusSingleton() - const state: { settings: SettingsInput } = { + type State = { + settings: SettingsData + } + + const state: State = { settings: {}, } - return { - internal: { + const api: SchemaInternal = { + private: { types: __types, compile: (c: any) => { c.plugins = c.plugins ?? [] c.plugins.push(...processConnectionsConfig(state.settings)) return makeSchema(c) }, - }, - external: { - settings(newSettings) { - if (newSettings.connections) { - state.settings.connections = state.settings.connections ?? {} - const { types, ...connectionPluginConfig } = newSettings.connections - if (types) { - state.settings.connections.types = - state.settings.connections.types ?? {} - Object.assign(state.settings.connections.types, types) + settings: { + data: state.settings, + change(newSettings) { + if (newSettings.connections) { + state.settings.connections = state.settings.connections ?? {} + const { types, ...connectionPluginConfig } = newSettings.connections + if (types) { + state.settings.connections.types = + state.settings.connections.types ?? {} + Object.assign(state.settings.connections.types, types) + } + Object.assign(state.settings.connections, connectionPluginConfig) } - Object.assign(state.settings.connections, connectionPluginConfig) - } + }, }, + }, + public: { queryType, mutationType, objectType, @@ -129,6 +141,8 @@ export function create(): SchemaInternal { extendInputType, }, } + + return api } /** From 20133b75213fbc75d52c6957eae74edc93cdcf2d Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Mon, 3 Feb 2020 22:06:20 -0500 Subject: [PATCH 3/4] server --- src/framework/app.ts | 31 ++++++++++++++++++++----------- src/framework/index.ts | 15 +++++++++++++-- src/framework/server.ts | 20 ++++++++++++++------ src/framework/testing.ts | 10 +++++++--- src/index.ts | 3 +++ 5 files changed, 57 insertions(+), 22 deletions(-) diff --git a/src/framework/app.ts b/src/framework/app.ts index d68c3b060..55c87124d 100644 --- a/src/framework/app.ts +++ b/src/framework/app.ts @@ -10,16 +10,10 @@ import * as singletonChecks from './singleton-checks' const log = Logger.create({ name: 'app' }) -/** - * The available server options to configure how your app runs its server. - */ -type ServerOptions = Partial< - Pick -> - type Request = HTTP.IncomingMessage & { log: Logger.Logger } -// TODO plugins could augment the request +// todo the jsdoc below is lost on the destructured object exports later on... +// todo plugins could augment the request // plugins will be able to use typegen to signal this fact // all places in the framework where the req object is referenced should be // actually referencing the typegen version, so that it reflects the req + @@ -43,7 +37,7 @@ export type App = { /** * todo */ - start: (config?: ServerOptions) => Promise + start: () => Promise /** * todo */ @@ -74,17 +68,23 @@ export type App = { type SettingsInput = { logger?: Logger.SettingsInput schema?: Schema.SettingsInput + server?: Server.ExtraSettingsInput } type SettingsData = Readonly<{ logger: Logger.SettingsData schema: Schema.SettingsData + server: Server.ExtraSettingsData }> /** * todo */ type Settings = { + /** + * todo + */ + original: SettingsData /** * todo */ @@ -120,11 +120,20 @@ export function create(appConfig?: { types?: any }): App { if (newSettings.schema) { schema.private.settings.change(newSettings.schema) } + if (newSettings.server) { + Object.assign(settings.current.server, newSettings.server) + } }, current: { logger: log.settings, schema: schema.private.settings.data, + server: { ...Server.defaultExtraSettings }, }, + original: Lo.cloneDeep({ + logger: log.settings, + schema: schema.private.settings.data, + server: { ...Server.defaultExtraSettings }, + }), } const api: App = { @@ -142,7 +151,7 @@ export function create(appConfig?: { types?: any }): App { * Start the server. If you do not call this explicitly then nexus will * for you. You should not normally need to call this function yourself. */ - async start(opts: ServerOptions = {}): Promise { + async start(): Promise { // Track the start call so that we can know in entrypoint whether to run // or not start for the user. singletonChecks.state.is_was_server_start_called = true @@ -278,7 +287,7 @@ export function create(appConfig?: { types?: any }): App { schema: await schema.private.compile(nexusConfig), plugins, contextContributors, - ...opts, + ...settings.current.server, }).start() }, async stop() { diff --git a/src/framework/index.ts b/src/framework/index.ts index b69afcf12..3b76aa7fb 100644 --- a/src/framework/index.ts +++ b/src/framework/index.ts @@ -1,7 +1,18 @@ import * as App from './app' const app = App.create() -const { log, schema, server, settings } = app export default app -export { log, schema, server, settings } + +// Destructure app for export +// Do not use destructuring syntax +// Breaks jsdoc, only first destructed member annotated +// todo jsdoc + +export const log = app.log + +export const schema = app.schema + +export const server = app.server + +export const settings = app.settings diff --git a/src/framework/server.ts b/src/framework/server.ts index 4576e1150..531310c0f 100644 --- a/src/framework/server.ts +++ b/src/framework/server.ts @@ -15,7 +15,7 @@ const log = Logger.create({ name: 'server' }) * The default server options. These are merged with whatever you provide. Your * settings take precedence over these. */ -const optDefaults: Required = { +export const defaultExtraSettings: Required = { port: typeof process.env.NEXUS_PORT === 'string' ? parseInt(process.env.NEXUS_PORT, 10) @@ -33,8 +33,14 @@ const optDefaults: Required = { playground: process.env.NODE_ENV === 'production' ? false : true, } -type ExtraOptions = { +export type ExtraSettingsInput = { + /** + * todo + */ port?: number + /** + * todo + */ playground?: boolean /** * Create a message suitable for printing to the terminal about the server @@ -43,19 +49,21 @@ type ExtraOptions = { startMessage?: (address: { port: number; host: string; ip: string }) => void } +export type ExtraSettingsData = Required + /** * The available server options to configure how your app runs its server. */ -export type Options = createExpressGraphql.OptionsData & - ExtraOptions & { +export type SettingsInput = createExpressGraphql.OptionsData & + ExtraSettingsInput & { plugins: Plugin.RuntimeContributions[] contextContributors: ContextContributor[] } -export function create(optsGiven: Options) { +export function create(settingsGiven: SettingsInput) { const http = HTTP.createServer() const express = createExpress() - const opts = { ...optDefaults, ...optsGiven } + const opts = { ...defaultExtraSettings, ...settingsGiven } http.on('request', express) diff --git a/src/framework/testing.ts b/src/framework/testing.ts index 500c5ae3b..cfcd42e98 100644 --- a/src/framework/testing.ts +++ b/src/framework/testing.ts @@ -79,12 +79,16 @@ export async function createTestContext(): Promise { require(appModule) } - if (singletonChecks.state.is_was_server_start_called === false) { - await oldServerStart({ + app.settings.change({ + server: { port, playground: false, startMessage: () => '', - }) + }, + }) + + if (singletonChecks.state.is_was_server_start_called === false) { + await oldServerStart() } else { return Promise.resolve() } diff --git a/src/index.ts b/src/index.ts index 496dc3f8d..4dc163b8d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1 +1,4 @@ +import app from './framework' + export * from './framework' +export default app From 1454618fea74c460f2a740c5948ce47d5dd84974 Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Mon, 3 Feb 2020 22:12:54 -0500 Subject: [PATCH 4/4] uber minimal doc --- docs/guides/configuration.md | 2 +- docs/references/api.md | 39 +++++++++++++++++++++++++++--------- 2 files changed, 31 insertions(+), 10 deletions(-) diff --git a/docs/guides/configuration.md b/docs/guides/configuration.md index 5f7ecf24f..802ac1f6a 100644 --- a/docs/guides/configuration.md +++ b/docs/guides/configuration.md @@ -1 +1 @@ -`nexus` does not yet have a configuration system. We are working on its design in [#297](https://github.com/graphql-nexus/nexus-future/issues/297). +## TODO diff --git a/docs/references/api.md b/docs/references/api.md index ec2f9bccf..ee4807b39 100644 --- a/docs/references/api.md +++ b/docs/references/api.md @@ -23,7 +23,7 @@ schema.objectType({ #### `log` -An instance of [`RootLogger`](#rootlogger). +An instance of [`Logger`](#logger). **Example** @@ -49,6 +49,25 @@ Framework Notes: - If your app does not call `server.start` then `nexus` will. It is idiomatic to allow `nexus` to take care of this. If you deviate, we would love to learn about your use-case! +#### `settings` + +An instance of [`Settings`](#settings). + +**Example** + +```ts +import { log, settings } from 'nexus-future' + +settings.change({ + server: { + startMessage: info => { + settings.original.server.startMessage(info) + log.warn('stowaway message! :p') + }, + }, +}) +``` + ### `nexus-future/testing` todo @@ -102,14 +121,6 @@ TODO #### `server.stop` -### `RootLogger` - -TODO - -Extends [`Logger`](#logger) - -#### `rootLogger.settings` - ### `Logger` TODO @@ -131,3 +142,13 @@ TODO #### `logger.addToContext` #### `logger.child` + +### `Settings` + +TODO + +#### `change` + +#### `current` + +#### `original`