@graphile/pro
Version:
Professional PostGraphile enhancements
306 lines (220 loc) • 11 kB
Markdown
# /pro
This is a PostGraphile server plugin that includes a number of optional
protections for your production server. Unlike PostGraphile, this plugin is
_NOT_ open source software - see "License key" lower down.
Current protections:
- Force pagination caps (require user to supply a 'first' or 'last' argument to collections, customise or disable per table via `` smart comment) - omitting limits from collection fetches is the number one cause of slow queries in PostGraphile applications, this option forces you to use pagination
- Ability to send queries to read replicas to increase scalability
- Limit GraphQL query depth
- GraphQL cost limit (experimental! Estimates the cost of a GraphQL query before sending it to the database, applies a limit)
- Schema cycle detection via `schema.extensions.cycles`
To read about these protections, see:
https://www.graphile.org/postgraphile/production/
## Usage
### CLI:
```bash
yarn add postgraphile /pro
export GRAPHILE_LICENSE="MY_LICENSE_KEY_HERE"
yarn postgraphile --plugins /pro
```
Note: the `--plugins` option must be the **first** option passed to `postgraphile`.
Optional CLI flags:
- `--read-only-connection <string>`:: pass the PostgreSQL connection string to use for read-only queries (i.e. not mutations) - typically for connecting to replicas via PgBouncer or similar
- `--default-pagination-cap [int]`:: Ensures all connections have first/last specified and are no large than this value (default: 50), set to ' -1' to disable; override via smart comment ` 50`
- `--graphql-depth-limit [string]`:: Validates GraphQL queries cannot be deeper than the specified options (default: {maxDepth:16,maxListDepth:6,maxSelfReferentialDepth:5,revealDetails:true}), set to ' -1' to disable
- `--graphql-cost-limit [int]`:: [experimental] Only allows queries with a computed cost below the specified int (default: 30000), set to ' -1' to disable
### Library:
Install:
```bash
yarn add postgraphile /pro
```
Add the `pluginHook` and relevant options to your server:
```js
// server.js
const http = require("http");
const { postgraphile, makePluginHook } = require("postgraphile");
const pluginHook = makePluginHook([require("@graphile/pro").default]);
http
.createServer(
postgraphile(process.env.DATABASE_URL, "public", {
license: process.env.GRAPHILE_LICENSE,
pluginHook,
// Same as CLI options:
readOnlyConnection: "postgres://user:pass@bouncer_host/db",
defaultPaginationCap: 50, // -1 to disable
graphqlDepthLimit: {
maxDepth: 16, // -1 to disable
maxListDepth: 6,
maxSelfReferentialDepth: 5,
revealDetails: true, // NOTICE: probably best to set this to false in production
},
graphqlCostLimit: 30000, // -1 to disable
// If true (default): reveal the query cost to clients to help them optimise queries
exposeGraphQLCost: true,
// Add schema cycle metadata to `schema.extensions.cycles`
graphileBuildOptions: {
maxPgCycleDepth: 4,
},
})
)
.listen(5000);
```
Set your `GRAPHILE_LICENSE` environmental variable and run the server:
```bash
export GRAPHILE_LICENSE="MY_LICENSE_KEY_HERE"
node server.js
```
**IMPORTANT**: this plugin does not "phone home" so you'll need to update your
license at least once every 9 months. You can check the expiry date of your
current license
[in the Graphile Store validator](https://store.graphile.com/validate) and log
in there to generate a new license code.
## Overriding limits on a pre-request basis
Sometimes you may wish that certain requests have higher (or lower) limits than
others. For example you might wish to have higher limits for requests
originating from your own applications (vs third-party apps), or let logged in
users run more complex queries, or allow raising the pre-request limits for
users who are on a higher payment plan.
`/pro` enables you to achieve this when you are using PostGraphile as
a library by providing the `overrideOptionsForRequest` function:
```js
app.use(
postgraphile(DATABASE_URL, SCHEMA_NAME, {
// ...
defaultPaginationCap: 10,
graphqlDepthLimit: {
maxDepth: 5,
maxListDepth: 2,
maxSelfReferentialDepth: 2,
revealDetails: false,
},
graphqlCostLimit: 500,
exposeGraphQLCost: false,
overrideOptionsForRequest(req) {
if (req.user) {
/* Logged in; raise limits: */
return {
defaultPaginationCap: 100,
graphqlDepthLimit: {
maxDepth: 8,
maxListDepth: 4,
maxSelfReferentialDepth: 3,
revealDetails: true,
},
graphqlCostLimit: 3000,
exposeGraphQLCost: true,
};
} else {
return null;
}
},
})
);
```
This function can only override certain options (see example); and it's
**synchronous**. If you need to call asynchronous code to figure out what the
limits should be, you should do that in a middleware before PostGraphile is
mounted.
## Smart comments
### ``
To override the `--default-pagination-cap` option for a specific table or function, you may add a `` smart comment to the table/function specifying the replacement cap. Remember, set cap to `-1` to disable. For example:
```sql
-- Disable pagination cap on my function
comment on function my_function() is E' -1';
-- Raise pagination cap on forums
comment on table forums is E' 500';
```
### `` (default 100000)
Applies to tables. If you've disabled pagination caps and someone requests a
root `{allForums{nodes{id}}}`, what's the worst cast for how many rows this
might return? Defaults to 100000, override with smart comment:
```sql
comment on table forums is E' 200';
```
NOTE: collection costs are not factored into the query cost linearly.
### `` (default 1000)
Applies to tables. Similar to ``, but for filtered
collections (e.g. relations, such as
`{currentUser{forumsByUserId{nodes{id}}}}`
```sql
comment on table forums is E' 50';
```
### ``
Applies to functions. If the user supplies no limits what's the worst case
for the number of records returned?
Defaults to 1000 for custom queries, 50 for computed columns.
Override via smart comment:
```sql
comment on function my_custom_query() is E' 50';
```
## GraphQL depth limit
For the options accepted, see [the /depth-limit
documentation](https://github.com/graphile/depth-limit#options).
## GraphQL cost limit
This feature is experimental; it allows us to estimate the cost of a query
_before_ sending it to the database. It has been honed by timing a wide array
of varied queries against a database and seeing how nested collections and
function calls affect the performance of the query.
Besides being useful if you wish to open your GraphQL API up to the world in
production, this feature is also useful in development as it allows you to
see how expensive we estimate your query is whilst you compose it in GraphiQL
or similar - it's a great way of determining that your query is too complex
before you get performance issues in production.
We recommend that you benchmark queries on a full database and then come up
with a query cost that works for you. A value of `2000` should be high enough
for simple workloads that use pagination and don't nest large collections too
deeply.
The numbers produced by this may change slightly in different versions of
`/pro`; we recommend that you ensure your queries remain well under
the limit you set, and that you have a test suite that ensures all your queries
are under the limit before you do a release.
## Schema cycle detection
`/pro` can annotate the built GraphQL schema with
`schema.extensions.cycles`, an array of arrays of schema coordinates showing
detected cycles through relations, connections, edges, and nodes.
Enable and tune this via `graphileBuildOptions.maxPgCycleDepth`:
```js
postgraphile(DATABASE_URL, SCHEMA_NAME, {
license: process.env.GRAPHILE_LICENSE,
pluginHook,
graphileBuildOptions: {
maxPgCycleDepth: 4,
},
});
```
We recommend a value in the range `2-4`; `4` is a reasonable default. If you
hit schema build performance issues, lower this value.
This is designed to be used alongside `-pro/checks` (sponsors only).
That package can use `gqlcheck` (OSS) to discourage introducing cycles in new
operations, or in new positions within existing operations, without
invalidating all of your existing code at once.
## License key
You must specify the license key in an environmental variable
`GRAPHILE_LICENSE`, or pass it via the `license` option to the library. You
can acquire the license key from https://store.graphile.com
If you fail to provide the license key, then the module will throw an error.
**IMPORTANT**: this plugin does not "phone home" so you'll need to update your
license at least once every 9 months. You can check the expiry date of your
current license
[in the Graphile Store validator](https://store.graphile.com/validate) and log
in there to generate a new license code.
Read more: https://www.graphile.org/postgraphile/plugins/#first-party-premium-plugins
## Changelog
2.1.1 - fix cycle detection to honour unique keys
2.1.0 - detect cycles in PostGraphile relationships, to help avoid expensive queries on the client side
2.0.0 - switch to [/depth-limit](https://github.com/graphile/depth-limit) validation rule; be sure to customize the options!
1.0.4 - add missing error handling to `readOnlyConnection` Pool
1.0.3 - support wider range of /pg
1.0.2 - adds support for overriding `defaultPaginationCap` via `overrideOptionsForRequest`
1.0.1 - adds `postgraphile` to `peerDependencies`
1.0.0 - fixes --read-only-connection integration with --allow-explain; bump to stable
0.11.0 - GraphQL v15 support (for PostGraphile ^4.10.0)
0.10.0 - behavior change from throwing error on invalid license to logging error and continuing without protections
0.9.1 - support pg@8.x, Node 14
0.9.0 - extend pagination limits and cost estimation to simple collections; add ability to override unbounded collection costs
0.8.1 - remove reference to `package.json` to improve compatibility with rollup
0.8.0 - allow overriding limits on a per-request basis; fixes an issue where errors are raised with the raw (pre-scaling) cost rather than the final cost
0.7.0 - match library defaults to CLI defaults; update README with CLI flags/library options/changelog
0.6.1 - fix the TypeScript definition file locations
0.6.0 - moved license check to runtime; convert to TypeScript; add support for subscriptions/live queries; fix conflicts when handling GraphQL validation errors
0.5.0 - released on NPM