graphile-search-plugin 1.1.1 โ†’ 3.1.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.
package/README.md CHANGED
@@ -16,44 +16,64 @@
16
16
  </a>
17
17
  </p>
18
18
 
19
- **`graphile-search-plugin`** enables `ts_rank` ordering and auto-generated full-text search helpers for all `tsvector` fields in PostGraphile schemas.
19
+ **`graphile-search-plugin`** enables auto-generated full-text search condition fields for all `tsvector` columns in PostGraphile v5 schemas.
20
20
 
21
- ## ๐Ÿš€ Installation
21
+ ## Installation
22
22
 
23
23
  ```sh
24
24
  npm install graphile-search-plugin
25
25
  ```
26
26
 
27
- ## โœจ Features
27
+ ## Features
28
28
 
29
- - Adds full-text search helpers for `tsvector` columns
30
- - Enables ordering via `ts_rank` on generated search fields
31
- - Works with PostGraphile append plugin pipeline
29
+ - Adds full-text search condition fields for `tsvector` columns
30
+ - Uses `websearch_to_tsquery` for natural search syntax
31
+ - Automatic `ORDER BY ts_rank(column, tsquery) DESC` relevance ordering (matching V4 behavior)
32
+ - Cursor-based pagination remains stable โ€” PostGraphile re-appends unique key columns after the relevance sort
33
+ - Works with PostGraphile v5 preset/plugin pipeline
32
34
 
33
- ## ๐Ÿ“ฆ Usage
35
+ ## Usage
34
36
 
35
- 1. Append the new plugins!
36
- 2. Query `search<YourTsvectorColumn>` in the `conditions` field
37
- 3. Enjoy!
37
+ ### With Preset (Recommended)
38
38
 
39
- ```js
39
+ ```typescript
40
+ import { PgSearchPreset } from 'graphile-search-plugin';
40
41
 
41
- import PgSearchPlugin from 'graphile-search-plugin';
42
-
43
- app.use(
44
- postgraphile(connectionStr, schemas, {
45
- appendPlugins: [
46
- PgSearchPlugin
47
- ]
48
- })
49
- );
42
+ const preset = {
43
+ extends: [
44
+ // ... your other presets
45
+ PgSearchPreset({ pgSearchPrefix: 'fullText' }),
46
+ ],
47
+ };
50
48
  ```
51
49
 
52
- ## ๐Ÿงช Examples
50
+ ### With Plugin Directly
51
+
52
+ ```typescript
53
+ import { PgSearchPlugin } from 'graphile-search-plugin';
53
54
 
54
- Look in the tests ;)
55
+ const preset = {
56
+ plugins: [
57
+ PgSearchPlugin({ pgSearchPrefix: 'fullText' }),
58
+ ],
59
+ };
60
+ ```
61
+
62
+ ### GraphQL Query
63
+
64
+ ```graphql
65
+ query SearchGoals($search: String!) {
66
+ goals(condition: { fullTextTsv: $search }) {
67
+ nodes {
68
+ id
69
+ title
70
+ description
71
+ }
72
+ }
73
+ }
74
+ ```
55
75
 
56
- ## ๐Ÿงช Testing
76
+ ## Testing
57
77
 
58
78
  ```sh
59
79
  # requires a local Postgres available (defaults to postgres/password@localhost:5432)
package/esm/index.d.ts ADDED
@@ -0,0 +1,26 @@
1
+ /**
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
+ * ```
22
+ */
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/esm/index.js CHANGED
@@ -1,57 +1,25 @@
1
- // plugins/PgSearchPlugin.ts
2
1
  /**
3
- * 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
+ * ```
4
22
  */
