GraphileConfig.Preset
Target Audience: all Graphile Config users ⚙️🔌📚
A preset bundles together a list of options and plugins to configure and extend
a library. GraphileConfig.Preset
is the primary interface that most Graphile
Config users will use.
Library consumers should define their preset as the default export of a
graphile.config.js
(or .ts
, .mjs
, etc.) file.
Here's an example in JavaScript:
const SomePlugin = require("some-plugin");
module.exports = {
plugins: [SomePlugin],
someScope: {
someConfigOption: 10,
},
};
And an equivalent configuration in TypeScript:
import type {} from "graphile-config";
import SomePlugin from "some-plugin";
const preset: GraphileConfig.Preset = {
plugins: [SomePlugin],
someScope: {
someConfigOption: 10,
},
};
export default preset;
Adding the import type {} from "graphile-config"
statement tells TypeScript
about the GraphileConfig
global namespace; this may or may not be necessary
depending on the other imports that you have, but we generally recommend it.
No code from the graphile-config
library will be included in the output
JavaScript for graphile.config.ts
above; see the TypeScript docs for more on
type-only imports.
Supporting TypeScript ESM
You can specify a graphile.config.ts
file, but if that uses export default
,
and your TypeScript is configured to export ESM, then you may get an error
telling you that you cannot require
an ES Module:
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.
Or, in newer versions, an error saying unknown file extension:
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:
export NODE_OPTIONS="$NODE_OPTIONS --loader ts-node/esm"
Then run your command again.
Preset scopes
Libraries that use Graphile Config define scopes. Scopes are properties in a
Graphile Config preset within which configuration options can be set. For
example, PostGraphile's dependencies define scopes
including pgServices
, schema
, grafast
, grafserv
, and more.
Graphile Worker defines a worker
scope.
Scopes are also used in plugins. Plugin authors 🔌 should see Plugin for more details.
Preset composition
Library authors can create Graphile Config presets that allow consumers to easily
use the library with default or recommended settings. The extends
property in
a preset allows library consumers to extend one or more presets with their own
configuration values or plugins. Extending presets resolves values according to
the preset resolution algorithm.
The following is a simple example of a Graphile Config preset that could be used to configure usage of Graphile Worker:
import { WorkerPreset } from "graphile-worker";
const preset: GraphileConfig.Preset = {
extends: [WorkerPreset],
worker: {
connectionString: "postgres:///my_db",
},
};
export default preset;
As a library consumer, you can build and share presets with your collaborators or even extend 3rd party presets provided by the community. As with any 3rd party code, take caution to ensure you can trust the code you are importing.
Preset resolution
Presets may compose (extend) zero or more other presets. When a library is
passed a preset, it resolves the preset using the ResolvePreset
algorithm
below.
TL;DR:
- All the presets in
extends
are resolved, recursively, depth-first, in order (order is important!). - The plugins are merged as a set (each plugin will only be included once) and
sorted according to
before
/after
. - The options are merged such that options specified last win.
ResolvePreset(preset):
- Let
flattenedPreset
beFlattenPreset(preset)
. - Let
resolvedPreset
be a copy offlattenedPreset
with theplugins
property sorted according to the plugin ordering rules. - Return
resolvedPreset
.
FlattenPreset(preset):
- Let
extends
be the list specified in theextends
property ofpreset
(or an empty list if none specified). - Let
extendsPreset
beMergePresets(extends)
. - Return
ExtendPreset(preset, extendsPreset)
.
MergePresets(presets):
- Let
mergedPreset
be an empty preset. - For each
preset
inpresets
:- Let
flattenedPreset
beFlattenPreset(preset)
. - Let
mergedPreset
beExtendPreset(mergedPreset, flattenedPreset)
.
- Let
- Return
mergedPreset
.
ExtendPreset(basePreset, extensionPreset):
- Note: this algorithm ignores the
extends
property of bothbasePreset
andextensionPreset
, assuming they has already been resolved elsewhere. - Let
mergedPreset
be an empty preset. - Let
plugins
be the unique list of plugins defined inbasePreset
followed those defined inextensionPreset
and not already defined inbasePreset
. - Let the list of plugins in
mergedPreset
beplugins
. - Let
scopes
be the set of scopes defined inbasePreset
union those defined inextensionPreset
. - For each
scope
inscopes
:- Let
baseScope
be the value of thescope
inbasePreset
. - Let
extendingScope
be the value of thescope
inextensionPreset
. - If
baseScope
andextendingScope
both exist:- Let
scope
inmergedPreset
be the result of mergingbaseScope
andextendingScope
akin toObject.assign({}, baseScope, extendingScope)
.
- Let
- Otherwise: let
scope
inmergedPreset
be whichever ofbaseScope
andextendingScope
actually exist.
- Let
- Return
mergedPreset
.
Consider a preset, APreset, that extends two other presets: Preset1 and
Preset2, each of which extends
the same preset, Preset0:
const Preset0 = { myScope: { option1: false, option2: false } };
const Preset1 = { extends: [Preset0], myScope: { option1: true } };
const Preset2 = { extends: [Preset0], myScope: { option2: true } };
const APreset = { extends: [Preset1, Preset2] };
Any overrides to the options set in Preset0 by Preset1 will be reset in APreset since they will be overridden when Preset2 applies the Preset0 options again:
// Resolving the presets operates depth first:
const Preset0 = { myScope: { option1: false, option2: false } };
const Preset1 = { myScope: { ...Preset0.myScope, option1: true } };
const Preset2 = { myScope: { ...Preset0.myScope, option2: true } };
const APreset = { myScope: { ...Preset1.myScope, ...Preset2.myScope } };
// Thus:
const APreset = { myScope: { option1: false, option2: true } };
For this reason, presets that are expected to be combined with other presets
should not extends
common or shared presets; instead, the end user should be
expected to add these presets themselves.
Said another way, the tree of presets should only ever have a depth of 2 or less to avoid unexpected behavior.
graphile
CLI
graphile
CLI!Graphile's open source projects only exist thanks to the sponsorship of
individuals and organizations that use them. To help convince your boss to fund
the ongoing development of the OSS that your company relies on, sponsorship
comes with perks. One such perk is the graphile
CLI which is only licensed for
usage by sponsors. See the
README
for more details.
The graphile
CLI has a config
subcommand with various tools to help library
consumers.
# Output the options your config may contain
npx graphile config options
# Print your resolved configuration
npx graphile config print