From 3c4a91a85ab2290fd2aa54c3359a701c3db8f23e Mon Sep 17 00:00:00 2001 From: Dan Lynch Date: Thu, 12 Mar 2026 01:04:27 +0000 Subject: [PATCH 1/3] fix(codegen): include plugin-injected fields in condition and orderBy types Previously, buildTableConditionProperties and buildOrderByValues only iterated table.fields (database columns), missing any fields injected by Graphile plugins (e.g., VectorSearchPlugin's embeddingNearby condition field and EMBEDDING_DISTANCE_ASC/DESC orderBy values). Changes: - Add Vector scalar type mapping (number[]) to SCALAR_TS_MAP - Modify buildTableConditionProperties to merge plugin-added fields from the TypeRegistry's condition type - Modify buildOrderByValues to merge plugin-added enum values from the TypeRegistry's orderBy type - Add collectConditionExtraInputTypes to discover and generate referenced input types (e.g., VectorNearbyInput, VectorMetric) - Pass typeRegistry through to condition/orderBy generators - Add comprehensive tests for plugin-injected condition and orderBy types Fixes: constructive-io/constructive-planning#663 --- .../codegen/input-types-generator.test.ts | 166 ++++++++++++++++++ .../core/codegen/orm/input-types-generator.ts | 146 +++++++++++++-- graphql/codegen/src/core/codegen/scalars.ts | 3 + 3 files changed, 304 insertions(+), 11 deletions(-) diff --git a/graphql/codegen/src/__tests__/codegen/input-types-generator.test.ts b/graphql/codegen/src/__tests__/codegen/input-types-generator.test.ts index 8e0c7d8de..25c8f136e 100644 --- a/graphql/codegen/src/__tests__/codegen/input-types-generator.test.ts +++ b/graphql/codegen/src/__tests__/codegen/input-types-generator.test.ts @@ -877,6 +877,172 @@ describe('collectPayloadTypeNames', () => { }); }); +// ============================================================================ +// Tests - Plugin-Injected Condition Fields (e.g., VectorSearchPlugin) +// ============================================================================ + +describe('plugin-injected condition fields', () => { + /** + * Simulates a table with a vector embedding column. + * The VectorSearchPlugin adds extra condition fields (e.g., embeddingNearby) + * that are NOT derived from the table's own columns, but are injected into + * the GraphQL schema's condition type via plugin hooks. + */ + const contactTable = createTable({ + name: 'Contact', + fields: [ + { name: 'id', type: fieldTypes.uuid }, + { name: 'name', type: fieldTypes.string }, + { name: 'email', type: fieldTypes.string }, + { + name: 'embedding', + type: { gqlType: 'Vector', isArray: false } as CleanFieldType, + }, + ], + query: { + all: 'contacts', + one: 'contact', + create: 'createContact', + update: 'updateContact', + delete: 'deleteContact', + }, + }); + + it('includes plugin-injected condition fields from TypeRegistry', () => { + // Registry simulates what PostGraphile + VectorSearchPlugin produce: + // The ContactCondition type has the regular columns PLUS an extra + // "embeddingNearby" field of type VectorNearbyInput injected by the plugin. + const registry = createTypeRegistry({ + ContactCondition: { + kind: 'INPUT_OBJECT', + name: 'ContactCondition', + inputFields: [ + { name: 'id', type: createTypeRef('SCALAR', 'UUID') }, + { name: 'name', type: createTypeRef('SCALAR', 'String') }, + { name: 'email', type: createTypeRef('SCALAR', 'String') }, + { name: 'embedding', type: createTypeRef('SCALAR', 'Vector') }, + { + name: 'embeddingNearby', + type: createTypeRef('INPUT_OBJECT', 'VectorNearbyInput'), + description: 'Find contacts near a vector embedding', + }, + ], + }, + VectorNearbyInput: { + kind: 'INPUT_OBJECT', + name: 'VectorNearbyInput', + inputFields: [ + { + name: 'vector', + type: createNonNull(createTypeRef('SCALAR', 'Vector')), + }, + { + name: 'metric', + type: createTypeRef('ENUM', 'VectorMetric'), + }, + { + name: 'threshold', + type: createTypeRef('SCALAR', 'Float'), + }, + ], + }, + VectorMetric: { + kind: 'ENUM', + name: 'VectorMetric', + enumValues: ['L2', 'INNER_PRODUCT', 'COSINE'], + }, + }); + + const result = generateInputTypesFile(registry, new Set(), [contactTable]); + + // Regular table column fields should still be present + expect(result.content).toContain('export interface ContactCondition {'); + expect(result.content).toContain('id?: string | null;'); + expect(result.content).toContain('name?: string | null;'); + expect(result.content).toContain('email?: string | null;'); + + // Plugin-injected field should also be present + expect(result.content).toContain('embeddingNearby?: VectorNearbyInput'); + + // The referenced VectorNearbyInput type should be generated as a custom input type + expect(result.content).toContain('export interface VectorNearbyInput {'); + }); + + it('does not duplicate fields already derived from table columns', () => { + const registry = createTypeRegistry({ + ContactCondition: { + kind: 'INPUT_OBJECT', + name: 'ContactCondition', + inputFields: [ + { name: 'id', type: createTypeRef('SCALAR', 'UUID') }, + { name: 'name', type: createTypeRef('SCALAR', 'String') }, + { name: 'email', type: createTypeRef('SCALAR', 'String') }, + { name: 'embedding', type: createTypeRef('SCALAR', 'Vector') }, + ], + }, + }); + + const result = generateInputTypesFile(registry, new Set(), [contactTable]); + + // Count occurrences of 'id?' in the ContactCondition interface + const conditionMatch = result.content.match( + /export interface ContactCondition \{([^}]*)\}/s, + ); + expect(conditionMatch).toBeTruthy(); + const conditionBody = conditionMatch![1]; + + // Each field should appear only once + const idOccurrences = (conditionBody.match(/\bid\?/g) || []).length; + expect(idOccurrences).toBe(1); + }); + + it('includes plugin-injected orderBy values from TypeRegistry', () => { + const registry = createTypeRegistry({ + ContactsOrderBy: { + kind: 'ENUM', + name: 'ContactsOrderBy', + enumValues: [ + 'PRIMARY_KEY_ASC', + 'PRIMARY_KEY_DESC', + 'NATURAL', + 'ID_ASC', + 'ID_DESC', + 'NAME_ASC', + 'NAME_DESC', + 'EMAIL_ASC', + 'EMAIL_DESC', + 'EMBEDDING_ASC', + 'EMBEDDING_DESC', + // Plugin-injected values from VectorSearchPlugin + 'EMBEDDING_DISTANCE_ASC', + 'EMBEDDING_DISTANCE_DESC', + ], + }, + }); + + const result = generateInputTypesFile(registry, new Set(), [contactTable]); + + expect(result.content).toContain('export type ContactsOrderBy ='); + // Standard column-derived values + expect(result.content).toContain('"ID_ASC"'); + expect(result.content).toContain('"NAME_DESC"'); + // Plugin-injected values + expect(result.content).toContain('"EMBEDDING_DISTANCE_ASC"'); + expect(result.content).toContain('"EMBEDDING_DISTANCE_DESC"'); + }); + + it('works without typeRegistry (backwards compatible)', () => { + // When no typeRegistry has the condition type, only table columns are used + const result = generateInputTypesFile(new Map(), new Set(), [contactTable]); + + expect(result.content).toContain('export interface ContactCondition {'); + expect(result.content).toContain('id?: string | null;'); + expect(result.content).toContain('name?: string | null;'); + // No plugin-injected fields + expect(result.content).not.toContain('embeddingNearby'); + }); +}); + // ============================================================================ // Tests - Edge Cases // ============================================================================ diff --git a/graphql/codegen/src/core/codegen/orm/input-types-generator.ts b/graphql/codegen/src/core/codegen/orm/input-types-generator.ts index 6d19a7977..8717c7ddb 100644 --- a/graphql/codegen/src/core/codegen/orm/input-types-generator.ts +++ b/graphql/codegen/src/core/codegen/orm/input-types-generator.ts @@ -1116,10 +1116,18 @@ function generateTableFilterTypes(tables: CleanTable[]): t.Statement[] { /** * Build properties for a table condition interface - * Condition types are simpler than Filter types - they use direct value equality + * Condition types are simpler than Filter types - they use direct value equality. + * + * Also merges any extra fields from the GraphQL schema's condition type + * (e.g., plugin-injected fields like vectorEmbedding from VectorSearchPlugin) + * that are not derived from the table's own columns. */ -function buildTableConditionProperties(table: CleanTable): InterfaceProperty[] { +function buildTableConditionProperties( + table: CleanTable, + typeRegistry?: TypeRegistry, +): InterfaceProperty[] { const properties: InterfaceProperty[] = []; + const generatedFieldNames = new Set(); for (const field of table.fields) { const fieldType = @@ -1133,6 +1141,30 @@ function buildTableConditionProperties(table: CleanTable): InterfaceProperty[] { type: `${tsType} | null`, optional: true, }); + generatedFieldNames.add(field.name); + } + + // Merge any additional fields from the schema's condition type + // (e.g., plugin-added fields like vectorEmbedding from VectorSearchPlugin) + if (typeRegistry) { + const conditionTypeName = getConditionTypeName(table); + const conditionType = typeRegistry.get(conditionTypeName); + if ( + conditionType?.kind === 'INPUT_OBJECT' && + conditionType.inputFields + ) { + for (const field of conditionType.inputFields) { + if (generatedFieldNames.has(field.name)) continue; + + const tsType = typeRefToTs(field.type); + properties.push({ + name: field.name, + type: tsType, + optional: true, + description: stripSmartComments(field.description, true), + }); + } + } } return properties; @@ -1141,7 +1173,10 @@ function buildTableConditionProperties(table: CleanTable): InterfaceProperty[] { /** * Generate table condition type statements */ -function generateTableConditionTypes(tables: CleanTable[]): t.Statement[] { +function generateTableConditionTypes( + tables: CleanTable[], + typeRegistry?: TypeRegistry, +): t.Statement[] { const statements: t.Statement[] = []; for (const table of tables) { @@ -1149,7 +1184,7 @@ function generateTableConditionTypes(tables: CleanTable[]): t.Statement[] { statements.push( createExportedInterface( conditionName, - buildTableConditionProperties(table), + buildTableConditionProperties(table, typeRegistry), ), ); } @@ -1165,9 +1200,16 @@ function generateTableConditionTypes(tables: CleanTable[]): t.Statement[] { // ============================================================================ /** - * Build OrderBy union type values + * Build OrderBy union type values. + * + * Also merges any extra values from the GraphQL schema's orderBy enum + * (e.g., plugin-injected values like EMBEDDING_DISTANCE_ASC/DESC + * from VectorSearchPlugin). */ -function buildOrderByValues(table: CleanTable): string[] { +function buildOrderByValues( + table: CleanTable, + typeRegistry?: TypeRegistry, +): string[] { const values: string[] = ['PRIMARY_KEY_ASC', 'PRIMARY_KEY_DESC', 'NATURAL']; for (const field of table.fields) { @@ -1177,18 +1219,36 @@ function buildOrderByValues(table: CleanTable): string[] { values.push(`${upperSnake}_DESC`); } + // Merge any additional values from the schema's orderBy enum type + // (e.g., plugin-added values like EMBEDDING_DISTANCE_ASC/DESC) + if (typeRegistry) { + const orderByTypeName = getOrderByTypeName(table); + const orderByType = typeRegistry.get(orderByTypeName); + if (orderByType?.kind === 'ENUM' && orderByType.enumValues) { + const existingValues = new Set(values); + for (const value of orderByType.enumValues) { + if (!existingValues.has(value)) { + values.push(value); + } + } + } + } + return values; } /** * Generate OrderBy type statements */ -function generateOrderByTypes(tables: CleanTable[]): t.Statement[] { +function generateOrderByTypes( + tables: CleanTable[], + typeRegistry?: TypeRegistry, +): t.Statement[] { const statements: t.Statement[] = []; for (const table of tables) { const enumName = getOrderByTypeName(table); - const values = buildOrderByValues(table); + const values = buildOrderByValues(table, typeRegistry); const unionType = createStringLiteralUnion(values); const typeAlias = t.tsTypeAliasDeclaration( t.identifier(enumName), @@ -1841,6 +1901,55 @@ function generateConnectionFieldsMap( return statements; } +// ============================================================================ +// Plugin-Injected Type Collector +// ============================================================================ + +/** + * Collect extra input type names referenced by plugin-injected condition fields. + * + * When plugins (like VectorSearchPlugin) inject fields into condition types, + * they reference types (like VectorNearbyInput, VectorMetric) that also need + * to be generated. This function discovers those types by comparing the + * schema's condition type fields against the table's own columns. + */ +function collectConditionExtraInputTypes( + tables: CleanTable[], + typeRegistry: TypeRegistry, +): Set { + const extraTypes = new Set(); + + for (const table of tables) { + const conditionTypeName = getConditionTypeName(table); + const conditionType = typeRegistry.get(conditionTypeName); + if ( + !conditionType || + conditionType.kind !== 'INPUT_OBJECT' || + !conditionType.inputFields + ) { + continue; + } + + const tableFieldNames = new Set( + table.fields + .filter((f) => !isRelationField(f.name, table)) + .map((f) => f.name), + ); + + for (const field of conditionType.inputFields) { + if (tableFieldNames.has(field.name)) continue; + + // Collect the base type name of this extra field + const baseName = getTypeBaseName(field.type); + if (baseName && !SCALAR_NAMES.has(baseName)) { + extraTypes.add(baseName); + } + } + } + + return extraTypes; +} + // ============================================================================ // Main Generator (AST-based) // ============================================================================ @@ -1889,10 +1998,14 @@ export function generateInputTypesFile( statements.push(...generateTableFilterTypes(tablesList)); // 4b. Table condition types (simple equality filter) - statements.push(...generateTableConditionTypes(tablesList)); + // Pass typeRegistry to merge plugin-injected condition fields + // (e.g., vectorEmbedding from VectorSearchPlugin) + statements.push(...generateTableConditionTypes(tablesList, typeRegistry)); // 5. OrderBy types - statements.push(...generateOrderByTypes(tablesList)); + // Pass typeRegistry to merge plugin-injected orderBy values + // (e.g., EMBEDDING_DISTANCE_ASC/DESC from VectorSearchPlugin) + statements.push(...generateOrderByTypes(tablesList, typeRegistry)); // 6. CRUD input types statements.push(...generateAllCrudInputTypes(tablesList, typeRegistry)); @@ -1903,9 +2016,20 @@ export function generateInputTypesFile( statements.push(...generateConnectionFieldsMap(tablesList, tableByName)); // 7. Custom input types from TypeRegistry + // Also include any extra types referenced by plugin-injected condition fields + const mergedUsedInputTypes = new Set(usedInputTypes); + if (hasTables) { + const conditionExtraTypes = collectConditionExtraInputTypes( + tablesList, + typeRegistry, + ); + for (const typeName of conditionExtraTypes) { + mergedUsedInputTypes.add(typeName); + } + } const tableCrudTypes = tables ? buildTableCrudTypeNames(tables) : undefined; statements.push( - ...generateCustomInputTypes(typeRegistry, usedInputTypes, tableCrudTypes, comments), + ...generateCustomInputTypes(typeRegistry, mergedUsedInputTypes, tableCrudTypes, comments), ); // 8. Payload/return types for custom operations diff --git a/graphql/codegen/src/core/codegen/scalars.ts b/graphql/codegen/src/core/codegen/scalars.ts index 579eb3398..95db6052d 100644 --- a/graphql/codegen/src/core/codegen/scalars.ts +++ b/graphql/codegen/src/core/codegen/scalars.ts @@ -39,6 +39,9 @@ export const SCALAR_TS_MAP: Record = { TsVector: 'string', TsQuery: 'string', + // Vector types (pgvector) + Vector: 'number[]', + // File upload Upload: 'File', }; From 0ffda8eb9f6d56d5a3e34142b6372aa23b1abf23 Mon Sep 17 00:00:00 2001 From: Dan Lynch Date: Thu, 12 Mar 2026 01:23:16 +0000 Subject: [PATCH 2/3] fix(codegen): resolve transitive enum types referenced by input fields MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, generateCustomInputTypes only followed nested types whose names ended with 'Input'. This meant enum types like VectorMetric (referenced by VectorNearbyInput.metric) were silently dropped from generated output, producing TypeScript with undefined type references. Now follows all non-scalar types that exist in the typeRegistry, including enums and other type kinds. Added unit test verifying transitive enum resolution through VectorNearbyInput → VectorMetric. --- .../codegen/input-types-generator.test.ts | 57 +++++++++++++++++++ .../core/codegen/orm/input-types-generator.ts | 7 ++- 2 files changed, 61 insertions(+), 3 deletions(-) diff --git a/graphql/codegen/src/__tests__/codegen/input-types-generator.test.ts b/graphql/codegen/src/__tests__/codegen/input-types-generator.test.ts index 25c8f136e..3746c73fd 100644 --- a/graphql/codegen/src/__tests__/codegen/input-types-generator.test.ts +++ b/graphql/codegen/src/__tests__/codegen/input-types-generator.test.ts @@ -966,6 +966,63 @@ describe('plugin-injected condition fields', () => { // The referenced VectorNearbyInput type should be generated as a custom input type expect(result.content).toContain('export interface VectorNearbyInput {'); + + // Transitively referenced enum type (VectorMetric) should also be generated + expect(result.content).toContain('VectorMetric'); + expect(result.content).toContain('"L2"'); + expect(result.content).toContain('"INNER_PRODUCT"'); + expect(result.content).toContain('"COSINE"'); + }); + + it('generates transitively referenced enum types from input fields', () => { + // This specifically tests that enum types referenced by input object fields + // are followed and generated, not just types ending with "Input". + // VectorNearbyInput.metric references VectorMetric (an ENUM), + // which must be included in the output. + const registry = createTypeRegistry({ + ContactCondition: { + kind: 'INPUT_OBJECT', + name: 'ContactCondition', + inputFields: [ + { name: 'id', type: createTypeRef('SCALAR', 'UUID') }, + { name: 'name', type: createTypeRef('SCALAR', 'String') }, + { + name: 'embeddingNearby', + type: createTypeRef('INPUT_OBJECT', 'VectorNearbyInput'), + }, + ], + }, + VectorNearbyInput: { + kind: 'INPUT_OBJECT', + name: 'VectorNearbyInput', + inputFields: [ + { + name: 'vector', + type: createNonNull(createTypeRef('SCALAR', 'Vector')), + }, + { + name: 'metric', + type: createTypeRef('ENUM', 'VectorMetric'), + }, + ], + }, + VectorMetric: { + kind: 'ENUM', + name: 'VectorMetric', + enumValues: ['L2', 'INNER_PRODUCT', 'COSINE'], + }, + }); + + const result = generateInputTypesFile(registry, new Set(), [contactTable]); + + // VectorNearbyInput should be generated (follows *Input pattern) + expect(result.content).toContain('export interface VectorNearbyInput {'); + + // VectorMetric enum should ALSO be generated (transitive enum resolution) + expect(result.content).toMatch(/export type VectorMetric\s*=/); + expect(result.content).toContain('"L2"'); + expect(result.content).toContain('"INNER_PRODUCT"'); + expect(result.content).toContain('"COSINE"'); }); it('does not duplicate fields already derived from table columns', () => { diff --git a/graphql/codegen/src/core/codegen/orm/input-types-generator.ts b/graphql/codegen/src/core/codegen/orm/input-types-generator.ts index 8717c7ddb..09b218ad9 100644 --- a/graphql/codegen/src/core/codegen/orm/input-types-generator.ts +++ b/graphql/codegen/src/core/codegen/orm/input-types-generator.ts @@ -1616,12 +1616,13 @@ function generateCustomInputTypes( description: stripSmartComments(field.description, comments), }); - // Follow nested Input types + // Follow nested types (Input objects, Enums, etc.) that exist in the registry const baseType = getTypeBaseName(field.type); if ( baseType && - baseType.endsWith('Input') && - !generatedTypes.has(baseType) + !SCALAR_NAMES.has(baseType) && + !generatedTypes.has(baseType) && + typeRegistry.has(baseType) ) { typesToGenerate.add(baseType); } From 7aa49cbd3db1d1c204f7902394cf03771b6674a6 Mon Sep 17 00:00:00 2001 From: Dan Lynch Date: Thu, 12 Mar 2026 01:37:31 +0000 Subject: [PATCH 3/3] fix(codegen): add VectorFilter scalar filter type for pgvector fields MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds Vector → VectorFilter mapping in SCALAR_FILTER_MAP and VectorFilter config (equality + distinct operators) in SCALAR_FILTER_CONFIGS, so that vector embedding fields in Filter types use VectorFilter instead of falling back to StringFilter. Includes unit test and updated snapshots. --- .../input-types-generator.test.ts.snap | 49 +++++++++++++++++++ .../react-query-hooks.test.ts.snap | 4 +- .../schema-types-generator.test.ts.snap | 10 ++-- .../codegen/input-types-generator.test.ts | 12 +++++ .../core/codegen/orm/input-types-generator.ts | 2 + graphql/codegen/src/core/codegen/scalars.ts | 1 + 6 files changed, 71 insertions(+), 7 deletions(-) diff --git a/graphql/codegen/src/__tests__/codegen/__snapshots__/input-types-generator.test.ts.snap b/graphql/codegen/src/__tests__/codegen/__snapshots__/input-types-generator.test.ts.snap index 713a844ee..afa9eb471 100644 --- a/graphql/codegen/src/__tests__/codegen/__snapshots__/input-types-generator.test.ts.snap +++ b/graphql/codegen/src/__tests__/codegen/__snapshots__/input-types-generator.test.ts.snap @@ -166,6 +166,13 @@ export interface InternetAddressFilter { export interface FullTextFilter { matches?: string; } +export interface VectorFilter { + isNull?: boolean; + equalTo?: number[]; + notEqualTo?: number[]; + distinctFrom?: number[]; + notDistinctFrom?: number[]; +} export interface StringListFilter { isNull?: boolean; equalTo?: string[]; @@ -648,6 +655,13 @@ export interface InternetAddressFilter { export interface FullTextFilter { matches?: string; } +export interface VectorFilter { + isNull?: boolean; + equalTo?: number[]; + notEqualTo?: number[]; + distinctFrom?: number[]; + notDistinctFrom?: number[]; +} export interface StringListFilter { isNull?: boolean; equalTo?: string[]; @@ -966,6 +980,13 @@ export interface InternetAddressFilter { export interface FullTextFilter { matches?: string; } +export interface VectorFilter { + isNull?: boolean; + equalTo?: number[]; + notEqualTo?: number[]; + distinctFrom?: number[]; + notDistinctFrom?: number[]; +} export interface StringListFilter { isNull?: boolean; equalTo?: string[]; @@ -1296,6 +1317,13 @@ export interface InternetAddressFilter { export interface FullTextFilter { matches?: string; } +export interface VectorFilter { + isNull?: boolean; + equalTo?: number[]; + notEqualTo?: number[]; + distinctFrom?: number[]; + notDistinctFrom?: number[]; +} export interface StringListFilter { isNull?: boolean; equalTo?: string[]; @@ -1631,6 +1659,13 @@ export interface InternetAddressFilter { export interface FullTextFilter { matches?: string; } +export interface VectorFilter { + isNull?: boolean; + equalTo?: number[]; + notEqualTo?: number[]; + distinctFrom?: number[]; + notDistinctFrom?: number[]; +} export interface StringListFilter { isNull?: boolean; equalTo?: string[]; @@ -2022,6 +2057,13 @@ export interface InternetAddressFilter { export interface FullTextFilter { matches?: string; } +export interface VectorFilter { + isNull?: boolean; + equalTo?: number[]; + notEqualTo?: number[]; + distinctFrom?: number[]; + notDistinctFrom?: number[]; +} export interface StringListFilter { isNull?: boolean; equalTo?: string[]; @@ -2409,6 +2451,13 @@ export interface InternetAddressFilter { export interface FullTextFilter { matches?: string; } +export interface VectorFilter { + isNull?: boolean; + equalTo?: number[]; + notEqualTo?: number[]; + distinctFrom?: number[]; + notDistinctFrom?: number[]; +} export interface StringListFilter { isNull?: boolean; equalTo?: string[]; diff --git a/graphql/codegen/src/__tests__/codegen/__snapshots__/react-query-hooks.test.ts.snap b/graphql/codegen/src/__tests__/codegen/__snapshots__/react-query-hooks.test.ts.snap index 2f15ed76f..89631177b 100644 --- a/graphql/codegen/src/__tests__/codegen/__snapshots__/react-query-hooks.test.ts.snap +++ b/graphql/codegen/src/__tests__/codegen/__snapshots__/react-query-hooks.test.ts.snap @@ -2138,7 +2138,7 @@ exports[`Schema Types Generator generateSchemaTypesFile generates schema types f * DO NOT EDIT - changes will be overwritten */ -import type { BigFloatFilter, BigIntFilter, BitStringFilter, BooleanFilter, DateFilter, DatetimeFilter, FloatFilter, FullTextFilter, IntFilter, IntListFilter, InternetAddressFilter, JSONFilter, StringFilter, StringListFilter, UUIDFilter, UUIDListFilter } from "./types"; +import type { BigFloatFilter, BigIntFilter, BitStringFilter, BooleanFilter, DateFilter, DatetimeFilter, FloatFilter, FullTextFilter, IntFilter, IntListFilter, InternetAddressFilter, JSONFilter, StringFilter, StringListFilter, UUIDFilter, UUIDListFilter, VectorFilter } from "./types"; export type UserRole = "ADMIN" | "USER" | "GUEST"; export interface RegisterInput { email: string; @@ -2165,7 +2165,7 @@ exports[`Schema Types Generator generateSchemaTypesFile generates schema types f * DO NOT EDIT - changes will be overwritten */ -import type { User, BigFloatFilter, BigIntFilter, BitStringFilter, BooleanFilter, DateFilter, DatetimeFilter, FloatFilter, FullTextFilter, IntFilter, IntListFilter, InternetAddressFilter, JSONFilter, StringFilter, StringListFilter, UUIDFilter, UUIDListFilter } from "./types"; +import type { User, BigFloatFilter, BigIntFilter, BitStringFilter, BooleanFilter, DateFilter, DatetimeFilter, FloatFilter, FullTextFilter, IntFilter, IntListFilter, InternetAddressFilter, JSONFilter, StringFilter, StringListFilter, UUIDFilter, UUIDListFilter, VectorFilter } from "./types"; export type UserRole = "ADMIN" | "USER" | "GUEST"; export interface RegisterInput { email: string; diff --git a/graphql/codegen/src/__tests__/codegen/__snapshots__/schema-types-generator.test.ts.snap b/graphql/codegen/src/__tests__/codegen/__snapshots__/schema-types-generator.test.ts.snap index e7d4d48d2..4115d64a6 100644 --- a/graphql/codegen/src/__tests__/codegen/__snapshots__/schema-types-generator.test.ts.snap +++ b/graphql/codegen/src/__tests__/codegen/__snapshots__/schema-types-generator.test.ts.snap @@ -7,7 +7,7 @@ exports[`schema-types-generator generates enum types as string unions 1`] = ` * DO NOT EDIT - changes will be overwritten */ -import type { BigFloatFilter, BigIntFilter, BitStringFilter, BooleanFilter, DateFilter, DatetimeFilter, FloatFilter, FullTextFilter, IntFilter, IntListFilter, InternetAddressFilter, JSONFilter, StringFilter, StringListFilter, UUIDFilter, UUIDListFilter } from "./types"; +import type { BigFloatFilter, BigIntFilter, BitStringFilter, BooleanFilter, DateFilter, DatetimeFilter, FloatFilter, FullTextFilter, IntFilter, IntListFilter, InternetAddressFilter, JSONFilter, StringFilter, StringListFilter, UUIDFilter, UUIDListFilter, VectorFilter } from "./types"; export type Status = "ACTIVE" | "INACTIVE" | "PENDING"; export type Priority = "LOW" | "MEDIUM" | "HIGH";" `; @@ -19,7 +19,7 @@ exports[`schema-types-generator generates input object types as interfaces 1`] = * DO NOT EDIT - changes will be overwritten */ -import type { BigFloatFilter, BigIntFilter, BitStringFilter, BooleanFilter, DateFilter, DatetimeFilter, FloatFilter, FullTextFilter, IntFilter, IntListFilter, InternetAddressFilter, JSONFilter, StringFilter, StringListFilter, UUIDFilter, UUIDListFilter } from "./types"; +import type { BigFloatFilter, BigIntFilter, BitStringFilter, BooleanFilter, DateFilter, DatetimeFilter, FloatFilter, FullTextFilter, IntFilter, IntListFilter, InternetAddressFilter, JSONFilter, StringFilter, StringListFilter, UUIDFilter, UUIDListFilter, VectorFilter } from "./types"; export interface CreateUserInput { email: string; name?: string; @@ -38,7 +38,7 @@ exports[`schema-types-generator generates payload types from mutation return typ * DO NOT EDIT - changes will be overwritten */ -import type { User, BigFloatFilter, BigIntFilter, BitStringFilter, BooleanFilter, DateFilter, DatetimeFilter, FloatFilter, FullTextFilter, IntFilter, IntListFilter, InternetAddressFilter, JSONFilter, StringFilter, StringListFilter, UUIDFilter, UUIDListFilter } from "./types"; +import type { User, BigFloatFilter, BigIntFilter, BitStringFilter, BooleanFilter, DateFilter, DatetimeFilter, FloatFilter, FullTextFilter, IntFilter, IntListFilter, InternetAddressFilter, JSONFilter, StringFilter, StringListFilter, UUIDFilter, UUIDListFilter, VectorFilter } from "./types"; export interface LoginPayload { token: string; refreshToken?: string | null; @@ -53,7 +53,7 @@ exports[`schema-types-generator generates union types 1`] = ` * DO NOT EDIT - changes will be overwritten */ -import type { BigFloatFilter, BigIntFilter, BitStringFilter, BooleanFilter, DateFilter, DatetimeFilter, FloatFilter, FullTextFilter, IntFilter, IntListFilter, InternetAddressFilter, JSONFilter, StringFilter, StringListFilter, UUIDFilter, UUIDListFilter } from "./types"; +import type { BigFloatFilter, BigIntFilter, BitStringFilter, BooleanFilter, DateFilter, DatetimeFilter, FloatFilter, FullTextFilter, IntFilter, IntListFilter, InternetAddressFilter, JSONFilter, StringFilter, StringListFilter, UUIDFilter, UUIDListFilter, VectorFilter } from "./types"; export type SearchResult = User | Post | Comment;" `; @@ -64,6 +64,6 @@ exports[`schema-types-generator skips table types and standard scalars 1`] = ` * DO NOT EDIT - changes will be overwritten */ -import type { BigFloatFilter, BigIntFilter, BitStringFilter, BooleanFilter, DateFilter, DatetimeFilter, FloatFilter, FullTextFilter, IntFilter, IntListFilter, InternetAddressFilter, JSONFilter, StringFilter, StringListFilter, UUIDFilter, UUIDListFilter } from "./types"; +import type { BigFloatFilter, BigIntFilter, BitStringFilter, BooleanFilter, DateFilter, DatetimeFilter, FloatFilter, FullTextFilter, IntFilter, IntListFilter, InternetAddressFilter, JSONFilter, StringFilter, StringListFilter, UUIDFilter, UUIDListFilter, VectorFilter } from "./types"; export type CustomEnum = "VALUE_A" | "VALUE_B";" `; diff --git a/graphql/codegen/src/__tests__/codegen/input-types-generator.test.ts b/graphql/codegen/src/__tests__/codegen/input-types-generator.test.ts index 3746c73fd..577134398 100644 --- a/graphql/codegen/src/__tests__/codegen/input-types-generator.test.ts +++ b/graphql/codegen/src/__tests__/codegen/input-types-generator.test.ts @@ -455,6 +455,18 @@ describe('scalar filter types', () => { // Float filters expect(result.content).toContain('export interface FloatFilter {'); }); + + it('includes VectorFilter for pgvector embedding fields', () => { + const result = generateInputTypesFile(new Map(), new Set(), [userTable]); + + // VectorFilter should be generated as a scalar filter type + expect(result.content).toContain('export interface VectorFilter {'); + expect(result.content).toContain('isNull?: boolean;'); + expect(result.content).toContain('equalTo?: number[];'); + expect(result.content).toContain('notEqualTo?: number[];'); + expect(result.content).toContain('distinctFrom?: number[];'); + expect(result.content).toContain('notDistinctFrom?: number[];'); + }); }); // ============================================================================ diff --git a/graphql/codegen/src/core/codegen/orm/input-types-generator.ts b/graphql/codegen/src/core/codegen/orm/input-types-generator.ts index 09b218ad9..31253bbf2 100644 --- a/graphql/codegen/src/core/codegen/orm/input-types-generator.ts +++ b/graphql/codegen/src/core/codegen/orm/input-types-generator.ts @@ -322,6 +322,8 @@ const SCALAR_FILTER_CONFIGS: ScalarFilterConfig[] = [ operators: ['equality', 'distinct', 'inArray', 'comparison', 'inet'], }, { name: 'FullTextFilter', tsType: 'string', operators: ['fulltext'] }, + // Vector filter (for pgvector embedding fields) + { name: 'VectorFilter', tsType: 'number[]', operators: ['equality', 'distinct'] }, // List filters (for array fields like string[], int[], uuid[]) { name: 'StringListFilter', diff --git a/graphql/codegen/src/core/codegen/scalars.ts b/graphql/codegen/src/core/codegen/scalars.ts index 95db6052d..a5e5914e1 100644 --- a/graphql/codegen/src/core/codegen/scalars.ts +++ b/graphql/codegen/src/core/codegen/scalars.ts @@ -61,6 +61,7 @@ export const SCALAR_FILTER_MAP: Record = { BigFloat: 'BigFloatFilter', BitString: 'BitStringFilter', InternetAddress: 'InternetAddressFilter', + Vector: 'VectorFilter', FullText: 'FullTextFilter', Interval: 'StringFilter', };