graphile-search-plugin
Version:
Generate search conditions for your tsvector columns (PostGraphile v5)
156 lines (155 loc) • 7.14 kB
JavaScript
/**
* TsvectorCodecPlugin
*
* Teaches PostGraphile v5 how to handle PostgreSQL's tsvector and tsquery types.
* Without this, tsvector columns are invisible to the schema builder and the
* search plugin cannot generate condition fields.
*
* This plugin:
* 1. Creates codecs for tsvector/tsquery via gather.hooks.pgCodecs_findPgCodec
* 2. Registers a custom "FullText" scalar type for tsvector columns
* 3. Maps tsvector codec to the FullText scalar (isolating filter operators)
* 4. Maps tsquery codec to GraphQL String
* 5. Optionally hides tsvector columns from output types
*/
import { GraphQLString } from 'graphql';
import sql from 'pg-sql2';
/**
* Creates a TsvectorCodecPlugin with the given options.
*
* @param options - Plugin configuration
* @returns GraphileConfig.Plugin
*/
export function createTsvectorCodecPlugin(options = {}) {
const { fullTextScalarName = 'FullText', hideTsvectorColumns = false, } = options;
return {
name: 'TsvectorCodecPlugin',
version: '1.0.0',
gather: {
hooks: {
async pgCodecs_findPgCodec(info, event) {
if (event.pgCodec) {
return;
}
const { pgType: type, serviceName } = event;
const pgCatalog = await info.helpers.pgIntrospection.getNamespaceByName(serviceName, 'pg_catalog');
if (!pgCatalog) {
return;
}
if (type.typnamespace === pgCatalog._id && type.typname === 'tsvector') {
event.pgCodec = {
name: 'tsvector',
sqlType: sql.identifier('pg_catalog', 'tsvector'),
fromPg: (value) => value,
toPg: (value) => value,
attributes: undefined,
executor: null,
extensions: {
oid: type._id,
pg: {
serviceName,
schemaName: 'pg_catalog',
name: 'tsvector',
},
},
};
return;
}
if (type.typnamespace === pgCatalog._id && type.typname === 'tsquery') {
event.pgCodec = {
name: 'tsquery',
sqlType: sql.identifier('pg_catalog', 'tsquery'),
fromPg: (value) => value,
toPg: (value) => value,
attributes: undefined,
executor: null,
extensions: {
oid: type._id,
pg: {
serviceName,
schemaName: 'pg_catalog',
name: 'tsquery',
},
},
};
return;
}
},
},
},
schema: {
hooks: {
// Must run before PgCodecsPlugin's init (to avoid "unknown codec" warning)
// and before PgConnectionArgFilterPlugin's init (which creates filter
// types like FullTextFilter based on codec→GraphQL type mappings).
init: {
before: ['PgCodecs', 'PgConnectionArgFilterPlugin'],
callback(_, build) {
const { setGraphQLTypeForPgCodec } = build;
// Register a custom scalar type for tsvector columns.
// This ensures filter operators like `matches` only appear on
// tsvector filters, not on all String filters.
build.registerScalarType(fullTextScalarName, {}, () => ({
description: 'A full-text search tsvector value represented as a string.',
serialize(value) {
return String(value);
},
parseValue(value) {
if (typeof value === 'string') {
return value;
}
throw new Error(`${fullTextScalarName} must be a string`);
},
parseLiteral(lit) {
if (lit.kind === 'NullValue')
return null;
if (lit.kind !== 'StringValue') {
throw new Error(`${fullTextScalarName} must be a string`);
}
return lit.value;
},
}), `TsvectorCodecPlugin registering ${fullTextScalarName} scalar`);
for (const codec of Object.values(build.input.pgRegistry.pgCodecs)) {
if (codec.name === 'tsvector') {
setGraphQLTypeForPgCodec(codec, 'input', fullTextScalarName);
setGraphQLTypeForPgCodec(codec, 'output', fullTextScalarName);
}
else if (codec.name === 'tsquery') {
setGraphQLTypeForPgCodec(codec, 'input', GraphQLString.name);
setGraphQLTypeForPgCodec(codec, 'output', GraphQLString.name);
}
}
return _;
},
},
},
...(hideTsvectorColumns
? {
entityBehavior: {
pgCodecAttribute: {
inferred: {
after: ['postInferred'],
provides: ['hideTsvectorColumns'],
callback(behavior, [codec, attributeName]) {
const attr = codec.attributes?.[attributeName];
if (attr?.codec?.name === 'tsvector') {
return [behavior, '-select'];
}
return behavior;
},
},
},
},
}
: {}),
},
};
}
/**
* Default static instance using default options.
* Maps tsvector to the "FullText" scalar.
*/
export const TsvectorCodecPlugin = createTsvectorCodecPlugin();
export const TsvectorCodecPreset = {
plugins: [TsvectorCodecPlugin],
};