From 3c4a91a85ab2290fd2aa54c3359a701c3db8f23e Mon Sep 17 00:00:00 2001 From: Dan Lynch Date: Thu, 12 Mar 2026 01:04:27 +0000 Subject: [PATCH 1/5] 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/5] 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/5] 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', }; From 1588296da227fe56633bbe5b4165858544615753 Mon Sep 17 00:00:00 2001 From: Dan Lynch Date: Thu, 12 Mar 2026 03:12:22 +0000 Subject: [PATCH 4/5] feat(codegen): wire condition arg through ORM query builder for findMany/findFirst - Add TCondition generic and condition?: TCondition to FindManyArgs and FindFirstArgs - Add conditionTypeName parameter and addVariable() call in buildFindManyDocument/buildFindFirstDocument - Wire ${TypeName}Condition into model-generator for findMany/findFirst methods - Add unit tests for condition wiring in model-generator and query-builder - Update snapshots --- .../client-generator.test.ts.snap | 6 +- .../model-generator.test.ts.snap | 42 +++++++----- .../__tests__/codegen/model-generator.test.ts | 38 +++++++++++ .../__tests__/codegen/query-builder.test.ts | 65 +++++++++++++++++-- .../src/core/codegen/orm/model-generator.ts | 24 +++++++ .../src/core/codegen/orm/select-types.ts | 6 +- .../core/codegen/templates/query-builder.ts | 33 ++++++++-- .../core/codegen/templates/select-types.ts | 6 +- 8 files changed, 187 insertions(+), 33 deletions(-) diff --git a/graphql/codegen/src/__tests__/codegen/__snapshots__/client-generator.test.ts.snap b/graphql/codegen/src/__tests__/codegen/__snapshots__/client-generator.test.ts.snap index 2645adc8b..4e96b9c09 100644 --- a/graphql/codegen/src/__tests__/codegen/__snapshots__/client-generator.test.ts.snap +++ b/graphql/codegen/src/__tests__/codegen/__snapshots__/client-generator.test.ts.snap @@ -276,9 +276,10 @@ export interface PageInfo { endCursor?: string | null; } -export interface FindManyArgs { +export interface FindManyArgs { select?: TSelect; where?: TWhere; + condition?: TCondition; orderBy?: TOrderBy[]; first?: number; last?: number; @@ -287,9 +288,10 @@ export interface FindManyArgs { offset?: number; } -export interface FindFirstArgs { +export interface FindFirstArgs { select?: TSelect; where?: TWhere; + condition?: TCondition; } export interface CreateArgs { diff --git a/graphql/codegen/src/__tests__/codegen/__snapshots__/model-generator.test.ts.snap b/graphql/codegen/src/__tests__/codegen/__snapshots__/model-generator.test.ts.snap index 96a940f74..4f11f28fe 100644 --- a/graphql/codegen/src/__tests__/codegen/__snapshots__/model-generator.test.ts.snap +++ b/graphql/codegen/src/__tests__/codegen/__snapshots__/model-generator.test.ts.snap @@ -9,11 +9,11 @@ exports[`model-generator generates model with all CRUD methods 1`] = ` import { OrmClient } from "../client"; import { QueryBuilder, buildFindManyDocument, buildFindFirstDocument, buildFindOneDocument, buildCreateDocument, buildUpdateByPkDocument, buildDeleteByPkDocument } from "../query-builder"; import type { ConnectionResult, FindManyArgs, FindFirstArgs, CreateArgs, UpdateArgs, DeleteArgs, InferSelectResult, StrictSelect } from "../select-types"; -import type { User, UserWithRelations, UserSelect, UserFilter, UsersOrderBy, CreateUserInput, UpdateUserInput, UserPatch } from "../input-types"; +import type { User, UserWithRelations, UserSelect, UserFilter, UserCondition, UsersOrderBy, CreateUserInput, UpdateUserInput, UserPatch } from "../input-types"; import { connectionFieldsMap } from "../input-types"; export class UserModel { constructor(private client: OrmClient) {} - findMany(args: FindManyArgs & { + findMany(args: FindManyArgs & { select: S; } & StrictSelect): QueryBuilder<{ users: ConnectionResult>; @@ -23,13 +23,14 @@ export class UserModel { variables } = buildFindManyDocument("User", "users", args.select, { where: args?.where, + condition: args?.condition, orderBy: args?.orderBy as string[] | undefined, first: args?.first, last: args?.last, after: args?.after, before: args?.before, offset: args?.offset - }, "UserFilter", "UsersOrderBy", connectionFieldsMap); + }, "UserFilter", "UsersOrderBy", connectionFieldsMap, "UserCondition"); return new QueryBuilder({ client: this.client, operation: "query", @@ -39,7 +40,7 @@ export class UserModel { variables }); } - findFirst(args: FindFirstArgs & { + findFirst(args: FindFirstArgs & { select: S; } & StrictSelect): QueryBuilder<{ users: { @@ -50,8 +51,9 @@ export class UserModel { document, variables } = buildFindFirstDocument("User", "users", args.select, { - where: args?.where - }, "UserFilter", connectionFieldsMap); + where: args?.where, + condition: args?.condition + }, "UserFilter", connectionFieldsMap, "UserCondition"); return new QueryBuilder({ client: this.client, operation: "query", @@ -156,11 +158,11 @@ exports[`model-generator generates model without update/delete when not availabl import { OrmClient } from "../client"; import { QueryBuilder, buildFindManyDocument, buildFindFirstDocument, buildFindOneDocument, buildCreateDocument, buildUpdateByPkDocument, buildDeleteByPkDocument } from "../query-builder"; import type { ConnectionResult, FindManyArgs, FindFirstArgs, CreateArgs, UpdateArgs, DeleteArgs, InferSelectResult, StrictSelect } from "../select-types"; -import type { AuditLog, AuditLogWithRelations, AuditLogSelect, AuditLogFilter, AuditLogsOrderBy, CreateAuditLogInput, UpdateAuditLogInput, AuditLogPatch } from "../input-types"; +import type { AuditLog, AuditLogWithRelations, AuditLogSelect, AuditLogFilter, AuditLogCondition, AuditLogsOrderBy, CreateAuditLogInput, UpdateAuditLogInput, AuditLogPatch } from "../input-types"; import { connectionFieldsMap } from "../input-types"; export class AuditLogModel { constructor(private client: OrmClient) {} - findMany(args: FindManyArgs & { + findMany(args: FindManyArgs & { select: S; } & StrictSelect): QueryBuilder<{ auditLogs: ConnectionResult>; @@ -170,13 +172,14 @@ export class AuditLogModel { variables } = buildFindManyDocument("AuditLog", "auditLogs", args.select, { where: args?.where, + condition: args?.condition, orderBy: args?.orderBy as string[] | undefined, first: args?.first, last: args?.last, after: args?.after, before: args?.before, offset: args?.offset - }, "AuditLogFilter", "AuditLogsOrderBy", connectionFieldsMap); + }, "AuditLogFilter", "AuditLogsOrderBy", connectionFieldsMap, "AuditLogCondition"); return new QueryBuilder({ client: this.client, operation: "query", @@ -186,7 +189,7 @@ export class AuditLogModel { variables }); } - findFirst(args: FindFirstArgs & { + findFirst(args: FindFirstArgs & { select: S; } & StrictSelect): QueryBuilder<{ auditLogs: { @@ -197,8 +200,9 @@ export class AuditLogModel { document, variables } = buildFindFirstDocument("AuditLog", "auditLogs", args.select, { - where: args?.where - }, "AuditLogFilter", connectionFieldsMap); + where: args?.where, + condition: args?.condition + }, "AuditLogFilter", connectionFieldsMap, "AuditLogCondition"); return new QueryBuilder({ client: this.client, operation: "query", @@ -259,11 +263,11 @@ exports[`model-generator handles custom query/mutation names 1`] = ` import { OrmClient } from "../client"; import { QueryBuilder, buildFindManyDocument, buildFindFirstDocument, buildFindOneDocument, buildCreateDocument, buildUpdateByPkDocument, buildDeleteByPkDocument } from "../query-builder"; import type { ConnectionResult, FindManyArgs, FindFirstArgs, CreateArgs, UpdateArgs, DeleteArgs, InferSelectResult, StrictSelect } from "../select-types"; -import type { Organization, OrganizationWithRelations, OrganizationSelect, OrganizationFilter, OrganizationsOrderBy, CreateOrganizationInput, UpdateOrganizationInput, OrganizationPatch } from "../input-types"; +import type { Organization, OrganizationWithRelations, OrganizationSelect, OrganizationFilter, OrganizationCondition, OrganizationsOrderBy, CreateOrganizationInput, UpdateOrganizationInput, OrganizationPatch } from "../input-types"; import { connectionFieldsMap } from "../input-types"; export class OrganizationModel { constructor(private client: OrmClient) {} - findMany(args: FindManyArgs & { + findMany(args: FindManyArgs & { select: S; } & StrictSelect): QueryBuilder<{ allOrganizations: ConnectionResult>; @@ -273,13 +277,14 @@ export class OrganizationModel { variables } = buildFindManyDocument("Organization", "allOrganizations", args.select, { where: args?.where, + condition: args?.condition, orderBy: args?.orderBy as string[] | undefined, first: args?.first, last: args?.last, after: args?.after, before: args?.before, offset: args?.offset - }, "OrganizationFilter", "OrganizationsOrderBy", connectionFieldsMap); + }, "OrganizationFilter", "OrganizationsOrderBy", connectionFieldsMap, "OrganizationCondition"); return new QueryBuilder({ client: this.client, operation: "query", @@ -289,7 +294,7 @@ export class OrganizationModel { variables }); } - findFirst(args: FindFirstArgs & { + findFirst(args: FindFirstArgs & { select: S; } & StrictSelect): QueryBuilder<{ allOrganizations: { @@ -300,8 +305,9 @@ export class OrganizationModel { document, variables } = buildFindFirstDocument("Organization", "allOrganizations", args.select, { - where: args?.where - }, "OrganizationFilter", connectionFieldsMap); + where: args?.where, + condition: args?.condition + }, "OrganizationFilter", connectionFieldsMap, "OrganizationCondition"); return new QueryBuilder({ client: this.client, operation: "query", diff --git a/graphql/codegen/src/__tests__/codegen/model-generator.test.ts b/graphql/codegen/src/__tests__/codegen/model-generator.test.ts index 577710a3e..84ac95a91 100644 --- a/graphql/codegen/src/__tests__/codegen/model-generator.test.ts +++ b/graphql/codegen/src/__tests__/codegen/model-generator.test.ts @@ -232,4 +232,42 @@ describe('model-generator', () => { expect(result.content).toContain('UpdateProductInput'); expect(result.content).toContain('ProductPatch'); }); + + it('imports and wires Condition type for findMany and findFirst', () => { + const table = createTable({ + name: 'Contact', + fields: [ + { name: 'id', type: fieldTypes.uuid }, + { name: 'name', type: fieldTypes.string }, + ], + query: { + all: 'contacts', + one: 'contact', + create: 'createContact', + update: 'updateContact', + delete: 'deleteContact', + }, + }); + + const result = generateModelFile(table, false); + + // Condition type should be imported + expect(result.content).toContain('ContactCondition'); + + // findMany should include condition in its args type + expect(result.content).toContain( + 'FindManyArgs', + ); + + // findFirst should include condition in its args type + expect(result.content).toContain( + 'FindFirstArgs', + ); + + // condition should be forwarded in the body args object + expect(result.content).toContain('condition: args?.condition'); + + // conditionTypeName should be passed as a string literal to the document builder + expect(result.content).toContain('"ContactCondition"'); + }); }); diff --git a/graphql/codegen/src/__tests__/codegen/query-builder.test.ts b/graphql/codegen/src/__tests__/codegen/query-builder.test.ts index c1ae2fbec..a2bba9375 100644 --- a/graphql/codegen/src/__tests__/codegen/query-builder.test.ts +++ b/graphql/codegen/src/__tests__/codegen/query-builder.test.ts @@ -38,12 +38,12 @@ function buildConnectionSelections(nodeSelections: FieldNode[]): FieldNode[] { } function addVariable( - spec: { varName: string; argName?: string; typeName: string; value: unknown }, + spec: { varName: string; argName?: string; typeName?: string; value: unknown }, definitions: VariableDefinitionNode[], args: ArgumentNode[], variables: Record, ): void { - if (spec.value === undefined) return; + if (spec.value === undefined || !spec.typeName) return; definitions.push( t.variableDefinition({ variable: t.variable({ name: spec.varName }), @@ -123,14 +123,15 @@ function buildSelections( return fields; } -function buildFindManyDocument( +function buildFindManyDocument( operationName: string, queryField: string, select: TSelect, - args: { where?: TWhere; first?: number; orderBy?: string[] }, + args: { where?: TWhere; condition?: TCondition; first?: number; orderBy?: string[] }, filterTypeName: string, orderByTypeName: string, connectionFieldsMap?: Record>, + conditionTypeName?: string, ): { document: string; variables: Record } { const selections = select ? buildSelections( @@ -143,6 +144,16 @@ function buildFindManyDocument( const queryArgs: ArgumentNode[] = []; const variables: Record = {}; + addVariable( + { + varName: 'condition', + typeName: conditionTypeName, + value: args.condition, + }, + variableDefinitions, + queryArgs, + variables, + ); addVariable( { varName: 'where', @@ -557,6 +568,52 @@ describe('query-builder', () => { orderBy: ['NAME_ASC'], }); }); + + it('includes condition variable when conditionTypeName is provided', () => { + const { document, variables } = buildFindManyDocument( + 'Contacts', + 'contacts', + { id: true, name: true }, + { + condition: { embeddingNearby: { vector: [0.1, 0.2], metric: 'COSINE' } }, + where: { name: { equalTo: 'test' } }, + first: 5, + }, + 'ContactFilter', + 'ContactsOrderBy', + undefined, + 'ContactCondition', + ); + + // condition variable should appear in the query + expect(document).toContain('$condition: ContactCondition'); + expect(document).toContain('condition: $condition'); + // filter should still work alongside condition + expect(document).toContain('$where: ContactFilter'); + expect(document).toContain('filter: $where'); + // variables should include both + expect(variables.condition).toEqual({ + embeddingNearby: { vector: [0.1, 0.2], metric: 'COSINE' }, + }); + expect(variables.where).toEqual({ name: { equalTo: 'test' } }); + }); + + it('omits condition variable when not provided', () => { + const { document } = buildFindManyDocument( + 'Users', + 'users', + { id: true }, + { first: 10 }, + 'UserFilter', + 'UsersOrderBy', + undefined, + 'UserCondition', + ); + + // condition should NOT appear since no value was provided + expect(document).not.toContain('$condition'); + expect(document).not.toContain('condition:'); + }); }); describe('buildMutationDocument', () => { diff --git a/graphql/codegen/src/core/codegen/orm/model-generator.ts b/graphql/codegen/src/core/codegen/orm/model-generator.ts index 3d426ab5b..66c591b34 100644 --- a/graphql/codegen/src/core/codegen/orm/model-generator.ts +++ b/graphql/codegen/src/core/codegen/orm/model-generator.ts @@ -175,6 +175,7 @@ export function generateModelFile( const selectTypeName = `${typeName}Select`; const relationTypeName = `${typeName}WithRelations`; const whereTypeName = getFilterTypeName(table); + const conditionTypeName = `${typeName}Condition`; const orderByTypeName = getOrderByTypeName(table); const createInputTypeName = `Create${typeName}Input`; const updateInputTypeName = `Update${typeName}Input`; @@ -228,6 +229,7 @@ export function generateModelFile( relationTypeName, selectTypeName, whereTypeName, + conditionTypeName, orderByTypeName, createInputTypeName, updateInputTypeName, @@ -270,6 +272,7 @@ export function generateModelFile( t.tsTypeParameterInstantiation([ sel, t.tsTypeReference(t.identifier(whereTypeName)), + t.tsTypeReference(t.identifier(conditionTypeName)), t.tsTypeReference(t.identifier(orderByTypeName)), ]), ); @@ -327,6 +330,15 @@ export function generateModelFile( true, ), ), + t.objectProperty( + t.identifier('condition'), + t.optionalMemberExpression( + t.identifier('args'), + t.identifier('condition'), + false, + true, + ), + ), t.objectProperty( t.identifier('orderBy'), t.tsAsExpression( @@ -391,6 +403,7 @@ export function generateModelFile( t.stringLiteral(whereTypeName), t.stringLiteral(orderByTypeName), t.identifier('connectionFieldsMap'), + t.stringLiteral(conditionTypeName), ]; classBody.push( createClassMethod( @@ -417,6 +430,7 @@ export function generateModelFile( t.tsTypeParameterInstantiation([ sel, t.tsTypeReference(t.identifier(whereTypeName)), + t.tsTypeReference(t.identifier(conditionTypeName)), ]), ); const retType = (sel: t.TSType) => @@ -477,9 +491,19 @@ export function generateModelFile( true, ), ), + t.objectProperty( + t.identifier('condition'), + t.optionalMemberExpression( + t.identifier('args'), + t.identifier('condition'), + false, + true, + ), + ), ]), t.stringLiteral(whereTypeName), t.identifier('connectionFieldsMap'), + t.stringLiteral(conditionTypeName), ]; classBody.push( createClassMethod( diff --git a/graphql/codegen/src/core/codegen/orm/select-types.ts b/graphql/codegen/src/core/codegen/orm/select-types.ts index 7bc20bad9..098bef89c 100644 --- a/graphql/codegen/src/core/codegen/orm/select-types.ts +++ b/graphql/codegen/src/core/codegen/orm/select-types.ts @@ -201,9 +201,10 @@ export interface PageInfo { /** * Arguments for findMany operations */ -export interface FindManyArgs { +export interface FindManyArgs { select?: TSelect; where?: TWhere; + condition?: TCondition; orderBy?: TOrderBy[]; first?: number; last?: number; @@ -215,9 +216,10 @@ export interface FindManyArgs { /** * Arguments for findFirst/findUnique operations */ -export interface FindFirstArgs { +export interface FindFirstArgs { select?: TSelect; where?: TWhere; + condition?: TCondition; } /** diff --git a/graphql/codegen/src/core/codegen/templates/query-builder.ts b/graphql/codegen/src/core/codegen/templates/query-builder.ts index 5756c6713..045f86a3d 100644 --- a/graphql/codegen/src/core/codegen/templates/query-builder.ts +++ b/graphql/codegen/src/core/codegen/templates/query-builder.ts @@ -201,12 +201,13 @@ export function buildSelections( // Document Builders // ============================================================================ -export function buildFindManyDocument( +export function buildFindManyDocument( operationName: string, queryField: string, select: TSelect, args: { where?: TWhere; + condition?: TCondition; orderBy?: string[]; first?: number; last?: number; @@ -217,6 +218,7 @@ export function buildFindManyDocument( filterTypeName: string, orderByTypeName: string, connectionFieldsMap?: Record>, + conditionTypeName?: string, ): { document: string; variables: Record } { const selections = select ? buildSelections( @@ -230,6 +232,16 @@ export function buildFindManyDocument( const queryArgs: ArgumentNode[] = []; const variables: Record = {}; + addVariable( + { + varName: 'condition', + typeName: conditionTypeName, + value: args.condition, + }, + variableDefinitions, + queryArgs, + variables, + ); addVariable( { varName: 'where', @@ -308,13 +320,14 @@ export function buildFindManyDocument( return { document: print(document), variables }; } -export function buildFindFirstDocument( +export function buildFindFirstDocument( operationName: string, queryField: string, select: TSelect, - args: { where?: TWhere }, + args: { where?: TWhere; condition?: TCondition }, filterTypeName: string, connectionFieldsMap?: Record>, + conditionTypeName?: string, ): { document: string; variables: Record } { const selections = select ? buildSelections( @@ -335,6 +348,16 @@ export function buildFindFirstDocument( queryArgs, variables, ); + addVariable( + { + varName: 'condition', + typeName: conditionTypeName, + value: args.condition, + }, + variableDefinitions, + queryArgs, + variables, + ); addVariable( { varName: 'where', @@ -799,7 +822,7 @@ function buildConnectionSelections(nodeSelections: FieldNode[]): FieldNode[] { interface VariableSpec { varName: string; argName?: string; - typeName: string; + typeName?: string; value: unknown; } @@ -850,7 +873,7 @@ function addVariable( args: ArgumentNode[], variables: Record, ): void { - if (spec.value === undefined) return; + if (spec.value === undefined || !spec.typeName) return; definitions.push( t.variableDefinition({ diff --git a/graphql/codegen/src/core/codegen/templates/select-types.ts b/graphql/codegen/src/core/codegen/templates/select-types.ts index 45a64a079..c27939da0 100644 --- a/graphql/codegen/src/core/codegen/templates/select-types.ts +++ b/graphql/codegen/src/core/codegen/templates/select-types.ts @@ -21,9 +21,10 @@ export interface PageInfo { endCursor?: string | null; } -export interface FindManyArgs { +export interface FindManyArgs { select?: TSelect; where?: TWhere; + condition?: TCondition; orderBy?: TOrderBy[]; first?: number; last?: number; @@ -32,9 +33,10 @@ export interface FindManyArgs { offset?: number; } -export interface FindFirstArgs { +export interface FindFirstArgs { select?: TSelect; where?: TWhere; + condition?: TCondition; } export interface CreateArgs { From c9b992e5316e196cdc5112e7f16c925274a13124 Mon Sep 17 00:00:00 2001 From: Dan Lynch Date: Thu, 12 Mar 2026 04:12:51 +0000 Subject: [PATCH 5/5] docs: add comments explaining why VectorFilter exists for pgvector columns VectorFilter provides equality/distinct operators for vector columns on Filter types. While similarity search is done via condition types (embeddingNearby), connection-filter may still auto-generate a filter for vector columns. Without this mapping, those fields would be silently omitted from the generated SDK. --- .../codegen/src/core/codegen/orm/input-types-generator.ts | 5 ++++- graphql/codegen/src/core/codegen/scalars.ts | 7 ++++++- 2 files changed, 10 insertions(+), 2 deletions(-) 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 31253bbf2..4a765433b 100644 --- a/graphql/codegen/src/core/codegen/orm/input-types-generator.ts +++ b/graphql/codegen/src/core/codegen/orm/input-types-generator.ts @@ -322,7 +322,10 @@ const SCALAR_FILTER_CONFIGS: ScalarFilterConfig[] = [ operators: ['equality', 'distinct', 'inArray', 'comparison', 'inet'], }, { name: 'FullTextFilter', tsType: 'string', operators: ['fulltext'] }, - // Vector filter (for pgvector embedding fields) + // VectorFilter: equality/distinct operators for vector columns on Filter types. + // Similarity search uses condition types (embeddingNearby), not filters, but + // connection-filter may still generate a filter for vector columns. This ensures + // the generated type uses number[] rather than being silently omitted. { name: 'VectorFilter', tsType: 'number[]', operators: ['equality', 'distinct'] }, // List filters (for array fields like string[], int[], uuid[]) { diff --git a/graphql/codegen/src/core/codegen/scalars.ts b/graphql/codegen/src/core/codegen/scalars.ts index a5e5914e1..a49d33170 100644 --- a/graphql/codegen/src/core/codegen/scalars.ts +++ b/graphql/codegen/src/core/codegen/scalars.ts @@ -39,7 +39,7 @@ export const SCALAR_TS_MAP: Record = { TsVector: 'string', TsQuery: 'string', - // Vector types (pgvector) + // Vector types (pgvector) — serialized as [Float] in GraphQL Vector: 'number[]', // File upload @@ -61,6 +61,11 @@ export const SCALAR_FILTER_MAP: Record = { BigFloat: 'BigFloatFilter', BitString: 'BitStringFilter', InternetAddress: 'InternetAddressFilter', + // VectorFilter provides equality/distinct operators (isNull, equalTo, etc.) for vector + // columns on Filter types. While similarity search is done via condition types + // (e.g., embeddingNearby on ContactCondition), postgraphile-plugin-connection-filter + // may still auto-generate a filter type for vector columns. Without this mapping, + // those fields would be silently omitted from the generated SDK. Vector: 'VectorFilter', FullText: 'FullTextFilter', Interval: 'StringFilter',