Files
Sebastian Poxhofer a31f9cf0f9 refactor(schemas): prefer Nullish schema utils instead of z.nullish() (#44565)
* ci(linter): add renovate/prefer-zod-nullish rule

Add a custom oxlint rule that flags zod chains calling .optional() and
.nullable() adjacently (in either order) and suggests .nullish(), which
is equivalent (both accept undefined | null in zod v4).

Fix all 26 pre-existing violations across 6 schema files.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* ci(linter): prefer schema-utils Nullish over zod .nullish()

Rename prefer-zod-nullish to prefer-nullish-util and extend it to flag
.nullish() itself, including the standalone forms z.nullish(x) and
z.optional(z.nullable(x)). Migrate all call sites to Nullish() and
DeepNullish() from schema-utils, which normalize null to undefined in
parsed output. Schemas with more than one nullish position are wrapped
in DeepNullish(); nullish object properties with nullish members use
Nullish(DeepNullish(...)) to keep the optional property type marker.

Spec files are exempt (result.spec.ts intentionally tests
null-preserving parses). Spec assertions expecting null in parsed
output were updated to undefined. Note: docker manifests with an
explicit "mediaType": null are now accepted and treated like an absent
mediaType instead of failing the discriminated union.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 10:15:55 +00:00

124 lines
2.9 KiB
TypeScript

import { z } from 'zod/v4';
import { DeepNullish, EmailAddress } from '../../../util/schema-utils/index.ts';
export const User = z.object({
mail: EmailAddress,
displayName: z.string(),
name: z.string(),
});
export type User = z.infer<typeof User>;
export const DefaultBranch = z.object({
defaultBranch: z.string(),
});
export const Link = DeepNullish(
z.object({
href: z.string(),
name: z.string().optional(),
templated: z.boolean().optional(),
}),
);
export type Link = z.infer<typeof Link>;
export const Links = z.record(z.string(), z.union([Link, z.array(Link)]));
export type Links = z.infer<typeof Links>;
export const PrState = z.enum(['DRAFT', 'OPEN', 'REJECTED', 'MERGED']);
export type PrState = z.infer<typeof PrState>;
export const PrMergeMethod = z.enum([
'MERGE_COMMIT',
'REBASE',
'FAST_FORWARD_IF_POSSIBLE',
'FAST_FORWARD_ONLY',
'SQUASH',
]);
export type PrMergeMethod = z.infer<typeof PrMergeMethod>;
export const PullRequest = DeepNullish(
z.object({
id: z.string(),
author: z
.object({
mail: z.string().optional(),
displayName: z.string(),
id: z.string(),
})
.optional(),
reviser: z
.object({
id: z.string().optional(),
displayName: z.string().optional(),
})
.optional(),
closeDate: z.string().optional(),
source: z.string(),
target: z.string(),
title: z.string(),
description: z.string().optional(),
creationDate: z.string(),
lastModified: z.string().optional(),
status: PrState,
reviewer: z
.array(
z.object({
id: z.string(),
displayName: z.string(),
mail: z.string().optional(),
approved: z.boolean(),
}),
)
.optional(),
labels: z.string().array(),
tasks: z.object({
todo: z.number(),
done: z.number(),
}),
_links: Links,
_embedded: z.object({
defaultConfig: z.object({
mergeStrategy: PrMergeMethod,
deleteBranchOnMerge: z.boolean(),
}),
}),
}),
);
export type PullRequest = z.infer<typeof PullRequest>;
const RepoType = z.enum(['git', 'svn', 'hg']);
export const Repo = DeepNullish(
z.object({
contact: z.string().optional(),
creationDate: z.string().optional(),
description: z.string().optional(),
lastModified: z.string().optional(),
namespace: z.string(),
name: z.string(),
type: RepoType,
archived: z.boolean(),
exporting: z.boolean(),
healthCheckRunning: z.boolean(),
_links: Links,
}),
);
export type Repo = z.infer<typeof Repo>;
const Paged = z.object({
page: z.number(),
pageTotal: z.number(),
});
export const PagedPullRequest = Paged.extend({
_embedded: z.object({
pullRequests: z.array(PullRequest),
}),
});
export const PagedRepo = Paged.extend({
_embedded: z.object({
repositories: z.array(Repo),
}),
});