Skip to content

Instantly share code, notes, and snippets.

@benjie
Forked from mlipscombe/postgraphile-tsv-plugin.js
Last active June 24, 2018 07:30
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save benjie/446f216787105fdeb5d8bfbf8495fced to your computer and use it in GitHub Desktop.
Save benjie/446f216787105fdeb5d8bfbf8495fced to your computer and use it in GitHub Desktop.
full text search plugin for postgraphile (edited)
const TSVECTOR_TYPE_ID = 3614;
export const PostGraphileTSVPlugin = builder => {
builder.hook('infection', (inflection, build) => {
return build.extend(inflection, {
fullTextScalarTypeName() {
return `FullText`;
},
pgTsvRank(fieldName) {
return this.camelCase(`${fieldName}-rank`);
},
pgTsvOrderByColumnRankEnum(attr: PgAttribute, ascending: boolean) {
const columnName = this._columnName(attr, {
skipRowId: true, // Because we messed up 😔
});
return this.constantCase(
`${columnName}_rank_${ascending ? "asc" : "desc"}`
);
},
});
});
builder.hook('build', (build) => {
const {
addConnectionFilterOperator,
addType,
getTypeByName,
connectionFilterOperators,
pgSql: sql,
pgRegisterGqlTypeByTypeId: registerGqlTypeByTypeId,
pgRegisterGqlInputTypeByTypeId: registerGqlInputTypeByTypeId,
graphql: {
GraphQLInputObjectType, GraphQLString, GraphQLScalarType,
},
inflection,
newWithHooks,
} = build;
if (!(addConnectionFilterOperator instanceof Function)) {
throw new Error('PostGraphileTSVPlugin requires PostGraphileConnectionFilterPlugin to be loaded before it.');
}
const FullText = new GraphQLScalarType({
name: inflection.fullTextScalarTypeName(),
serialize(value) {
return value;
},
parseValue(value) {
return value;
},
parseLiteral(lit) {
return lit;
},
});
// Don't register the type until it's used
// addType(FullText);
registerGqlTypeByTypeId(TSVECTOR_TYPE_ID, () => FullText);
registerGqlInputTypeByTypeId(TSVECTOR_TYPE_ID, () => FullText);
addConnectionFilterOperator(
'matches',
'Performs a full text search on the field.',
() => GraphQLString,
(identifier, val, fieldName, queryBuilder) => {
queryBuilder.select(
sql.query`ts_rank(${identifier}, to_tsquery(${val}))`,
// I'd rather use `attr.name` over `fieldName` here:
`__${fieldName}Rank`, // Prefixing with two underscores means we can use this without worrying about aliases and conflicts
);
return sql.query`${identifier} @@ to_tsquery(${val})`;
},
{
allowedFieldTypes: ['FullText'],
},
);
const FullTextFilter = newWithHooks(GraphQLInputObjectType, {
name: 'FullTextFilter',
description: 'A filter to be used against `FullText` fields.',
fields: () => {
const operator = connectionFilterOperators.matches;
return {
matches: {
description: operator.description,
type: operator.resolveType(getTypeByName('FullText')),
},
};
},
}, {
isPgTSVFilterInputType: true,
});
addType(FullTextFilter);
return build;
});
builder.hook('GraphQLObjectType:fields', (fields, build, context) => {
const {
pgIntrospectionResultsByKind: introspectionResultsByKind,
pg2gql,
graphql: { GraphQLFloat },
pgColumnFilter,
inflection,
} = build;
const {
scope: { isPgRowType, isPgCompoundType, pgIntrospection: table },
fieldWithHooks,
} = context;
if (
!(isPgRowType || isPgCompoundType) ||
!table ||
table.kind !== 'class'
) {
return fields;
}
const tsvColumns = introspectionResultsByKind.attribute
.filter(attr => attr.classId === table.id)
.filter(attr => parseInt(attr.typeId, 10) === TSVECTOR_TYPE_ID);
if (tsvColumns.length === 0) {
return fields;
}
const tsvFields = tsvColumns
// Might want to add `@omit` logic here (for `filter`)
.filter(attr => pgColumnFilter(attr, build, context))
.reduce((memo, attr) => {
const fieldName = inflection.column(attr);
const rankFieldName = inflection.pgTsvRank(fieldName);
memo[rankFieldName] = fieldWithHooks(
rankFieldName,
{
description: 'The score ranking.',
type: GraphQLFloat,
resolve: (data, _args, _context) => pg2gql(data[`__${fieldName}Rank`], GraphQLFloat),
},
{},
);
return memo;
}, {});
return Object.assign({}, fields, tsvFields);
});
builder.hook('GraphQLEnumType:values', (values, build, context) => {
const {
extend,
pgSql: sql,
pgColumnFilter,
pgIntrospectionResultsByKind: introspectionResultsByKind,
inflection,
} = build;
const { scope: { isPgRowSortEnum, pgIntrospection: table } } = context;
if (!isPgRowSortEnum || !table || table.kind !== 'class') {
return values;
}
const tsvColumns = introspectionResultsByKind.attribute
.filter(attr => attr.classId === table.id)
.filter(attr => parseInt(attr.typeId, 10) === TSVECTOR_TYPE_ID);
if (tsvColumns.length === 0) {
return values;
}
return extend(
values,
tsvColumns
// Honor `@omit` here (for `order`)
.filter(attr => pgColumnFilter(attr, build, context))
.reduce((memo, attr) => {
const fieldName = inflection.column(attr);
const ascFieldName = inflection.pgTsvOrderByColumnRankEnum(attr, true);
const descFieldName = inflection.pgTsvOrderByColumnRankEnum(attr, false);
const findExpr = ({ queryBuilder }) => {
const expr = queryBuilder.data.select.filter(obj => obj[1] === `__${fieldName}Rank`);
return expr.length ? expr.shift()[0] : sql.fragment`1`;
};
memo[ascFieldName] = {
value: {
alias: `${attr.name}_rank_asc`,
specs: [[findExpr, true]],
},
};
memo[descFieldName] = {
value: {
alias: `${attr.name}_rank_desc`,
specs: [[findExpr, false]],
},
};
return memo;
}, {}),
`Adding TSV rank columns for sorting on table '${table.name}'`,
);
});
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment