graphile-search-plugin 1.1.1 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,155 @@
1
+ /**
2
+ * TsvectorCodecPlugin
3
+ *
4
+ * Teaches PostGraphile v5 how to handle PostgreSQL's tsvector and tsquery types.
5
+ * Without this, tsvector columns are invisible to the schema builder and the
6
+ * search plugin cannot generate condition fields.
7
+ *
8
+ * This plugin:
9
+ * 1. Creates codecs for tsvector/tsquery via gather.hooks.pgCodecs_findPgCodec
10
+ * 2. Registers a custom "FullText" scalar type for tsvector columns
11
+ * 3. Maps tsvector codec to the FullText scalar (isolating filter operators)
12
+ * 4. Maps tsquery codec to GraphQL String
13
+ * 5. Optionally hides tsvector columns from output types
14
+ */
15
+ import { GraphQLString } from 'graphql';
16
+ import sql from 'pg-sql2';
17
+ /**
18
+ * Creates a TsvectorCodecPlugin with the given options.
19
+ *
20
+ * @param options - Plugin configuration
21
+ * @returns GraphileConfig.Plugin
22
+ */
23
+ export function createTsvectorCodecPlugin(options = {}) {
24
+ const { fullTextScalarName = 'FullText', hideTsvectorColumns = false, } = options;
25
+ return {
26
+ name: 'TsvectorCodecPlugin',
27
+ version: '1.0.0',
28
+ gather: {
29
+ hooks: {
30
+ async pgCodecs_findPgCodec(info, event) {
31
+ if (event.pgCodec) {
32
+ return;
33
+ }
34
+ const { pgType: type, serviceName } = event;
35
+ const pgCatalog = await info.helpers.pgIntrospection.getNamespaceByName(serviceName, 'pg_catalog');
36
+ if (!pgCatalog) {
37
+ return;
38
+ }
39
+ if (type.typnamespace === pgCatalog._id && type.typname === 'tsvector') {
40
+ event.pgCodec = {
41
+ name: 'tsvector',
42
+ sqlType: sql.identifier('pg_catalog', 'tsvector'),
43
+ fromPg: (value) => value,
44
+ toPg: (value) => value,
45
+ attributes: undefined,
46
+ executor: null,
47
+ extensions: {
48
+ oid: type._id,
49
+ pg: {
50
+ serviceName,
51
+ schemaName: 'pg_catalog',
52
+ name: 'tsvector',
53
+ },
54
+ },
55
+ };
56
+ return;
57
+ }
58
+ if (type.typnamespace === pgCatalog._id && type.typname === 'tsquery') {
59
+ event.pgCodec = {
60
+ name: 'tsquery',
61
+ sqlType: sql.identifier('pg_catalog', 'tsquery'),
62
+ fromPg: (value) => value,
63
+ toPg: (value) => value,
64
+ attributes: undefined,
65
+ executor: null,
66
+ extensions: {
67
+ oid: type._id,
68
+ pg: {
69
+ serviceName,
70
+ schemaName: 'pg_catalog',
71
+ name: 'tsquery',
72
+ },
73
+ },
74
+ };
75
+ return;
76
+ }
77
+ },
78
+ },
79
+ },
80
+ schema: {
81
+ hooks: {
82
+ // Must run before PgCodecsPlugin's init (to avoid "unknown codec" warning)
83
+ // and before PgConnectionArgFilterPlugin's init (which creates filter
84
+ // types like FullTextFilter based on codec→GraphQL type mappings).
85
+ init: {
86
+ before: ['PgCodecs', 'PgConnectionArgFilterPlugin'],
87
+ callback(_, build) {
88
+ const { setGraphQLTypeForPgCodec } = build;
89
+ // Register a custom scalar type for tsvector columns.
90
+ // This ensures filter operators like `matches` only appear on
91
+ // tsvector filters, not on all String filters.
92
+ build.registerScalarType(fullTextScalarName, {}, () => ({
93
+ description: 'A full-text search tsvector value represented as a string.',
94
+ serialize(value) {
95
+ return String(value);
96
+ },
97
+ parseValue(value) {
98
+ if (typeof value === 'string') {
99
+ return value;
100
+ }
101
+ throw new Error(`${fullTextScalarName} must be a string`);
102
+ },
103
+ parseLiteral(lit) {
104
+ if (lit.kind === 'NullValue')
105
+ return null;
106
+ if (lit.kind !== 'StringValue') {
107
+ throw new Error(`${fullTextScalarName} must be a string`);
108
+ }
109
+ return lit.value;
110
+ },
111
+ }), `TsvectorCodecPlugin registering ${fullTextScalarName} scalar`);
112
+ for (const codec of Object.values(build.input.pgRegistry.pgCodecs)) {
113
+ if (codec.name === 'tsvector') {
114
+ setGraphQLTypeForPgCodec(codec, 'input', fullTextScalarName);
115
+ setGraphQLTypeForPgCodec(codec, 'output', fullTextScalarName);
116
+ }
117
+ else if (codec.name === 'tsquery') {
118
+ setGraphQLTypeForPgCodec(codec, 'input', GraphQLString.name);
119
+ setGraphQLTypeForPgCodec(codec, 'output', GraphQLString.name);
120
+ }
121
+ }
122
+ return _;
123
+ },
124
+ },
125
+ },
126
+ ...(hideTsvectorColumns
127
+ ? {
128
+ entityBehavior: {
129
+ pgCodecAttribute: {
130
+ inferred: {
131
+ after: ['postInferred'],
132
+ provides: ['hideTsvectorColumns'],
133
+ callback(behavior, [codec, attributeName]) {
134
+ const attr = codec.attributes?.[attributeName];
135
+ if (attr?.codec?.name === 'tsvector') {
136
+ return [behavior, '-select'];
137
+ }
138
+ return behavior;
139
+ },
140
+ },
141
+ },
142
+ },
143
+ }
144
+ : {}),
145
+ },
146
+ };
147
+ }
148
+ /**
149
+ * Default static instance using default options.
150
+ * Maps tsvector to the "FullText" scalar.
151
+ */
152
+ export const TsvectorCodecPlugin = createTsvectorCodecPlugin();
153
+ export const TsvectorCodecPreset = {
154
+ plugins: [TsvectorCodecPlugin],
155
+ };
package/esm/types.d.ts ADDED
@@ -0,0 +1,37 @@
1
+ /**
2
+ * PgSearch Plugin Types
3
+ *
4
+ * Type definitions for the PostGraphile v5 search plugin configuration.
5
+ */
6
+ /**
7
+ * Plugin configuration options.
8
+ */
9
+ export interface PgSearchPluginOptions {
10
+ /**
11
+ * Prefix for tsvector condition fields.
12
+ * For example, with prefix 'fullText' and a column named 'tsv',
13
+ * the generated condition field will be 'fullTextTsv'.
14
+ * @default 'tsv'
15
+ */
16
+ pgSearchPrefix?: string;
17
+ /**
18
+ * Whether to hide tsvector columns from output types.
19
+ * When true, tsvector columns won't appear as fields on the GraphQL object type.
20
+ * @default false
21
+ */
22
+ hideTsvectorColumns?: boolean;
23
+ /**
24
+ * Name of the custom GraphQL scalar for tsvector columns.
25
+ * This scalar isolates filter operators (like `matches`) to tsvector columns
26
+ * rather than all String fields.
27
+ * @default 'FullText'
28
+ */
29
+ fullTextScalarName?: string;
30
+ /**
31
+ * PostgreSQL text search configuration used with `websearch_to_tsquery`.
32
+ * Must match the configuration used when building your tsvector columns
33
+ * (e.g., `'english'`, `'simple'`, `'spanish'`).
34
+ * @default 'english'
35
+ */
36
+ tsConfig?: string;
37
+ }
package/esm/types.js ADDED
@@ -0,0 +1,6 @@
1
+ /**
2
+ * PgSearch Plugin Types
3
+ *
4
+ * Type definitions for the PostGraphile v5 search plugin configuration.
5
+ */
6
+ export {};
package/index.d.ts CHANGED
@@ -1,10 +1,26 @@
1
- export interface PgSearchPluginOptions {
2
- /** Prefix for tsvector fields, default is 'tsv' */
3
- pgSearchPrefix?: string;
4
- }
5
1
  /**
6
- * PgSearchPlugin - Generates search conditions for tsvector columns
2
+ * PostGraphile v5 Search Plugin
3
+ *
4
+ * Provides full-text search capabilities for tsvector columns.
5
+ *
6
+ * @example
7
+ * ```typescript
8
+ * import { PgSearchPlugin, PgSearchPreset } from 'graphile-search-plugin';
9
+ *
10
+ * // Option 1: Use the preset (recommended)
11
+ * const preset = {
12
+ * extends: [
13
+ * PgSearchPreset({
14
+ * pgSearchPrefix: 'fullText',
15
+ * }),
16
+ * ],
17
+ * };
18
+ *
19
+ * // Option 2: Use the plugin directly
20
+ * const plugin = PgSearchPlugin({ pgSearchPrefix: 'fullText' });
21
+ * ```
7
22
  */
