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 8e0c7d8de..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[];'); + }); }); // ============================================================================ @@ -877,6 +889,229 @@ 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 {'); + + // 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', () => { + 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..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', @@ -1116,10 +1118,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 +1143,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 +1175,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 +1186,7 @@ function generateTableConditionTypes(tables: CleanTable[]): t.Statement[] { statements.push( createExportedInterface( conditionName, - buildTableConditionProperties(table), + buildTableConditionProperties(table, typeRegistry), ), ); } @@ -1165,9 +1202,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 +1221,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), @@ -1556,12 +1618,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); } @@ -1841,6 +1904,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 +2001,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 +2019,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..a5e5914e1 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', }; @@ -58,6 +61,7 @@ export const SCALAR_FILTER_MAP: Record = { BigFloat: 'BigFloatFilter', BitString: 'BitStringFilter', InternetAddress: 'InternetAddressFilter', + Vector: 'VectorFilter', FullText: 'FullTextFilter', Interval: 'StringFilter', };