graphile-search 1.1.3 → 1.2.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.
@@ -18,5 +18,11 @@ export declare function createMatchesOperatorFactory(fullTextScalarName: string,
18
18
  * Creates the `similarTo` and `wordSimilarTo` filter operator factories
19
19
  * for pg_trgm fuzzy text matching. Declared here so they're registered
20
20
  * via the declarative `connectionFilterOperatorFactories` API.
21
+ *
22
+ * These operators target 'StringTrgm' (resolved to 'StringTrgmFilter'),
23
+ * NOT the global 'String' type. The unified search plugin registers
24
+ * 'StringTrgmFilter' and selectively assigns it to string columns on
25
+ * tables that qualify for trgm (via intentional search or @trgmSearch tag).
26
+ * This prevents trgm operators from appearing on every string field.
21
27
  */
22
28
  export declare function createTrgmOperatorFactories(): ConnectionFilterOperatorFactory;
@@ -38,13 +38,19 @@ function createMatchesOperatorFactory(fullTextScalarName, tsConfig) {
38
38
  * Creates the `similarTo` and `wordSimilarTo` filter operator factories
39
39
  * for pg_trgm fuzzy text matching. Declared here so they're registered
40
40
  * via the declarative `connectionFilterOperatorFactories` API.
41
+ *
42
+ * These operators target 'StringTrgm' (resolved to 'StringTrgmFilter'),
43
+ * NOT the global 'String' type. The unified search plugin registers
44
+ * 'StringTrgmFilter' and selectively assigns it to string columns on
45
+ * tables that qualify for trgm (via intentional search or @trgmSearch tag).
46
+ * This prevents trgm operators from appearing on every string field.
41
47
  */
42
48
  function createTrgmOperatorFactories() {
43
49
  return (build) => {
44
50
  const { sql } = build;
45
51
  return [
46
52
  {
47
- typeNames: 'String',
53
+ typeNames: 'StringTrgm',
48
54
  operatorName: 'similarTo',
49
55
  spec: {
50
56
  description: 'Fuzzy matches using pg_trgm trigram similarity. Tolerates typos and misspellings.',
@@ -62,7 +68,7 @@ function createTrgmOperatorFactories() {
62
68
  },
63
69
  },
64
70
  {
65
- typeNames: 'String',
71
+ typeNames: 'StringTrgm',
66
72
  operatorName: 'wordSimilarTo',
67
73
  spec: {
68
74
  description: 'Fuzzy matches using pg_trgm word_similarity. Finds the best matching substring within the column value.',
@@ -18,5 +18,11 @@ export declare function createMatchesOperatorFactory(fullTextScalarName: string,
18
18
  * Creates the `similarTo` and `wordSimilarTo` filter operator factories
19
19
  * for pg_trgm fuzzy text matching. Declared here so they're registered
20
20
  * via the declarative `connectionFilterOperatorFactories` API.
21
+ *
22
+ * These operators target 'StringTrgm' (resolved to 'StringTrgmFilter'),
23
+ * NOT the global 'String' type. The unified search plugin registers
24
+ * 'StringTrgmFilter' and selectively assigns it to string columns on
25
+ * tables that qualify for trgm (via intentional search or @trgmSearch tag).
26
+ * This prevents trgm operators from appearing on every string field.
21
27
  */
22
28
  export declare function createTrgmOperatorFactories(): ConnectionFilterOperatorFactory;
@@ -34,13 +34,19 @@ export function createMatchesOperatorFactory(fullTextScalarName, tsConfig) {
34
34
  * Creates the `similarTo` and `wordSimilarTo` filter operator factories
35
35
  * for pg_trgm fuzzy text matching. Declared here so they're registered
36
36
  * via the declarative `connectionFilterOperatorFactories` API.
37
+ *
38
+ * These operators target 'StringTrgm' (resolved to 'StringTrgmFilter'),
39
+ * NOT the global 'String' type. The unified search plugin registers
40
+ * 'StringTrgmFilter' and selectively assigns it to string columns on
41
+ * tables that qualify for trgm (via intentional search or @trgmSearch tag).
42
+ * This prevents trgm operators from appearing on every string field.
37
43
  */
38
44
  export function createTrgmOperatorFactories() {
39
45
  return (build) => {
40
46
  const { sql } = build;
41
47
  return [
42
48
  {
43
- typeNames: 'String',
49
+ typeNames: 'StringTrgm',
44
50
  operatorName: 'similarTo',
45
51
  spec: {
46
52
  description: 'Fuzzy matches using pg_trgm trigram similarity. Tolerates typos and misspellings.',
@@ -58,7 +64,7 @@ export function createTrgmOperatorFactories() {
58
64
  },
59
65
  },
60
66
  {
61
- typeNames: 'String',
67
+ typeNames: 'StringTrgm',
62
68
  operatorName: 'wordSimilarTo',
63
69
  spec: {
64
70
  description: 'Fuzzy matches using pg_trgm word_similarity. Finds the best matching substring within the column value.',
package/esm/plugin.js CHANGED
@@ -61,10 +61,15 @@ export function createUnifiedSearchPlugin(options) {
61
61
  }
62
62
  }
63
63
  }
64
- // Phase 2: Only run supplementary adapters if at least one primary
65
- // adapter with isIntentionalSearch found columns on this codec.
64
+ // Phase 2: Run supplementary adapters if intentional search exists
65
+ // OR if the table/column has a @trgmSearch smart tag.
66
66
  // pgvector (isIntentionalSearch: false) alone won't trigger trgm.
67
- if (hasIntentionalSearch) {
67
+ const hasTrgmSearchTag =
68
+ // Table-level tag
69
+ codec.extensions?.tags?.trgmSearch ||
70
+ // Column-level tag
71
+ (codec.attributes && Object.values(codec.attributes).some((attr) => attr?.extensions?.tags?.trgmSearch));
72
+ if (hasIntentionalSearch || hasTrgmSearchTag) {
68
73
  for (const adapter of supplementaryAdapters) {
69
74
  const columns = adapter.detectColumns(codec, build);
70
75
  if (columns.length > 0) {
@@ -85,6 +90,8 @@ export function createUnifiedSearchPlugin(options) {
85
90
  'PgConnectionArgFilterAttributesPlugin',
86
91
  'PgConnectionArgFilterOperatorsPlugin',
87
92
  'AddConnectionFilterOperatorPlugin',
93
+ 'ConnectionFilterTypesPlugin',
94
+ 'ConnectionFilterCustomOperatorsPlugin',
88
95
  // Allow individual codec plugins to load first (e.g. Bm25CodecPlugin)
89
96
  'Bm25CodecPlugin',
90
97
  'VectorCodecPlugin',
@@ -161,6 +168,26 @@ export function createUnifiedSearchPlugin(options) {
161
168
  for (const adapter of adapters) {
162
169
  adapter.registerTypes(build);
163
170
  }
171
+ // Register StringTrgmFilter — a variant of StringFilter that includes
172
+ // trgm operators (similarTo, wordSimilarTo). Only string columns on
173
+ // tables that qualify for trgm will use this type instead of StringFilter.
174
+ const hasTrgmAdapter = adapters.some((a) => a.name === 'trgm');
175
+ if (hasTrgmAdapter) {
176
+ const DPTYPES = build.dataplanPg?.TYPES;
177
+ const textCodec = DPTYPES?.text ?? TYPES.text;
178
+ build.registerInputObjectType('StringTrgmFilter', {
179
+ pgConnectionFilterOperators: {
180
+ isList: false,
181
+ pgCodecs: [textCodec],
182
+ inputTypeName: 'String',
183
+ rangeElementInputTypeName: null,
184
+ domainBaseTypeName: null,
185
+ },
186
+ }, () => ({
187
+ description: 'A filter to be used against String fields with pg_trgm support. ' +
188
+ 'All fields are combined with a logical \u2018and.\u2019',
189
+ }), 'UnifiedSearchPlugin (StringTrgmFilter)');
190
+ }
164
191
  return _;
165
192
  },
166
193
  /**
@@ -446,6 +473,26 @@ export function createUnifiedSearchPlugin(options) {
446
473
  return fields;
447
474
  }
448
475
  let newFields = fields;
476
+ // ── StringFilter → StringTrgmFilter type swapping ──
477
+ // For tables that qualify for trgm, swap the type of string attribute
478
+ // filter fields so they get similarTo/wordSimilarTo operators.
479
+ const hasTrgm = adapterColumns.some((ac) => ac.adapter.name === 'trgm');
480
+ if (hasTrgm) {
481
+ const StringTrgmFilterType = build.getTypeByName('StringTrgmFilter');
482
+ const StringFilterType = build.getTypeByName('StringFilter');
483
+ if (StringTrgmFilterType && StringFilterType) {
484
+ const swapped = {};
485
+ for (const [key, field] of Object.entries(newFields)) {
486
+ if (field && typeof field === 'object' && field.type === StringFilterType) {
487
+ swapped[key] = Object.assign({}, field, { type: StringTrgmFilterType });
488
+ }
489
+ else {
490
+ swapped[key] = field;
491
+ }
492
+ }
493
+ newFields = swapped;
494
+ }
495
+ }
449
496
  for (const { adapter, columns } of adapterColumns) {
450
497
  for (const column of columns) {
451
498
  const fieldName = inflection.camelCase(`${adapter.filterPrefix}_${column.attributeName}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "graphile-search",
3
- "version": "1.1.3",
3
+ "version": "1.2.0",
4
4
  "description": "Unified PostGraphile v5 search plugin — abstracts tsvector, BM25, pg_trgm, and pgvector behind a single adapter-based architecture with composite searchScore",
5
5
  "author": "Constructive <developers@constructive.io>",
6
6
  "homepage": "https://github.com/constructive-io/constructive",
@@ -31,11 +31,11 @@
31
31
  "devDependencies": {
32
32
  "@types/node": "^22.19.11",
33
33
  "@types/pg": "^8.18.0",
34
- "graphile-connection-filter": "^1.1.3",
35
- "graphile-test": "^4.5.5",
34
+ "graphile-connection-filter": "^1.1.4",
35
+ "graphile-test": "^4.5.6",
36
36
  "makage": "^0.1.10",
37
37
  "pg": "^8.20.0",
38
- "pgsql-test": "^4.5.5"
38
+ "pgsql-test": "^4.5.6"
39
39
  },
40
40
  "peerDependencies": {
41
41
  "@dataplan/pg": "1.0.0-rc.8",
@@ -62,5 +62,5 @@
62
62
  "hybrid-search",
63
63
  "searchScore"
64
64
  ],
65
- "gitHead": "9c322f47ca08b5b853fcb395fe2bfc224f8c4c27"
65
+ "gitHead": "9f3837555601b15e693ae0b750124fb0a8df3176"
66
66
  }
package/plugin.js CHANGED
@@ -64,10 +64,15 @@ function createUnifiedSearchPlugin(options) {
64
64
  }
65
65
  }
66
66
  }
67
- // Phase 2: Only run supplementary adapters if at least one primary
68
- // adapter with isIntentionalSearch found columns on this codec.
67
+ // Phase 2: Run supplementary adapters if intentional search exists
68
+ // OR if the table/column has a @trgmSearch smart tag.
69
69
  // pgvector (isIntentionalSearch: false) alone won't trigger trgm.
70
- if (hasIntentionalSearch) {
70
+ const hasTrgmSearchTag =
71
+ // Table-level tag
72
+ codec.extensions?.tags?.trgmSearch ||
73
+ // Column-level tag
74
+ (codec.attributes && Object.values(codec.attributes).some((attr) => attr?.extensions?.tags?.trgmSearch));
75
+ if (hasIntentionalSearch || hasTrgmSearchTag) {
71
76
  for (const adapter of supplementaryAdapters) {
72
77
  const columns = adapter.detectColumns(codec, build);
73
78
  if (columns.length > 0) {
@@ -88,6 +93,8 @@ function createUnifiedSearchPlugin(options) {
88
93
  'PgConnectionArgFilterAttributesPlugin',
89
94
  'PgConnectionArgFilterOperatorsPlugin',
90
95
  'AddConnectionFilterOperatorPlugin',
96
+ 'ConnectionFilterTypesPlugin',
97
+ 'ConnectionFilterCustomOperatorsPlugin',
91
98
  // Allow individual codec plugins to load first (e.g. Bm25CodecPlugin)
92
99
  'Bm25CodecPlugin',
93
100
  'VectorCodecPlugin',
@@ -164,6 +171,26 @@ function createUnifiedSearchPlugin(options) {
164
171
  for (const adapter of adapters) {
165
172
  adapter.registerTypes(build);
166
173
  }
174
+ // Register StringTrgmFilter — a variant of StringFilter that includes
175
+ // trgm operators (similarTo, wordSimilarTo). Only string columns on
176
+ // tables that qualify for trgm will use this type instead of StringFilter.
177
+ const hasTrgmAdapter = adapters.some((a) => a.name === 'trgm');
178
+ if (hasTrgmAdapter) {
179
+ const DPTYPES = build.dataplanPg?.TYPES;
180
+ const textCodec = DPTYPES?.text ?? pg_1.TYPES.text;
181
+ build.registerInputObjectType('StringTrgmFilter', {
182
+ pgConnectionFilterOperators: {
183
+ isList: false,
184
+ pgCodecs: [textCodec],
185
+ inputTypeName: 'String',
186
+ rangeElementInputTypeName: null,
187
+ domainBaseTypeName: null,
188
+ },
189
+ }, () => ({
190
+ description: 'A filter to be used against String fields with pg_trgm support. ' +
191
+ 'All fields are combined with a logical \u2018and.\u2019',
192
+ }), 'UnifiedSearchPlugin (StringTrgmFilter)');
193
+ }
167
194
  return _;
168
195
  },
169
196
  /**
@@ -449,6 +476,26 @@ function createUnifiedSearchPlugin(options) {
449
476
  return fields;
450
477
  }
451
478
  let newFields = fields;
479
+ // ── StringFilter → StringTrgmFilter type swapping ──
480
+ // For tables that qualify for trgm, swap the type of string attribute
481
+ // filter fields so they get similarTo/wordSimilarTo operators.
482
+ const hasTrgm = adapterColumns.some((ac) => ac.adapter.name === 'trgm');
483
+ if (hasTrgm) {
484
+ const StringTrgmFilterType = build.getTypeByName('StringTrgmFilter');
485
+ const StringFilterType = build.getTypeByName('StringFilter');
486
+ if (StringTrgmFilterType && StringFilterType) {
487
+ const swapped = {};
488
+ for (const [key, field] of Object.entries(newFields)) {
489
+ if (field && typeof field === 'object' && field.type === StringFilterType) {
490
+ swapped[key] = Object.assign({}, field, { type: StringTrgmFilterType });
491
+ }
492
+ else {
493
+ swapped[key] = field;
494
+ }
495
+ }
496
+ newFields = swapped;
497
+ }
498
+ }
452
499
  for (const { adapter, columns } of adapterColumns) {
453
500
  for (const column of columns) {
454
501
  const fieldName = inflection.camelCase(`${adapter.filterPrefix}_${column.attributeName}`);