From 1588296da227fe56633bbe5b4165858544615753 Mon Sep 17 00:00:00 2001 From: Dan Lynch Date: Thu, 12 Mar 2026 03:12:22 +0000 Subject: [PATCH] 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 {