Skip to content

Commit 00e2410

Browse files
committed
fix(codegen): merge condition into where filter instead of separate GraphQL argument
PostGraphile's PgConditionArgumentPlugin is disabled in ConstructivePreset, so *Condition input types don't exist in the schema. The ORM codegen was generating a separate $condition variable that referenced these non-existent types, causing 'cannot be used as an input type' errors. This converts condition values (simple equality matching) into the connection-filter format used by the 'where' argument: condition: { title: 'Financial', status: null } becomes: where: { title: { equalTo: 'Financial' }, status: { isNull: true } } If both condition and where are provided, they are merged together.
1 parent 3bf7c52 commit 00e2410

2 files changed

Lines changed: 151 additions & 42 deletions

File tree

graphql/codegen/src/__tests__/codegen/query-builder.test.ts

Lines changed: 91 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,35 @@ function buildSelections(
123123
return fields;
124124
}
125125

126+
function conditionToFilter(
127+
condition: Record<string, unknown>,
128+
): Record<string, unknown> {
129+
const filter: Record<string, unknown> = {};
130+
for (const [key, value] of Object.entries(condition)) {
131+
if (value === undefined) continue;
132+
if (value === null) {
133+
filter[key] = { isNull: true };
134+
} else {
135+
filter[key] = { equalTo: value };
136+
}
137+
}
138+
return filter;
139+
}
140+
141+
function mergeConditionIntoWhere(
142+
where: Record<string, unknown> | undefined,
143+
condition: Record<string, unknown> | undefined,
144+
): Record<string, unknown> | undefined {
145+
if (!condition || Object.keys(condition).length === 0) {
146+
return where;
147+
}
148+
const converted = conditionToFilter(condition);
149+
if (!where || Object.keys(where).length === 0) {
150+
return converted;
151+
}
152+
return { ...where, ...converted };
153+
}
154+
126155
function buildFindManyDocument<TSelect, TWhere, TCondition>(
127156
operationName: string,
128157
queryField: string,
@@ -144,21 +173,15 @@ function buildFindManyDocument<TSelect, TWhere, TCondition>(
144173
const queryArgs: ArgumentNode[] = [];
145174
const variables: Record<string, unknown> = {};
146175

147-
addVariable(
148-
{
149-
varName: 'condition',
150-
typeName: conditionTypeName,
151-
value: args.condition,
152-
},
153-
variableDefinitions,
154-
queryArgs,
155-
variables,
176+
const mergedWhere = mergeConditionIntoWhere(
177+
args.where as Record<string, unknown> | undefined,
178+
args.condition as Record<string, unknown> | undefined,
156179
);
157180
addVariable(
158181
{
159182
varName: 'where',
160183
typeName: filterTypeName,
161-
value: args.where,
184+
value: mergedWhere,
162185
},
163186
variableDefinitions,
164187
queryArgs,
@@ -568,14 +591,41 @@ describe('query-builder', () => {
568591
});
569592
});
570593

571-
it('includes condition variable when conditionTypeName is provided', () => {
594+
it('merges condition into where as equality filters', () => {
595+
const { document, variables } = buildFindManyDocument(
596+
'Documents',
597+
'documents',
598+
{ id: true, title: true },
599+
{
600+
condition: { title: 'Financial', category: 'legal' },
601+
first: 5,
602+
},
603+
'DocumentFilter',
604+
'DocumentsOrderBy',
605+
undefined,
606+
'DocumentCondition',
607+
);
608+
609+
// condition should NOT appear as a separate variable
610+
expect(document).not.toContain('$condition');
611+
expect(document).not.toContain('DocumentCondition');
612+
// should be merged into where
613+
expect(document).toContain('$where: DocumentFilter');
614+
expect(document).toContain('where: $where');
615+
expect(variables.where).toEqual({
616+
title: { equalTo: 'Financial' },
617+
category: { equalTo: 'legal' },
618+
});
619+
});
620+
621+
it('merges condition with existing where filters', () => {
572622
const { document, variables } = buildFindManyDocument(
573623
'Contacts',
574624
'contacts',
575625
{ id: true, name: true },
576626
{
577-
condition: { embeddingNearby: { vector: [0.1, 0.2], metric: 'COSINE' } },
578-
where: { name: { equalTo: 'test' } },
627+
condition: { status: 'active' },
628+
where: { name: { startsWith: 'Acme' } },
579629
first: 5,
580630
},
581631
'ContactFilter',
@@ -584,17 +634,34 @@ describe('query-builder', () => {
584634
'ContactCondition',
585635
);
586636

587-
// condition variable should appear in the query
588-
expect(document).toContain('$condition: ContactCondition');
589-
expect(document).toContain('condition: $condition');
590-
// where should still work alongside condition
637+
// condition should NOT appear as a separate variable
638+
expect(document).not.toContain('$condition');
639+
expect(document).not.toContain('ContactCondition');
640+
// merged where should contain both
591641
expect(document).toContain('$where: ContactFilter');
592-
expect(document).toContain('where: $where');
593-
// variables should include both
594-
expect(variables.condition).toEqual({
595-
embeddingNearby: { vector: [0.1, 0.2], metric: 'COSINE' },
642+
expect(variables.where).toEqual({
643+
name: { startsWith: 'Acme' },
644+
status: { equalTo: 'active' },
645+
});
646+
});
647+
648+
it('converts null condition values to isNull filters', () => {
649+
const { variables } = buildFindManyDocument(
650+
'Users',
651+
'users',
652+
{ id: true },
653+
{
654+
condition: { deletedAt: null },
655+
},
656+
'UserFilter',
657+
'UsersOrderBy',
658+
undefined,
659+
'UserCondition',
660+
);
661+
662+
expect(variables.where).toEqual({
663+
deletedAt: { isNull: true },
596664
});
597-
expect(variables.where).toEqual({ name: { equalTo: 'test' } });
598665
});
599666

600667
it('omits condition variable when not provided', () => {
@@ -612,6 +679,8 @@ describe('query-builder', () => {
612679
// condition should NOT appear since no value was provided
613680
expect(document).not.toContain('$condition');
614681
expect(document).not.toContain('condition:');
682+
// where should also not appear since no where was provided
683+
expect(document).not.toContain('$where');
615684
});
616685
});
617686

graphql/codegen/src/core/codegen/templates/query-builder.ts

Lines changed: 60 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -232,21 +232,19 @@ export function buildFindManyDocument<TSelect, TWhere, TCondition = never>(
232232
const queryArgs: ArgumentNode[] = [];
233233
const variables: Record<string, unknown> = {};
234234

235-
addVariable(
236-
{
237-
varName: 'condition',
238-
typeName: conditionTypeName,
239-
value: args.condition,
240-
},
241-
variableDefinitions,
242-
queryArgs,
243-
variables,
235+
// Merge condition values into where as equality filters.
236+
// This avoids depending on PgConditionArgumentPlugin (which generates
237+
// *Condition input types) and routes everything through the
238+
// always-available connection-filter `where` argument instead.
239+
const mergedWhere = mergeConditionIntoWhere(
240+
args.where as Record<string, unknown> | undefined,
241+
args.condition as Record<string, unknown> | undefined,
244242
);
245243
addVariable(
246244
{
247245
varName: 'where',
248246
typeName: filterTypeName,
249-
value: args.where,
247+
value: mergedWhere,
250248
},
251249
variableDefinitions,
252250
queryArgs,
@@ -347,21 +345,16 @@ export function buildFindFirstDocument<TSelect, TWhere, TCondition = never>(
347345
queryArgs,
348346
variables,
349347
);
350-
addVariable(
351-
{
352-
varName: 'condition',
353-
typeName: conditionTypeName,
354-
value: args.condition,
355-
},
356-
variableDefinitions,
357-
queryArgs,
358-
variables,
348+
// Merge condition values into where as equality filters
349+
const mergedWhere = mergeConditionIntoWhere(
350+
args.where as Record<string, unknown> | undefined,
351+
args.condition as Record<string, unknown> | undefined,
359352
);
360353
addVariable(
361354
{
362355
varName: 'where',
363356
typeName: filterTypeName,
364-
value: args.where,
357+
value: mergedWhere,
365358
},
366359
variableDefinitions,
367360
queryArgs,
@@ -840,6 +833,53 @@ interface VariableSpec {
840833
value: unknown;
841834
}
842835

836+
/**
837+
* Convert condition (exact equality) values to connection-filter format.
838+
*
839+
* The ORM exposes a simple `condition` API for equality matching:
840+
* condition: { title: "Financial", status: null }
841+
*
842+
* This converts each field to the filter operator format used by
843+
* graphile-connection-filter's `where` argument:
844+
* { title: { equalTo: "Financial" }, status: { isNull: true } }
845+
*
846+
* This allows `condition` to work regardless of whether PostGraphile's
847+
* PgConditionArgumentPlugin is enabled, since the values are routed
848+
* through the always-available `where` (filter) argument instead.
849+
*/
850+
function conditionToFilter(
851+
condition: Record<string, unknown>,
852+
): Record<string, unknown> {
853+
const filter: Record<string, unknown> = {};
854+
for (const [key, value] of Object.entries(condition)) {
855+
if (value === undefined) continue;
856+
if (value === null) {
857+
filter[key] = { isNull: true };
858+
} else {
859+
filter[key] = { equalTo: value };
860+
}
861+
}
862+
return filter;
863+
}
864+
865+
/**
866+
* Merge condition values (converted to filter equality format) into
867+
* the existing where/filter object.
868+
*/
869+
function mergeConditionIntoWhere(
870+
where: Record<string, unknown> | undefined,
871+
condition: Record<string, unknown> | undefined,
872+
): Record<string, unknown> | undefined {
873+
if (!condition || Object.keys(condition).length === 0) {
874+
return where;
875+
}
876+
const converted = conditionToFilter(condition);
877+
if (!where || Object.keys(where).length === 0) {
878+
return converted;
879+
}
880+
return { ...where, ...converted };
881+
}
882+
843883
interface InputMutationConfig {
844884
operationName: string;
845885
mutationField: string;

0 commit comments

Comments
 (0)