Skip to content

Commit

Permalink
graphile config doc overhaul (#2282)
Browse files Browse the repository at this point in the history
  • Loading branch information
benjie authored Dec 19, 2024
2 parents b9c4e89 + 0a62056 commit dd081e8
Show file tree
Hide file tree
Showing 10 changed files with 881 additions and 420 deletions.
2 changes: 1 addition & 1 deletion utils/graphile-config/src/functionality.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export function orderedApply<
after: (keyof GraphileConfig.Plugins | keyof GraphileConfig.Provides)[];
callback: UnwrapCallback<TFunctionality[keyof TFunctionality]>;
};
// Normalize all the hooks and gather them into collections
// Normalize all the plugin "functionalities" and gather them into collections
const allFunctionalities: {
[key in keyof TFunctionality]?: Array<FullFunctionalitySpec>;
} = Object.create(null);
Expand Down
52 changes: 24 additions & 28 deletions utils/website/graphile-config/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,38 +5,34 @@ sidebar_position: 1
# Graphile Config

**PRERELEASE**: this is pre-release software; use at your own risk. This will
likely change a lot before it's ultimately released.
likely change a lot before it is ultimately released.

`graphile-config` provides a standard plugin interface and helpers that can be
used across the entire of the Graphile suite. Primarily users will only use this
as `import type Plugin from 'graphile-config';` so that they can export plugins.
Graphile Config helps Node.js library authors make their libraries configurable
and _extensible_. Graphile Config is used across the Graphile suite to provide a
standard configuration and plugin interface.

This package provides two interfaces: `Plugin` and `Preset`
## Features