8
- declare const PgSearchPlugin: (builder: any, options?: PgSearchPluginOptions) => void;
9
- export { PgSearchPlugin };
10
- export default PgSearchPlugin;
23
+ export { PgSearchPlugin, createPgSearchPlugin } from './plugin';
24
+ export { PgSearchPreset } from './preset';
25
+ export { TsvectorCodecPlugin, TsvectorCodecPreset, createTsvectorCodecPlugin, } from './tsvector-codec';
26
+ export type { PgSearchPluginOptions } from './types';
package/index.js CHANGED
@@ -1,60 +1,34 @@
1
1
  "use strict";
2
- // plugins/PgSearchPlugin.ts
3
- Object.defineProperty(exports, "__esModule", { value: true });
4
- exports.PgSearchPlugin = void 0;
5
2
  /**
6
- * PgSearchPlugin - Generates search conditions for tsvector columns
3
+ * PostGraphile v5 Search Plugin
4
+ *
5
+ * Provides full-text search capabilities for tsvector columns.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * import { PgSearchPlugin, PgSearchPreset } from 'graphile-search-plugin';
10
+ *
11
+ * // Option 1: Use the preset (recommended)
12
+ * const preset = {
13
+ * extends: [
14
+ * PgSearchPreset({
15
+ * pgSearchPrefix: 'fullText',
16
+ * }),
17
+ * ],
18
+ * };
19
+ *
20
+ * // Option 2: Use the plugin directly
21
+ * const plugin = PgSearchPlugin({ pgSearchPrefix: 'fullText' });
22
+ * ```
7
23
  */