5
- const PgSearchPlugin = (builder, options = {}) => {
6
- const { pgSearchPrefix = 'tsv' } = options;
7
- builder.hook('GraphQLInputObjectType:fields', (fields, build, context) => {
8
- const { inflection } = build;
9
- const { scope: { isPgCondition, pgIntrospection: table }, fieldWithHooks } = context;
10
- if (!isPgCondition || !table || table.kind !== 'class')
11
- return fields;
12
- const tsvs = table.attributes.filter((attr) => attr.type.name === 'tsvector');
13
- if (!tsvs.length)
14
- return fields;
15
- return build.extend(fields, tsvs.reduce((memo, attr) => {
16
- const fieldName = inflection.camelCase(`${pgSearchPrefix}_${attr.name}`);
17
- memo[fieldName] = fieldWithHooks(fieldName, { type: build.graphql.GraphQLString }, {});
18
- return memo;
19
- }, {}));
20
- });
21
- builder.hook('GraphQLObjectType:fields:field:args', (args, build, context) => {
22
- const { pgSql: sql, inflection } = build;
23
- const { scope: { isPgFieldConnection, isPgFieldSimpleCollection, pgFieldIntrospection: procOrTable, pgFieldIntrospectionTable: tableIfProc, }, addArgDataGenerator, } = context;
24
- const table = tableIfProc || procOrTable;
25
- if ((!isPgFieldConnection && !isPgFieldSimpleCollection) ||
26
- !table ||
27
- table.kind !== 'class') {
28
- return args;
29
- }
30
- const tsvs = table.attributes.filter((attr) => attr.type.name === 'tsvector');
31
- if (!tsvs.length)
32
- return args;
33
- tsvs.forEach((tsv) => {
34
- const conditionFieldName = inflection.camelCase(`${pgSearchPrefix}_${tsv.name}`);
35
- addArgDataGenerator(function addSearchCondition({ condition }) {
36
- if (!condition || !(conditionFieldName in condition))
37
- return {};
38
- const value = condition[conditionFieldName];
39
- if (value == null)
40
- return {};
41
- return {
42
- pgQuery: (queryBuilder) => {
43
- const tsquery = sql.fragment `websearch_to_tsquery('english', ${sql.value(value)})`;
44
- const tableAlias = queryBuilder.getTableAlias();
45
- // WHERE condition
46
- queryBuilder.where(sql.fragment `${tableAlias}.${sql.identifier(tsv.name)} @@ ${tsquery}`);
47
- // Automatically add ordering by relevance (descending)
48
- queryBuilder.orderBy(sql.fragment `ts_rank(${tableAlias}.${sql.identifier(tsv.name)}, ${tsquery})`, false);
49
- },
50
- };
51
- });
52
- });
53
- return args;
54
- }, [], ['PgConnectionArgOrderBy']);
55
- };
56
- export { PgSearchPlugin };
57
- export default PgSearchPlugin;
23
+ export { PgSearchPlugin, createPgSearchPlugin } from './plugin';
24
+ export { PgSearchPreset } from './preset';
25
+ export { TsvectorCodecPlugin, TsvectorCodecPreset, createTsvectorCodecPlugin, } from './tsvector-codec';
@@ -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;
package/esm/plugin.js ADDED
@@ -0,0 +1,263 @@
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 { TYPES } from '@dataplan/pg';
34
+ const ftsRankSlots = new WeakMap();
35
+ /**
36
+ * FinalizationRegistry for defensive cleanup of ftsRankSlots entries.
37
+ * WeakMap entries are already eligible for GC when keys are unreachable,
38
+ * but this provides explicit cleanup and a hook for debugging leaks.
39
+ */
40
+ const ftsRankCleanup = new FinalizationRegistry((heldValue) => {
41
+ ftsRankSlots.delete(heldValue);
42
+ });
43
+ function isTsvectorCodec(codec) {
44
+ return (codec?.extensions?.pg?.schemaName === 'pg_catalog' &&
45
+ codec?.extensions?.pg?.name === 'tsvector');
46
+ }
47
+ /**
48
+ * Navigates from a PgSelectSingleStep up to the PgSelectStep.
49
+ * Uses duck-typing to avoid dependency on exact class names across rc versions.
50
+ */
51
+ function getPgSelectStep($someStep) {
52
+ let $step = $someStep;
53
+ if ($step && typeof $step.getClassStep === 'function') {
54
+ $step = $step.getClassStep();
55
+ }
56
+ if ($step && typeof $step.orderBy === 'function' && $step.id !== undefined) {
57
+ return $step;
58
+ }
59
+ return null;
60
+ }
61
+ /**
62
+ * Creates the search plugin with the given options.
63
+ */
64
+ export function createPgSearchPlugin(options = {}) {
65
+ const { pgSearchPrefix = 'tsv', fullTextScalarName = 'FullText', tsConfig = 'english' } = options;
66
+ return {
67
+ name: 'PgSearchPlugin',
68
+ version: '2.0.0',
69
+ description: 'Generates search conditions for tsvector columns in PostGraphile v5',
70
+ after: ['PgAttributesPlugin', 'PgConnectionArgFilterPlugin', 'PgConnectionArgFilterOperatorsPlugin', 'AddConnectionFilterOperatorPlugin'],
71
+ schema: {
72
+ hooks: {
73
+ init(_, build) {
74
+ const { sql, graphql: { GraphQLString }, } = build;
75
+ // Register the `matches` filter operator for the FullText scalar.
76
+ // Requires postgraphile-plugin-connection-filter; skip if not loaded.
77
+ const addConnectionFilterOperator = build
78
+ .addConnectionFilterOperator;
79
+ if (typeof addConnectionFilterOperator === 'function') {
80
+ const TYPES = build.dataplanPg?.TYPES;
81
+ addConnectionFilterOperator(fullTextScalarName, 'matches', {
82
+ description: 'Performs a full text search on the field.',
83
+ resolveType: () => GraphQLString,
84
+ resolveInputCodec: TYPES ? () => TYPES.text : undefined,
85
+ resolve(sqlIdentifier, sqlValue, _input, _$where, _details) {
86
+ return sql `${sqlIdentifier} @@ websearch_to_tsquery(${sql.literal(tsConfig)}, ${sqlValue})`;
87
+ },
88
+ });
89
+ }
90
+ return _;
91
+ },
92
+ GraphQLObjectType_fields(fields, build, context) {
93
+ const { sql, inflection, graphql: { GraphQLFloat }, grafast: { constant, lambda }, } = build;
94
+ const { scope: { isPgClassType, pgCodec }, fieldWithHooks, } = context;
95
+ if (!isPgClassType || !pgCodec?.attributes) {
96
+ return fields;
97
+ }
98
+ let newFields = fields;
99
+ for (const [attributeName, attribute] of Object.entries(pgCodec.attributes)) {
100
+ if (!isTsvectorCodec(attribute.codec))
101
+ continue;
102
+ const baseFieldName = inflection.attribute({ codec: pgCodec, attributeName });
103
+ const fieldName = inflection.camelCase(`${baseFieldName}-rank`);
104
+ newFields = build.extend(newFields, {
105
+ [fieldName]: fieldWithHooks({ fieldName }, () => ({
106
+ description: `Full-text search ranking when filtered by \`${baseFieldName}\`. Returns null when no search condition is active.`,
107
+ type: GraphQLFloat,
108
+ plan($step) {
109
+ const $select = getPgSelectStep($step);
110
+ if (!$select)
111
+ return constant(null);
112
+ if (typeof $select.setInliningForbidden === 'function') {
113
+ $select.setInliningForbidden();
114
+ }
115
+ // Initialise the WeakMap slot for this query, keyed by the
116
+ // SQL alias (same object ref on PgSelectStep and the proxy).
117
+ const alias = $select.alias;
118
+ if (!ftsRankSlots.has(alias)) {
119
+ ftsRankSlots.set(alias, {
120
+ indices: Object.create(null),
121
+ });
122
+ ftsRankCleanup.register($select, alias);
123
+ }
124
+ // Return a lambda that reads the rank value from the result
125
+ // row at a dynamically-determined index. The index is set
126
+ // by the condition apply (deferred phase) via the proxy's
127
+ // selectAndReturnIndex, and stored in the WeakMap slot.
128
+ const capturedField = baseFieldName;
129
+ const capturedAlias = alias;
130
+ return lambda($step, (row) => {
131
+ if (row == null)
132
+ return null;
133
+ const slot = ftsRankSlots.get(capturedAlias);
134
+ if (!slot || slot.indices[capturedField] === undefined)
135
+ return null;
136
+ const rawValue = row[slot.indices[capturedField]];
137
+ return rawValue == null ? null : parseFloat(rawValue);
138
+ }, true);
139
+ },
140
+ })),
141
+ }, `PgSearchPlugin adding rank field '${fieldName}' for '${attributeName}' on '${pgCodec.name}'`);
142
+ }
143
+ return newFields;
144
+ },
145
+ GraphQLEnumType_values(values, build, context) {
146
+ const { sql, inflection, } = build;
147
+ const { scope: { isPgRowSortEnum, pgCodec }, } = context;
148
+ if (!isPgRowSortEnum || !pgCodec?.attributes) {
149
+ return values;
150
+ }
151
+ let newValues = values;
152
+ for (const [attributeName, attribute] of Object.entries(pgCodec.attributes)) {
153
+ if (!isTsvectorCodec(attribute.codec))
154
+ continue;
155
+ const fieldName = inflection.attribute({ codec: pgCodec, attributeName });
156
+ const metaKey = `fts_order_${fieldName}`;
157
+ const makePlan = (direction) => (step) => {
158
+ // The enum apply runs during the PLANNING phase on PgSelectStep.
159
+ // Store the requested direction in PgSelectStep._meta so that
160
+ // the condition apply (deferred phase) can read it via the
161
+ // proxy's getMetaRaw and add the actual ORDER BY clause.
162
+ if (typeof step.setMeta === 'function') {
163
+ step.setMeta(metaKey, direction);
164
+ }
165
+ };
166
+ const ascName = inflection.constantCase(`${attributeName}_rank_asc`);
167
+ const descName = inflection.constantCase(`${attributeName}_rank_desc`);
168
+ newValues = build.extend(newValues, {
169
+ [ascName]: {
170
+ extensions: {
171
+ grafast: {
172
+ apply: makePlan('ASC'),
173
+ },
174
+ },
175
+ },
176
+ [descName]: {
177
+ extensions: {
178
+ grafast: {
179
+ apply: makePlan('DESC'),
180
+ },
181
+ },
182
+ },
183
+ }, `PgSearchPlugin adding rank orderBy for '${attributeName}' on '${pgCodec.name}'`);
184
+ }
185
+ return newValues;
186
+ },
187
+ GraphQLInputObjectType_fields(fields, build, context) {
188
+ const { inflection, sql, graphql: { GraphQLString }, } = build;
189
+ const { scope: { isPgCondition, pgCodec }, fieldWithHooks, } = context;
190
+ if (!isPgCondition ||
191
+ !pgCodec ||
192
+ !pgCodec.attributes ||
193
+ pgCodec.isAnonymous) {
194
+ return fields;
195
+ }
196
+ const tsvectorAttributes = Object.entries(pgCodec.attributes).filter(([_name, attr]) => isTsvectorCodec(attr.codec));
197
+ if (tsvectorAttributes.length === 0) {
198
+ return fields;
199
+ }
200
+ let newFields = fields;
201
+ for (const [attributeName] of tsvectorAttributes) {
202
+ const fieldName = inflection.camelCase(`${pgSearchPrefix}_${attributeName}`);
203
+ const baseFieldName = inflection.attribute({ codec: pgCodec, attributeName });
204
+ newFields = build.extend(newFields, {
205
+ [fieldName]: fieldWithHooks({
206
+ fieldName,
207
+ isPgConnectionConditionInputField: true,
208
+ }, {
209
+ description: build.wrapDescription(`Full-text search on the \`${attributeName}\` tsvector column using \`websearch_to_tsquery\`.`, 'field'),
210
+ type: GraphQLString,
211
+ apply: function plan($condition, val) {
212
+ if (val == null)
213
+ return;
214
+ const tsquery = sql `websearch_to_tsquery(${sql.literal(tsConfig)}, ${sql.value(val)})`;
215
+ const columnExpr = sql `${$condition.alias}.${sql.identifier(attributeName)}`;
216
+ // WHERE: column @@ tsquery
217
+ $condition.where(sql `${columnExpr} @@ ${tsquery}`);
218
+ // Add ts_rank to the SELECT list via the proxy's
219
+ // selectAndReturnIndex. This runs during the deferred
220
+ // SQL-generation phase, so the expression goes into
221
+ // info.selects (the live array used for SQL generation).
222
+ const $parent = $condition.dangerouslyGetParent();
223
+ if (typeof $parent.selectAndReturnIndex === 'function') {
224
+ const rankSql = sql `ts_rank(${columnExpr}, ${tsquery})`;
225
+ const wrappedRankSql = sql `${sql.parens(rankSql)}::text`;
226
+ const rankIndex = $parent.selectAndReturnIndex(wrappedRankSql);
227
+ // Store the index in the alias-keyed WeakMap slot so
228
+ // the rank field's lambda can read it at execute time.
229
+ const slot = ftsRankSlots.get($condition.alias);
230
+ if (slot) {
231
+ slot.indices[baseFieldName] = rankIndex;
232
+ }
233
+ }
234
+ // ORDER BY ts_rank: check if the user provided an
235
+ // explicit rank orderBy enum (stored in meta during
236
+ // planning). If so, use their direction. Otherwise add
237
+ // automatic DESC ordering for relevance.
238
+ const metaKey = `fts_order_${baseFieldName}`;
239
+ const explicitDir = typeof $parent.getMetaRaw === 'function'
240
+ ? $parent.getMetaRaw(metaKey)
241
+ : undefined;
242
+ const orderDirection = explicitDir ?? 'DESC';
243
+ $parent.orderBy({
244
+ fragment: sql `ts_rank(${columnExpr}, ${tsquery})`,
245
+ codec: TYPES.float4,
246
+ direction: orderDirection,
247
+ });
248
+ },
249
+ }),
250
+ }, `PgSearchPlugin adding condition field '${fieldName}' for tsvector column '${attributeName}' on '${pgCodec.name}'`);
251
+ }
252
+ return newFields;
253
+ },
254
+ },
255
+ },
256
+ };
257
+ }
258
+ /**
259
+ * Creates a PgSearchPlugin with the given options.
260
+ * This is the main entry point for using the plugin.
261
+ */
262
+ export const PgSearchPlugin = createPgSearchPlugin;
263
+ export default PgSearchPlugin;
@@ -0,0 +1,25 @@
1
+ /**
2
+ * PostGraphile v5 Search Preset
3
+ *
4
+ * Provides a convenient preset for including search support in PostGraphile.
5
+ */
6
+ import type { GraphileConfig } from 'graphile-config';
7
+ import type { PgSearchPluginOptions } from './types';
8
+ /**
9
+ * Creates a preset that includes the search plugin with the given options.
10
+ *
11
+ * @example
12
+ * ```typescript
13
+ * import { PgSearchPreset } from 'graphile-search-plugin';
14
+ *
15
+ * const preset = {
16
+ * extends: [
17
+ * PgSearchPreset({
18
+ * pgSearchPrefix: 'fullText',
19
+ * }),
20
+ * ],
21
+ * };
22
+ * ```
23
+ */
24
+ export declare function PgSearchPreset(options?: PgSearchPluginOptions): GraphileConfig.Preset;
25
+ export default PgSearchPreset;
package/esm/preset.js ADDED
@@ -0,0 +1,29 @@
1
+ /**
2
+ * PostGraphile v5 Search Preset
3
+ *
4
+ * Provides a convenient preset for including search support in PostGraphile.
5
+ */
6
+ import { createPgSearchPlugin } from './plugin';
7
+ import { createTsvectorCodecPlugin } from './tsvector-codec';
8
+ /**
9
+ * Creates a preset that includes the search plugin with the given options.
10
+ *
11
+ * @example
12
+ * ```typescript
13
+ * import { PgSearchPreset } from 'graphile-search-plugin';
14
+ *
15
+ * const preset = {
16
+ * extends: [
17
+ * PgSearchPreset({
18
+ * pgSearchPrefix: 'fullText',
19
+ * }),
20
+ * ],
21
+ * };
22
+ * ```
23
+ */
24
+ export function PgSearchPreset(options = {}) {
25
+ return {
26
+ plugins: [createTsvectorCodecPlugin(options), createPgSearchPlugin(options)],
27
+ };
28
+ }
29
+ export default PgSearchPreset;
@@ -0,0 +1,29 @@
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 type { GraphileConfig } from 'graphile-config';
16
+ import type { PgSearchPluginOptions } from './types';
17
+ /**
18
+ * Creates a TsvectorCodecPlugin with the given options.
19
+ *
20
+ * @param options - Plugin configuration
21
+ * @returns GraphileConfig.Plugin
22
+ */
23
+ export declare function createTsvectorCodecPlugin(options?: PgSearchPluginOptions): GraphileConfig.Plugin;
24
+ /**
25
+ * Default static instance using default options.
26
+ * Maps tsvector to the "FullText" scalar.
27
+ */
28
+ export declare const TsvectorCodecPlugin: GraphileConfig.Plugin;
29
+ export declare const TsvectorCodecPreset: GraphileConfig.Preset;