Skip to content
  •  
  •  
  •  
4 changes: 2 additions & 2 deletions graphql/codegen/SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -326,8 +326,8 @@ export class UserModel {
}

findFirst<const S extends UserSelect>(
args?: FindFirstArgs<DeepExact<S, UserSelect>, UserFilter>
): QueryBuilder<{ users: { nodes: InferSelectResult<UserWithRelations, S>[] } }> {
args?: FindFirstArgs<DeepExact<S, UserSelect>, UserFilter, UsersOrderBy>
): QueryBuilder<{ user: InferSelectResult<UserWithRelations, S> | null }> {
// ...
}

Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -287,9 +287,10 @@ export interface FindManyArgs<TSelect, TWhere, TOrderBy = never> {
offset?: number;
}

export interface FindFirstArgs<TSelect, TWhere> {
export interface FindFirstArgs<TSelect, TWhere, TOrderBy = never> {
select?: TSelect;
where?: TWhere;
orderBy?: TOrderBy[];
}

export interface CreateArgs<TSelect, TData> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,26 +39,32 @@ export class UserModel {
variables
});
}
findFirst<S extends UserSelect>(args: FindFirstArgs<S, UserFilter> & {
findFirst<S extends UserSelect>(args: FindFirstArgs<S, UserFilter, UsersOrderBy> & {
select: S;
} & StrictSelect<S, UserSelect>): QueryBuilder<{
users: {
nodes: InferSelectResult<UserWithRelations, S>[];
};
user: InferSelectResult<UserWithRelations, S> | null;
}> {
const {
document,
variables
} = buildFindFirstDocument("User", "users", args.select, {
where: args?.where
}, "UserFilter", connectionFieldsMap);
where: args?.where,
orderBy: args?.orderBy as string[] | undefined
}, "UserFilter", "UsersOrderBy", connectionFieldsMap);
return new QueryBuilder({
client: this.client,
operation: "query",
operationName: "User",
fieldName: "users",
fieldName: "user",
document,
variables
variables,
transform: (data: {
users?: {
nodes?: InferSelectResult<UserWithRelations, S>[];
};
}) => ({
"user": data.users?.nodes?.[0] ?? null
})
});
}
findOne<S extends UserSelect>(args: {
Expand Down Expand Up @@ -188,26 +194,32 @@ export class AuditLogModel {
variables
});
}
findFirst<S extends AuditLogSelect>(args: FindFirstArgs<S, AuditLogFilter> & {
findFirst<S extends AuditLogSelect>(args: FindFirstArgs<S, AuditLogFilter, AuditLogsOrderBy> & {
select: S;
} & StrictSelect<S, AuditLogSelect>): QueryBuilder<{
auditLogs: {
nodes: InferSelectResult<AuditLogWithRelations, S>[];
};
auditLog: InferSelectResult<AuditLogWithRelations, S> | null;
}> {
const {
document,
variables
} = buildFindFirstDocument("AuditLog", "auditLogs", args.select, {
where: args?.where
}, "AuditLogFilter", connectionFieldsMap);
where: args?.where,
orderBy: args?.orderBy as string[] | undefined
}, "AuditLogFilter", "AuditLogsOrderBy", connectionFieldsMap);
return new QueryBuilder({
client: this.client,
operation: "query",
operationName: "AuditLog",
fieldName: "auditLogs",
fieldName: "auditLog",
document,
variables
variables,
transform: (data: {
auditLogs?: {
nodes?: InferSelectResult<AuditLogWithRelations, S>[];
};
}) => ({
"auditLog": data.auditLogs?.nodes?.[0] ?? null
})
});
}
findOne<S extends AuditLogSelect>(args: {
Expand Down Expand Up @@ -291,33 +303,39 @@ export class OrganizationModel {
variables
});
}
findFirst<S extends OrganizationSelect>(args: FindFirstArgs<S, OrganizationFilter> & {
findFirst<S extends OrganizationSelect>(args: FindFirstArgs<S, OrganizationFilter, OrganizationsOrderBy> & {
select: S;
} & StrictSelect<S, OrganizationSelect>): QueryBuilder<{
allOrganizations: {
nodes: InferSelectResult<OrganizationWithRelations, S>[];
};
organization: InferSelectResult<OrganizationWithRelations, S> | null;
}> {
const {
document,
variables
} = buildFindFirstDocument("Organization", "allOrganizations", args.select, {
where: args?.where
}, "OrganizationFilter", connectionFieldsMap);
where: args?.where,
orderBy: args?.orderBy as string[] | undefined
}, "OrganizationFilter", "OrganizationsOrderBy", connectionFieldsMap);
return new QueryBuilder({
client: this.client,
operation: "query",
operationName: "Organization",
fieldName: "allOrganizations",
document,
variables
fieldName: "organization",
document,
variables,
transform: (data: {
allOrganizations?: {
nodes?: InferSelectResult<OrganizationWithRelations, S>[];
};
}) => ({
"organization": data.allOrganizations?.nodes?.[0] ?? null
})
});
}
findOne<S extends OrganizationSelect>(args: {
id: string;
select: S;
} & StrictSelect<S, OrganizationSelect>): QueryBuilder<{
organizationById: InferSelectResult<OrganizationWithRelations, S> | null;
organization: InferSelectResult<OrganizationWithRelations, S> | null;
}> {
const {
document,
Expand All @@ -327,7 +345,7 @@ export class OrganizationModel {
client: this.client,
operation: "query",
operationName: "Organization",
fieldName: "organizationById",
fieldName: "organization",
document,
variables
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -520,7 +520,7 @@ function buildFindManyArgsType(table: Table): t.TSType {

/**
* Build the FindFirstArgs type instantiation for a table:
* FindFirstArgs<SelectType, FilterType> & { select: SelectType }
* FindFirstArgs<SelectType, FilterType, OrderByType> & { select: SelectType }
*
* The intersection with { select: SelectType } makes select required,
* matching what the ORM's findFirst method expects.
Expand All @@ -529,11 +529,13 @@ function buildFindFirstArgsType(table: Table): t.TSType {
const { typeName } = getTableNames(table);
const selectTypeName = `${typeName}Select`;
const whereTypeName = getFilterTypeName(table);
const orderByTypeName = getOrderByTypeName(table);
const findFirstType = t.tsTypeReference(
t.identifier('FindFirstArgs'),
t.tsTypeParameterInstantiation([
t.tsTypeReference(t.identifier(selectTypeName)),
t.tsTypeReference(t.identifier(whereTypeName)),
t.tsTypeReference(t.identifier(orderByTypeName)),
]),
);
// Intersect with { select: SelectType } to make select required
Expand Down Expand Up @@ -1483,6 +1485,7 @@ export function generateTableCommand(table: Table, options?: TableCommandOptions
' --select <fields> Comma-separated list of fields to return',
' --where.<field>.<op> Filter (dot-notation, e.g. --where.status.equalTo active)',
' --condition.<f>.<op> Condition filter (dot-notation)',
' --orderBy <values> Comma-separated ordering values (e.g. NAME_ASC,CREATED_AT_DESC)',
'',
);
if (hasSearchFields) {
Expand Down
94 changes: 76 additions & 18 deletions graphql/codegen/src/core/codegen/orm/model-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import {
getGeneratedFileHeader,
getOrderByTypeName,
getPrimaryKeyInfo,
getSingleRowQueryName,
getTableNames,
hasValidPrimaryKey,
lcFirst,
Expand Down Expand Up @@ -193,7 +192,10 @@ export function generateModelFile(
const pkField = pkFields[0];
const pluralQueryName = table.query?.all ?? pluralName;
const singleQueryName = table.query?.one;
const singleResultFieldName = getSingleRowQueryName(table);
// The unwrapped result key for findFirst/findOne — must be the friendly
// singular noun (e.g. "animal"), NOT the GraphQL by-id query name (e.g.
// "animalById"), so the surface aligns with the rest of the SDK.
const singleResultFieldName = singularName;
const createMutationName = table.query?.create ?? `create${typeName}`;
const updateMutationName = table.query?.update;
const deleteMutationName = table.query?.delete;
Expand Down Expand Up @@ -440,6 +442,7 @@ export function generateModelFile(
const findFirstTypeArgs: Array<(sel: t.TSType) => t.TSType> = [
(sel: t.TSType) => sel,
() => t.tsTypeReference(t.identifier(whereTypeName)),
() => t.tsTypeReference(t.identifier(orderByTypeName)),
];
const argsType = (sel: t.TSType) =>
t.tsTypeReference(
Expand All @@ -455,23 +458,17 @@ export function generateModelFile(
t.tsTypeParameterInstantiation([
t.tsTypeLiteral([
t.tsPropertySignature(
t.identifier(pluralQueryName),
t.identifier(singleResultFieldName),
t.tsTypeAnnotation(
t.tsTypeLiteral([
t.tsPropertySignature(
t.identifier('nodes'),
t.tsTypeAnnotation(
t.tsArrayType(
t.tsTypeReference(
t.identifier('InferSelectResult'),
t.tsTypeParameterInstantiation([
t.tsTypeReference(t.identifier(relationTypeName)),
sel,
]),
),
),
),
t.tsUnionType([
t.tsTypeReference(
t.identifier('InferSelectResult'),
t.tsTypeParameterInstantiation([
t.tsTypeReference(t.identifier(relationTypeName)),
sel,
]),
),
t.tsNullKeyword(),
]),
),
),
Expand Down Expand Up @@ -502,15 +499,75 @@ export function generateModelFile(
true,
),
),
t.objectProperty(
t.identifier('orderBy'),
t.tsAsExpression(
t.optionalMemberExpression(
t.identifier('args'),
t.identifier('orderBy'),
false,
true,
),
t.tsUnionType([
t.tsArrayType(t.tsStringKeyword()),
t.tsUndefinedKeyword(),
]),
),
),
];
const bodyArgs = [
t.stringLiteral(typeName),
t.stringLiteral(pluralQueryName),
selectExpr,
t.objectExpression(findFirstObjProps),
t.stringLiteral(whereTypeName),
t.stringLiteral(orderByTypeName),
t.identifier('connectionFieldsMap'),
];
const transformDataParam = t.identifier('data');
const transformedNodesProp = t.tsPropertySignature(
t.identifier('nodes'),
t.tsTypeAnnotation(
t.tsArrayType(
t.tsTypeReference(
t.identifier('InferSelectResult'),
t.tsTypeParameterInstantiation([
t.tsTypeReference(t.identifier(relationTypeName)),
sRef(),
]),
),
),
),
);
transformedNodesProp.optional = true;
const transformedCollectionProp = t.tsPropertySignature(
t.identifier(pluralQueryName),
t.tsTypeAnnotation(t.tsTypeLiteral([transformedNodesProp])),
);
transformedCollectionProp.optional = true;
transformDataParam.typeAnnotation = t.tsTypeAnnotation(
t.tsTypeLiteral([transformedCollectionProp]),
);
const firstNodeExpr = t.optionalMemberExpression(
t.optionalMemberExpression(
t.memberExpression(t.identifier('data'), t.identifier(pluralQueryName)),
t.identifier('nodes'),
false,
true,
),
t.numericLiteral(0),
true,
true,
);
const transformFn = t.arrowFunctionExpression(
[transformDataParam],
t.objectExpression([
t.objectProperty(
t.stringLiteral(singleResultFieldName),
t.logicalExpression('??', firstNodeExpr, t.nullLiteral()),
),
]),
);
classBody.push(
createClassMethod(
'findFirst',
Expand All @@ -522,7 +579,8 @@ export function generateModelFile(
bodyArgs,
'query',
typeName,
pluralQueryName,
singleResultFieldName,
[t.objectProperty(t.identifier('transform'), transformFn)],
),
),
);
Expand Down
3 changes: 2 additions & 1 deletion graphql/codegen/src/core/codegen/orm/select-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,9 +215,10 @@ export interface FindManyArgs<TSelect, TWhere, TOrderBy> {
/**
* Arguments for findFirst/findUnique operations
*/
export interface FindFirstArgs<TSelect, TWhere> {
export interface FindFirstArgs<TSelect, TWhere, TOrderBy = never> {
select?: TSelect;
where?: TWhere;
orderBy?: TOrderBy[];
}

/**
Expand Down
6 changes: 4 additions & 2 deletions graphql/codegen/src/core/codegen/templates/cli-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -283,8 +283,8 @@ export function parseFindManyArgs<T = Record<string, unknown>>(

/**
* Build findFirst args from CLI argv.
* Like parseFindManyArgs but only includes select and where
* (no pagination flags — findFirst returns the first matching record).
* Like parseFindManyArgs but without pagination flags (no limit/offset/after/before/last)
* — findFirst returns the first matching record. Supports select, where, and orderBy.
*/
export function parseFindFirstArgs<T = Record<string, unknown>>(
argv: Record<string, unknown>,
Expand All @@ -293,10 +293,12 @@ export function parseFindFirstArgs<T = Record<string, unknown>>(
const select = parseSelectFlag(argv, defaultSelect);
const parsed = unflattenDotNotation(argv);
const where = parsed.where;
const orderBy = parseOrderByFlag(argv);

return {
select,
...(where !== undefined ? { where } : {}),
...(orderBy !== undefined ? { orderBy } : {}),
} as unknown as T;
}

Expand Down
Loading
Loading