## Supporting TypeScript ESM
- Define and document strongly typed configuration options for your library.
- Allow users to extend the functionality of your library via plugins.
- Plugins can add their own additional configuration options.
- Bundle configuration options and plugins into default presets for your users.
- You and your users can compose presets with preset extension.
- Allow your users to share configuration across multiple modes (e.g. CLI and library).
- Powerful middleware system to make your library extensible.
- Users don't need to put plugins in a particular order, thanks to the ordering system.
- View the available options and resolved values of a preset with the `graphile`
CLI
([available to sponsors](https://github.com/graphile/crystal/blob/main/utils/graphile/README.md)).

You can specify a `graphile.config.ts` file; but if that uses `export default`
and your TypeScript is configured to export ESM then you'll get an error telling
you that you cannot `require` an ES Module:
## Different Users

```js
Error [ERR_REQUIRE_ESM]: Must use import to load ES Module: /path/to/graphile.config.ts
require() of ES modules is not supported.
require() of /path/to/graphile.config.ts from /path/to/node_modules/graphile-config/dist/loadConfig.js is an ES module file as it is a .ts file whose nearest parent package.json contains "type": "module" which defines all .ts files in that package scope as ES modules.
Instead change the requiring code to use import(), or remove "type": "module" from /path/to/package.json.
```
As a user of Graphile Config, you may not need to understand everything. There
are three common levels of usage, in order of the amount of knowledge required:

Or, in newer versions, an error saying unknown file extension:
1. Library consumers ⚙️
2. Plugin authors 🔌
3. Library authors 📚

```js
TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".ts" for /path/to/graphile.config.ts
```

To solve this, use the experimental loaders API to add support for TS ESM via
the `ts-node/esm` loader:

```js
export NODE_OPTIONS="$NODE_OPTIONS --loader ts-node/esm"
```

Then run your command again.
Each section in the Graphile Config docs will indicate the intended audience.
Feel free to learn only what you need, or learn it all!
158 changes: 158 additions & 0 deletions utils/website/graphile-config/library-authors.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
---
sidebar_position: 4
title: "Library Authors"
---

# Library Authors

Graphile Config is currently only used by Graphile projects. Should you find it
useful for other projects, please reach out via GitHub issues and we can discuss
what is necessary to make this more universal. Should you decide to not heed
this advice, please at least make sure that the scopes you add are named to
reduce the likelihood of future conflicts with features we may wish to add.

## Naming scopes

By convention, scopes are camelCase strings. Scopes should be descriptive enough
to reduce the chance of either conflicts across libraries or conflicts with
future additions to Graphile Config.

<details>

<summary>Click for specifically reserved scope names</summary>

The following strings are reserved by Graphile Config, and should not be used as
preset scopes or plugin scopes:

- Anything beginning with an underscore (`_`)
- after
- appendPlugins (to avoid confusion with PostGraphile v4 plugins)
- before
- callback
- description
- default (to enable compatibility with the various ESM emulations)
- disablePlugins
- experimental
- export
- exports
- extend
- extends
- functionality
- id
- import
- imports
- include
- includes
- label
- name
- plugin
- plugins
- prependPlugins (to avoid confusion with PostGraphile v4 plugins)
- provides
- skipPlugins (to avoid confusion with PostGraphile v4 plugins)
- title

</details>

## Middleware

(This section was primarily written by Benjie for Benjie... so you may want to
skip it.)

If you need to create a middleware system for your library, you might follow
something along these lines (replacing `libraryName` with the name of your
library):

```ts title="src/interfaces.ts"
// Define the middlewares that you support, their event type and their return type
export interface MyMiddleware {
someAction(event: SomeActionEvent): PromiseOrDirect<SomeActionResult>;
}
interface SomeActionEvent {
someParameter: number;
/*
* Use a per-middleware-method interface to define the various pieces of
* data relevant to this event. **ALWAYS** use the event as an abstraction
* so that new information can be added in future without causing any
* knock-on consequences. Note that these parameters of the event may be
* mutated. The values here can be anything, they don't need to be simple
* values.
*/
}
// Middleware wraps a function call; this represents whatever the function returns
type SomeActionResult = number;

export type PromiseOrDirect<T> = Promise<T> | T;
```

```ts title="src/index.ts"
import type { MiddlewareHandlers } from "graphile-config";

// Extend Plugin with support for registering handlers for the middleware activities:
declare global {
namespace GraphileConfig {
interface Plugin {
libraryName?: {
middleware?: MiddlewareHandlers<MyMiddleware>;
};
}
}
}
```

```ts title="src/getMiddleware.ts"
import { Middleware, orderedApply, resolvePreset } from "graphile-config";

export function getMiddleware(resolvedPreset: GraphileConfig.ResolvedPreset) {
// Create your middleware instance. The generic describes the events supported
const middleware = new Middleware<MyMiddleware>();
// Now apply the relevant middlewares registered by each plugin (if any) to the
// Middleware instance
orderedApply(
resolvedPreset.plugins,
(plugin) => plugin.libraryName?.middleware,
(name, fn, _plugin) => {
middleware.register(name, fn as any);
},
);
}
```

```ts title="src/main.ts"
// Get the user's Graphile Config from somewhere, e.g.
import config from "./graphile.config.js";

// Resolve the above config, recursively applying all the presets it extends from
const resolvedPreset = resolvePreset(config);

// Get the middleware for this preset
const middleware = getMiddleware(resolvedPreset);

// Then in the relevant place in your code, call the middleware around the
// relevant functionality
const result = await middleware.run(
"someAction",
{ someParameter: 42 }, // < `event` object
async (event) => {
// Note: `event` will be the same object as above, but its contents may
// have been modified by middlewares.
const { someParameter } = event;

// Call the underlying method to perform the action.
return await someAction(someParameter);
},
);
// The value of `result` should match the return value of `someAction(...)`
// (unless a middleware tweaked or replaced it, of course!)

// This is the thing that your middleware wraps. It can do anything, it's just
// an arbitrary JavaScript function.
function someAction(someParameter: number): PromiseOrDirect<SomeActionResult> {
// Do something here...
if (Math.random() < 0.5) {
return someParameter;
} else {
return sleep(200).then(() => someParameter);
}
}
```
Loading

0 comments on commit dd081e8

Please sign in to comment.