8
- const PgSearchPlugin = (builder, options = {}) => {
9
- const { pgSearchPrefix = 'tsv' } = options;
10
- builder.hook('GraphQLInputObjectType:fields', (fields, build, context) => {
11
- const { inflection } = build;
12
- const { scope: { isPgCondition, pgIntrospection: table }, fieldWithHooks } = context;
13
- if (!isPgCondition || !table || table.kind !== 'class')
14
- return fields;
15
- const tsvs = table.attributes.filter((attr) => attr.type.name === 'tsvector');
16
- if (!tsvs.length)
17
- return fields;
18
- return build.extend(fields, tsvs.reduce((memo, attr) => {
19
- const fieldName = inflection.camelCase(`${pgSearchPrefix}_${attr.name}`);
20
- memo[fieldName] = fieldWithHooks(fieldName, { type: build.graphql.GraphQLString }, {});
21
- return memo;
22
- }, {}));
23
- });
24
- builder.hook('GraphQLObjectType:fields:field:args', (args, build, context) => {
25
- const { pgSql: sql, inflection } = build;
26
- const { scope: { isPgFieldConnection, isPgFieldSimpleCollection, pgFieldIntrospection: procOrTable, pgFieldIntrospectionTable: tableIfProc, }, addArgDataGenerator, } = context;
27
- const table = tableIfProc || procOrTable;
28
- if ((!isPgFieldConnection && !isPgFieldSimpleCollection) ||
29
- !table ||
30
- table.kind !== 'class') {
31
- return args;
32
- }
33
- const tsvs = table.attributes.filter((attr) => attr.type.name === 'tsvector');
34
- if (!tsvs.length)
35
- return args;
36
- tsvs.forEach((tsv) => {
37
- const conditionFieldName = inflection.camelCase(`${pgSearchPrefix}_${tsv.name}`);
38
- addArgDataGenerator(function addSearchCondition({ condition }) {
39
- if (!condition || !(conditionFieldName in condition))
40
- return {};
41
- const value = condition[conditionFieldName];
42
- if (value == null)
43
- return {};
44
- return {
45
- pgQuery: (queryBuilder) => {
46
- const tsquery = sql.fragment `websearch_to_tsquery('english', ${sql.value(value)})`;
47
- const tableAlias = queryBuilder.getTableAlias();
48
- // WHERE condition
49
- queryBuilder.where(sql.fragment `${tableAlias}.${sql.identifier(tsv.name)} @@ ${tsquery}`);
50
- // Automatically add ordering by relevance (descending)
51
- queryBuilder.orderBy(sql.fragment `ts_rank(${tableAlias}.${sql.identifier(tsv.name)}, ${tsquery})`, false);
52
- },
53
- };
54
- });
55
- });
56
- return args;
57
- }, [], ['PgConnectionArgOrderBy']);
58
- };
59
- exports.PgSearchPlugin = PgSearchPlugin;
60
- exports.default = PgSearchPlugin;
24
+ Object.defineProperty(exports, "__esModule", { value: true });
25
+ exports.createTsvectorCodecPlugin = exports.TsvectorCodecPreset = exports.TsvectorCodecPlugin = exports.PgSearchPreset = exports.createPgSearchPlugin = exports.PgSearchPlugin = void 0;
26
+ var plugin_1 = require("./plugin");
27
+ Object.defineProperty(exports, "PgSearchPlugin", { enumerable: true, get: function () { return plugin_1.PgSearchPlugin; } });
28
+ Object.defineProperty(exports, "createPgSearchPlugin", { enumerable: true, get: function () { return plugin_1.createPgSearchPlugin; } });
29
+ var preset_1 = require("./preset");
30
+ Object.defineProperty(exports, "PgSearchPreset", { enumerable: true, get: function () { return preset_1.PgSearchPreset; } });
31
+ var tsvector_codec_1 = require("./tsvector-codec");
32
+ Object.defineProperty(exports, "TsvectorCodecPlugin", { enumerable: true, get: function () { return tsvector_codec_1.TsvectorCodecPlugin; } });
33
+ Object.defineProperty(exports, "TsvectorCodecPreset", { enumerable: true, get: function () { return tsvector_codec_1.TsvectorCodecPreset; } });
34
+ Object.defineProperty(exports, "createTsvectorCodecPlugin", { enumerable: true, get: function () { return tsvector_codec_1.createTsvectorCodecPlugin; } });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "graphile-search-plugin",
3
- "version": "1.1.1",
4
- "description": "generate search conditions for your tsvector columns",
3
+ "version": "3.0.0",
4
+ "description": "Generate search conditions for your tsvector columns (PostGraphile v5)",
5
5
  "author": "Constructive <developers@constructive.io>",
