Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -757,7 +757,7 @@ async function handleList(argv: Partial<Record<string, unknown>>, _prompter: Inq
isElectric: true,
createdAt: true
};
const findManyArgs = parseFindManyArgs<FindManyArgs<CarSelect, CarFilter, never, CarsOrderBy> & {
const findManyArgs = parseFindManyArgs<FindManyArgs<CarSelect, CarFilter, CarsOrderBy> & {
select: CarSelect;
}>(argv, defaultSelect);
const client = getClient();
Expand All @@ -781,7 +781,7 @@ async function handleFindFirst(argv: Partial<Record<string, unknown>>, _prompter
isElectric: true,
createdAt: true
};
const findFirstArgs = parseFindFirstArgs<FindFirstArgs<CarSelect, CarFilter, never> & {
const findFirstArgs = parseFindFirstArgs<FindFirstArgs<CarSelect, CarFilter> & {
select: CarSelect;
}>(argv, defaultSelect);
const client = getClient();
Expand Down Expand Up @@ -1219,7 +1219,7 @@ async function handleList(argv: Partial<Record<string, unknown>>, _prompter: Inq
name: true,
licenseNumber: true
};
const findManyArgs = parseFindManyArgs<FindManyArgs<DriverSelect, DriverFilter, never, DriversOrderBy> & {
const findManyArgs = parseFindManyArgs<FindManyArgs<DriverSelect, DriverFilter, DriversOrderBy> & {
select: DriverSelect;
}>(argv, defaultSelect);
const client = getClient();
Expand All @@ -1240,7 +1240,7 @@ async function handleFindFirst(argv: Partial<Record<string, unknown>>, _prompter
name: true,
licenseNumber: true
};
const findFirstArgs = parseFindFirstArgs<FindFirstArgs<DriverSelect, DriverFilter, never> & {
const findFirstArgs = parseFindFirstArgs<FindFirstArgs<DriverSelect, DriverFilter> & {
select: DriverSelect;
}>(argv, defaultSelect);
const client = getClient();
Expand Down Expand Up @@ -3255,7 +3255,7 @@ async function handleList(argv: Partial<Record<string, unknown>>, _prompter: Inq
email: true,
name: true
};
const findManyArgs = parseFindManyArgs<FindManyArgs<UserSelect, UserFilter, never, UsersOrderBy> & {
const findManyArgs = parseFindManyArgs<FindManyArgs<UserSelect, UserFilter, UsersOrderBy> & {
select: UserSelect;
}>(argv, defaultSelect);
const client = getClient("auth");
Expand All @@ -3276,7 +3276,7 @@ async function handleFindFirst(argv: Partial<Record<string, unknown>>, _prompter
email: true,
name: true
};
const findFirstArgs = parseFindFirstArgs<FindFirstArgs<UserSelect, UserFilter, never> & {
const findFirstArgs = parseFindFirstArgs<FindFirstArgs<UserSelect, UserFilter> & {
select: UserSelect;
}>(argv, defaultSelect);
const client = getClient("auth");
Expand Down Expand Up @@ -3487,7 +3487,7 @@ async function handleList(argv: Partial<Record<string, unknown>>, _prompter: Inq
id: true,
role: true
};
const findManyArgs = parseFindManyArgs<FindManyArgs<MemberSelect, MemberFilter, never, MembersOrderBy> & {
const findManyArgs = parseFindManyArgs<FindManyArgs<MemberSelect, MemberFilter, MembersOrderBy> & {
select: MemberSelect;
}>(argv, defaultSelect);
const client = getClient("members");
Expand All @@ -3507,7 +3507,7 @@ async function handleFindFirst(argv: Partial<Record<string, unknown>>, _prompter
id: true,
role: true
};
const findFirstArgs = parseFindFirstArgs<FindFirstArgs<MemberSelect, MemberFilter, never> & {
const findFirstArgs = parseFindFirstArgs<FindFirstArgs<MemberSelect, MemberFilter> & {
select: MemberSelect;
}>(argv, defaultSelect);
const client = getClient("members");
Expand Down Expand Up @@ -3711,7 +3711,7 @@ async function handleList(argv: Partial<Record<string, unknown>>, _prompter: Inq
isElectric: true,
createdAt: true
};
const findManyArgs = parseFindManyArgs<FindManyArgs<CarSelect, CarFilter, never, CarsOrderBy> & {
const findManyArgs = parseFindManyArgs<FindManyArgs<CarSelect, CarFilter, CarsOrderBy> & {
select: CarSelect;
}>(argv, defaultSelect);
const client = getClient("app");
Expand All @@ -3735,7 +3735,7 @@ async function handleFindFirst(argv: Partial<Record<string, unknown>>, _prompter
isElectric: true,
createdAt: true
};
const findFirstArgs = parseFindFirstArgs<FindFirstArgs<CarSelect, CarFilter, never> & {
const findFirstArgs = parseFindFirstArgs<FindFirstArgs<CarSelect, CarFilter> & {
select: CarSelect;
}>(argv, defaultSelect);
const client = getClient("app");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -276,10 +276,9 @@ export interface PageInfo {
endCursor?: string | null;
}

export interface FindManyArgs<TSelect, TWhere, TCondition = never, TOrderBy = never> {
export interface FindManyArgs<TSelect, TWhere, TOrderBy = never> {
select?: TSelect;
where?: TWhere;
condition?: TCondition;
orderBy?: TOrderBy[];
first?: number;
last?: number;
Expand All @@ -288,10 +287,9 @@ export interface FindManyArgs<TSelect, TWhere, TCondition = never, TOrderBy = ne
offset?: number;
}

export interface FindFirstArgs<TSelect, TWhere, TCondition = never> {
export interface FindFirstArgs<TSelect, TWhere> {
select?: TSelect;
where?: TWhere;
condition?: TCondition;
}

export interface CreateArgs<TSelect, TData> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import type { User, UserWithRelations, UserSelect, UserFilter, UsersOrderBy, Cre
import { connectionFieldsMap } from "../input-types";
export class UserModel {
constructor(private client: OrmClient) {}
findMany<S extends UserSelect>(args: FindManyArgs<S, UserFilter, never, UsersOrderBy> & {
findMany<S extends UserSelect>(args: FindManyArgs<S, UserFilter, UsersOrderBy> & {
select: S;
} & StrictSelect<S, UserSelect>): QueryBuilder<{
users: ConnectionResult<InferSelectResult<UserWithRelations, S>>;
Expand Down Expand Up @@ -162,7 +162,7 @@ import type { AuditLog, AuditLogWithRelations, AuditLogSelect, AuditLogFilter, A
import { connectionFieldsMap } from "../input-types";
export class AuditLogModel {
constructor(private client: OrmClient) {}
findMany<S extends AuditLogSelect>(args: FindManyArgs<S, AuditLogFilter, never, AuditLogsOrderBy> & {
findMany<S extends AuditLogSelect>(args: FindManyArgs<S, AuditLogFilter, AuditLogsOrderBy> & {
select: S;
} & StrictSelect<S, AuditLogSelect>): QueryBuilder<{
auditLogs: ConnectionResult<InferSelectResult<AuditLogWithRelations, S>>;
Expand Down Expand Up @@ -265,7 +265,7 @@ import type { Organization, OrganizationWithRelations, OrganizationSelect, Organ
import { connectionFieldsMap } from "../input-types";
export class OrganizationModel {
constructor(private client: OrmClient) {}
findMany<S extends OrganizationSelect>(args: FindManyArgs<S, OrganizationFilter, never, OrganizationsOrderBy> & {
findMany<S extends OrganizationSelect>(args: FindManyArgs<S, OrganizationFilter, OrganizationsOrderBy> & {
select: S;
} & StrictSelect<S, OrganizationSelect>): QueryBuilder<{
allOrganizations: ConnectionResult<InferSelectResult<OrganizationWithRelations, S>>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1672,7 +1672,7 @@ import type { ListSelectionConfig } from "../selection";
import type { UserSelect, UserWithRelations, UserFilter, UsersOrderBy } from "../../orm/input-types";
import type { FindManyArgs, InferSelectResult, ConnectionResult, HookStrictSelect } from "../../orm/select-types";
export type { UserSelect, UserWithRelations, UserFilter, UsersOrderBy } from "../../orm/input-types";
export const usersQueryKey = (variables?: FindManyArgs<unknown, UserFilter, never, UsersOrderBy>) => ["user", "list", variables] as const;
export const usersQueryKey = (variables?: FindManyArgs<unknown, UserFilter, UsersOrderBy>) => ["user", "list", variables] as const;
/**
* Query hook for fetching User list
*
Expand Down
165 changes: 5 additions & 160 deletions graphql/codegen/src/__tests__/codegen/input-types-generator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -893,13 +893,7 @@ 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.
*/
describe('plugin-injected orderBy values', () => {
const contactTable = createTable({
name: 'Contact',
fields: [
Expand All @@ -920,151 +914,6 @@ describe('plugin-injected condition fields', () => {
},
});

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], undefined, true, { condition: true });

// 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], undefined, true, { condition: true });

// 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], undefined, true, { condition: true });

// 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: {
Expand Down Expand Up @@ -1100,15 +949,11 @@ describe('plugin-injected condition fields', () => {
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], undefined, true, { condition: true });
it('does not generate Condition types', () => {
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');
// Condition types should NOT be generated
expect(result.content).not.toContain('ContactCondition');
});
});

Expand Down
25 changes: 5 additions & 20 deletions graphql/codegen/src/__tests__/codegen/model-generator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ describe('model-generator', () => {
expect(result.content).toContain('ProductPatch');
});

it('imports and wires Condition type for findMany and findFirst', () => {
it('does not include Condition type in findMany or findFirst', () => {
const table = createTable({
name: 'Contact',
fields: [
Expand All @@ -249,25 +249,10 @@ describe('model-generator', () => {
},
});

const result = generateModelFile(table, false, { condition: true });

// Condition type should be imported
expect(result.content).toContain('ContactCondition');

// findMany should include condition in its args type
expect(result.content).toContain(
'FindManyArgs<S, ContactFilter, ContactCondition, ContactsOrderBy>',
);

// findFirst should include condition in its args type
expect(result.content).toContain(
'FindFirstArgs<S, ContactFilter, ContactCondition>',
);

// condition should be forwarded in the body args object
expect(result.content).toContain('condition: args?.condition');
const result = generateModelFile(table, false);

// conditionTypeName should be passed as a string literal to the document builder
expect(result.content).toContain('"ContactCondition"');
// Condition type should NOT be imported or referenced
expect(result.content).not.toContain('ContactCondition');
expect(result.content).not.toContain('condition');
});
});
Loading
Loading