Skip to main content
Version: Draft

Registry

As you know: PostGraphile builds a GraphQL schema for you by introspecting your database. What you may not know is that it does this through multiple phases using the Graphile Build library. In the gather phase, PostGraphile introspects your database, and builds up a "registry" of all of the "codecs", "resources" and "relations" that it finds. Then during the schema phase, it inspects this registry and uses it to decide what your GraphQL schema should contain.

When you're just getting started with PostGraphile, it's not very important to understand the registry, you can get away with these basic concepts to help you to understand error messages:

  • A codec represents a database type (a scalar, composite, list, domain or range type)
  • A resource represents something in the database from which you can pull data (a table, view, materialized view or function)
  • A relation is a uni-directional link from a codec (e.g. a table type) to a resource (e.g. a table itself)

However, once you want to start writing your own plans, for example via extendSchema, understanding the registry becomes more important.

Codecs

A "codec" describes a type in your PostgreSQL database. There are built-in codecs for the basic scalars:

import { TYPES } from "postgraphile/@dataplan/pg";

const { int, bool, text /* ... */ } = TYPES;

PostGraphile will automatically generate codecs for all of the types in your database, whether they are scalar, composite (including the underlying type that each of your tables/views/etc have), list, domain, or range types.

PostGraphile automatically names these types after their name in the database via an inflector. For example, composite types use the classCodecName inflector.

You can read more about codecs (including how to make your own, and what the built-in scalar codecs are) in the @dataplan/pg documentation: codecs.

Custom codecs in PostGraphile

When PostGraphile cannot build a codec for a database type, you can supply one via the gather phase hook pgCodecs_findPgCodec, then wire it into your GraphQL schema during the schema phase. Create a plugin, export it, and load it from your graphile.config.ts preset. The core PgLtreePlugin is a tested example of this pattern; the following sample embeds its source (renamed to avoid conflicts).

Click to reveal MyPgLtreePlugin
plugins/MyPgLtreePlugin.ts
import assert from "node:assert";

import type { PgCodec } from "@dataplan/pg";
import { gatherConfig } from "graphile-build";

interface State {
ltreeCodec: PgCodec<string, any, any, any, undefined, any, any>;
ltreeArrayCodec: PgCodec;
}
interface Cache {}

// Optional: declaration merge the plugin name so users get autocomplete in `disablePlugins: [...]`
declare global {
namespace GraphileConfig {
interface Plugins {
MyPgLtreePlugin: true;
}
}
}

export const MyPgLtreePlugin: GraphileConfig.Plugin = {
name: "MyPgLtreePlugin",
// !Hide
version,

gather: gatherConfig<never, State, Cache>({
initialState(cache, { lib }) {
const {
dataplanPg: { listOfCodec },
graphileBuild: { EXPORTABLE },
sql,
} = lib;
const ltreeCodec: PgCodec<string, any, any, any, undefined, any, any> =
EXPORTABLE(
(sql) => ({
name: "ltree",
sqlType: sql`ltree`,
toPg(str) {
return str;
},
fromPg(str) {
return str;
},
executor: null,
attributes: undefined,
}),
[sql],
);
const ltreeArrayCodec = EXPORTABLE(
(listOfCodec, ltreeCodec) => listOfCodec(ltreeCodec),
[listOfCodec, ltreeCodec],
);
return { ltreeCodec, ltreeArrayCodec };
},
hooks: {
async pgCodecs_findPgCodec(info, event) {
// If another plugin has already supplied a codec; skip
if (event.pgCodec) return;

const { serviceName, pgType } = event;
const typname = pgType.typname;
if (typname !== "ltree" && typname !== "_ltree") return;

const ltreeExt = await info.helpers.pgIntrospection.getExtensionByName(
serviceName,
"ltree",
);
if (!ltreeExt || pgType.typnamespace !== ltreeExt.extnamespace) {
return;
} else if (typname === "ltree") {
event.pgCodec = info.state.ltreeCodec;
} else {
assert(typname === "_ltree");
event.pgCodec = info.state.ltreeArrayCodec;
}
},
},
}),
schema: {
hooks: {
init(_, build) {
const codec = build.pgCodecs.ltree;
if (codec) {
// Highly recommended you use your own inflector here, choosing
// `builtin` will mean your users may get conflicts which they cannot
// then resolve through inflection.
const ltreeTypeName = build.inflection.builtin("LTree");

build.registerScalarType(
ltreeTypeName,
{ pgCodec: codec },
() => ({
description: build.wrapDescription(
"Represents an `ltree` hierarchical label tree as outlined in https://www.postgresql.org/docs/current/ltree.html",
"type",
),
// !Hide
// TODO: specifiedByURL: https://postgraphile.org/scalars/ltree
}),
'Adding "LTree" scalar type from MyPgLtreePlugin.',
);
build.setGraphQLTypeForPgCodec(codec, "output", ltreeTypeName);
build.setGraphQLTypeForPgCodec(codec, "input", ltreeTypeName);
}
return _;
},
},
},
};
graphile.config.ts
import { PostGraphileAmberPreset } from "postgraphile/presets/amber";
import { MyPgLtreePlugin } from "./plugins/MyPgLtreePlugin.ts";

const preset: GraphileConfig.Preset = {
extends: [PostGraphileAmberPreset],
plugins: [MyPgLtreePlugin],
};

export default preset;
Is your Postgres type really a GraphQL scalar?

If you are mapping a list-like database type to a GraphQL scalar, consider whether a dedicated object type (and input object type) provides a clearer schema. Scalars are best for opaque values.

For more details on codecs, see the @dataplan/pg codec reference above.

Resources

A "resource" represents something that you can pull data from in your database. Most commonly this is a table, but it also includes views, materialized views and functions. You can even build resources for custom SQL expressions should you wish.

PostGraphile automatically builds resources for you based on all your tables, views, materialized views and functions.

There are two main classes of resources. "Table-like" resources don't accept any parameters, you can get resources from them directly using resource.find(spec) or resource.get(spec). "Function-like" resources require a list of parameters (even an empty list), and for these you would use resource.execute(args).

You can read more about resources in the @dataplan/pg documentation: resources.

Relations

A "relation" is a uni-directional (one way) relationship from a codec (i.e. type) to a table-like resource (i.e. table). Assuming you have some data for the given codec (whether you got that data from a table, function, or even read it from a file), a relation describes how to get from that to the related records (or record) in the given resource.

In general, foreign key constraints will register two relations, one for the referring table (the table on which the foreign key is defined) to the referenced table (this is the "forward" relation, and is always unique) and one from the referenced table back to the referring table (this is the "backward" or "referencee" relation, and may or may not be unique depending on the unique constraints on the referring table).

You can read more about relations in the @dataplan/pg documentation: relations.

Registry

The registry is the container for codecs, resources, and relations. When you're writing a plugin, if you have a reference to the build object then you can access the registry via build.input.pgRegistry. It contains the properties pgExecutor, pgCodecs, pgResources and pgRelations.

For improved DX, some shortcuts are added to the build object: build.pgExecutor (the primary executor; most schemas only have one), build.pgCodecs, build.pgResources, and build.pgRelations.

If you had a users table then, depending on the inflectors you're using, it's codec might be build.pgCodecs.users, its resource build.pgResources.users and its relations a keyed object (hash/map/record) stored at build.pgRelations.users.

You can read more about the registry in the @dataplan/pg documentation: registry.