6
6
  "homepage": "https://github.com/constructive-io/constructive",
7
7
  "license": "MIT",
@@ -10,8 +10,7 @@
10
10
  "types": "index.d.ts",
11
11
  "scripts": {
12
12
  "clean": "makage clean",
13
- "copy": "makage assets",
14
- "prepack": "pnpm run build",
13
+ "prepack": "npm run build",
15
14
  "build": "makage build",
16
15
  "build:dev": "makage build --dev",
17
16
  "lint": "eslint . --fix",
@@ -33,22 +32,37 @@
33
32
  "pgpm",
34
33
  "plugin",
35
34
  "postgres",
36
- "graphql"
35
+ "graphql",
36
+ "search",
37
+ "tsvector",
38
+ "full-text-search"
37
39
  ],
38
40
  "bugs": {
39
41
  "url": "https://github.com/constructive-io/constructive/issues"
40
42
  },
41
43
  "devDependencies": {
42
- "graphile-plugin-connection-filter": "^3.1.1",
43
- "graphile-plugin-fulltext-filter": "^3.1.1",
44
- "graphile-simple-inflector": "^1.1.1",
45
- "graphile-test": "^3.1.1",
46
- "makage": "^0.1.12",
47
- "pgsql-test": "^3.1.1"
44
+ "@types/node": "^22.19.1",
45
+ "graphile-test": "^4.0.0",
46
+ "makage": "^0.1.10",
47
+ "pgsql-test": "^4.0.0",
48
+ "postgraphile-plugin-connection-filter": "^3.0.0-rc.1"
48
49
  },
49
50
  "dependencies": {
50
- "graphile-build": "^4.14.1",
51
- "graphql-tag": "2.12.6"
51
+ "@dataplan/pg": "1.0.0-rc.3",
52
+ "graphile-build": "^5.0.0-rc.3",
53
+ "graphile-build-pg": "^5.0.0-rc.3",
54
+ "graphile-config": "1.0.0-rc.3",
55
+ "pg-sql2": "^5.0.0-rc.3"
52
56
  },
53
- "gitHead": "49049ad3ddd762d35625f657cb42fa0862b829a0"
57
+ "peerDependencies": {
58
+ "graphql": "^16.9.0",
59
+ "postgraphile": "^5.0.0-rc.4",
60
+ "postgraphile-plugin-connection-filter": "^3.0.0-rc.1"
61
+ },
62
+ "peerDependenciesMeta": {
63
+ "postgraphile-plugin-connection-filter": {
64
+ "optional": true
65
+ }
66
+ },
67
+ "gitHead": "b2daeefe49cdefb3d01ea63cf778fb9b847ab5fe"
54
68
  }
package/plugin.d.ts ADDED
@@ -0,0 +1,44 @@
1
+ /**
2
+ * PostGraphile v5 Search Plugin
3
+ *
4
+ * Generates search condition fields for tsvector columns. When a search term
5
+ * is provided via the condition input, this plugin applies a
6
+ * `column @@ websearch_to_tsquery('english', $value)` WHERE clause and
7
+ * automatically orders results by `ts_rank` (descending) for relevance.
8
+ *
9
+ * Additionally provides:
10
+ * - `matches` filter operator for postgraphile-plugin-connection-filter
11
+ * - `fullTextRank` computed fields on output types (null when no search active)
12
+ * - `FULL_TEXT_RANK_ASC/DESC` orderBy enum values
13
+ *
14
+ * Uses the graphile-build hooks API to extend condition input types with
15
+ * search fields for each tsvector column found on a table's codec.
16
+ *
17
+ * ARCHITECTURE NOTE:
18
+ * Condition field apply functions run during a deferred phase (SQL generation)
19
+ * on a queryBuilder proxy — NOT on the real PgSelectStep. The rank field plan
20
+ * runs earlier, during Grafast's planning phase, on the real PgSelectStep.
21
+ *
22
+ * To bridge these two phases we use a module-level WeakMap keyed by the SQL
23
+ * alias object (shared between proxy and PgSelectStep via reference identity).
24
+ *
25
+ * The rank field plan creates a `lambda` step that reads the row tuple at a
26
+ * dynamically-determined index. The condition apply adds `ts_rank(...)` to
27
+ * the SQL SELECT list via `proxy.selectAndReturnIndex()` and stores the
28
+ * resulting index in the WeakMap slot. At execution time the lambda reads
29
+ * the rank value from that index.
30
+ */
31
+ import 'graphile-build';
32
+ import 'graphile-build-pg';
33
+ import type { GraphileConfig } from 'graphile-config';
34
+ import type { PgSearchPluginOptions } from './types';
35
+ /**
36
+ * Creates the search plugin with the given options.
37
+ */
38
+ export declare function createPgSearchPlugin(options?: PgSearchPluginOptions): GraphileConfig.Plugin;
39
+ /**
40
+ * Creates a PgSearchPlugin with the given options.
41
+ * This is the main entry point for using the plugin.
42
+ */
43
+ export declare const PgSearchPlugin: typeof createPgSearchPlugin;
44
+ export default PgSearchPlugin;