refactor: ⬆️ Upgrade to Zod v4 and hono-openapi 0.5.0

This commit is contained in:
Jesse Wierzbinski 2025-07-07 03:42:35 +02:00
parent add2429606
commit 24d4150da4
No known key found for this signature in database
209 changed files with 1331 additions and 1622 deletions

View file

@ -1,4 +1,4 @@
import { z } from "zod";
import { z } from "zod/v4";
import { userAddressRegex } from "../regex.ts";
import { iso631, zBoolean } from "./common.ts";
import { CustomEmoji } from "./emoji.ts";
@ -10,7 +10,7 @@ export const Field = z
.string()
.trim()
.min(1)
.openapi({
.meta({
description: "The key of a given fields key-value pair.",
example: "Freak level",
externalDocs: {
@ -21,18 +21,17 @@ export const Field = z
.string()
.trim()
.min(1)
.openapi({
.meta({
description: "The value associated with the name key.",
example: "<p>High</p>",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Account/#value",
},
}),
verified_at: z
.string()
verified_at: z.iso
.datetime()
.nullable()
.openapi({
.meta({
description:
"Timestamp of when the server verified a URL value for a rel=“me” link.",
example: null,
@ -41,11 +40,11 @@ export const Field = z
},
}),
})
.openapi({ ref: "AccountField" });
.meta({ id: "AccountField" });
export const Source = z
.object({
privacy: z.enum(["public", "unlisted", "private", "direct"]).openapi({
privacy: z.enum(["public", "unlisted", "private", "direct"]).meta({
description:
"The default post privacy to be used for new statuses.",
example: "unlisted",
@ -53,7 +52,7 @@ export const Source = z
url: "https://docs.joinmastodon.org/entities/Account/#source-privacy",
},
}),
sensitive: zBoolean.openapi({
sensitive: zBoolean.meta({
description:
"Whether new statuses should be marked sensitive by default.",
example: false,
@ -61,7 +60,7 @@ export const Source = z
url: "https://docs.joinmastodon.org/entities/Account/#source-sensitive",
},
}),
language: iso631.openapi({
language: iso631.meta({
description: "The default posting language for new statuses.",
example: "en",
externalDocs: {
@ -73,7 +72,7 @@ export const Source = z
.int()
.nonnegative()
.optional()
.openapi({
.meta({
description: "The number of pending follow requests.",
example: 3,
externalDocs: {
@ -84,39 +83,35 @@ export const Source = z
.string()
.trim()
.min(0)
.openapi({
.meta({
description: "Profile bio, in plain-text instead of in HTML.",
example: "ermmm what the meow meow",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Account/#source-note",
},
}),
fields: z.array(Field).openapi({
fields: z.array(Field).meta({
description: "Metadata about the account.",
}),
})
.openapi({
.meta({
description:
"An extra attribute that contains source values to be used with API methods that verify credentials and update credentials.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Account/#source",
},
ref: "AccountSource",
id: "AccountSource",
});
// Because Account has some recursive references, we need to define it like this
const BaseAccount = z
export const Account = z
.object({
id: z
.string()
.uuid()
.openapi({
description: "The account ID in the database.",
example: "9e84842b-4db6-4a9b-969d-46ab408278da",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Account/#id",
},
}),
id: z.uuid().meta({
description: "The account ID in the database.",
example: "9e84842b-4db6-4a9b-969d-46ab408278da",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Account/#id",
},
}),
username: z
.string()
.min(3)
@ -125,7 +120,7 @@ const BaseAccount = z
/^[a-z0-9_-]+$/,
"Username can only contain letters, numbers, underscores and hyphens",
)
.openapi({
.meta({
description:
"The username of the account, not including domain.",
example: "lexi",
@ -138,7 +133,7 @@ const BaseAccount = z
.min(1)
.trim()
.regex(userAddressRegex, "Invalid user address")
.openapi({
.meta({
description:
"The Webfinger account URI. Equal to username for local users, or username@domain for remote users.",
example: "lexi@beta.versia.social",
@ -146,21 +141,18 @@ const BaseAccount = z
url: "https://docs.joinmastodon.org/entities/Account/#acct",
},
}),
url: z
.string()
.url()
.openapi({
description: "The location of the users profile page.",
example: "https://beta.versia.social/@lexi",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Account/#url",
},
}),
url: z.url().meta({
description: "The location of the users profile page.",
example: "https://beta.versia.social/@lexi",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Account/#url",
},
}),
display_name: z
.string()
.min(3)
.trim()
.openapi({
.meta({
description: "The profiles display name.",
example: "Lexi :flower:",
externalDocs: {
@ -171,29 +163,26 @@ const BaseAccount = z
.string()
.min(0)
.trim()
.openapi({
.meta({
description: "The profiles bio or description.",
example: "<p>ermmm what the meow meow</p>",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Account/#note",
},
}),
avatar: z
.string()
.url()
.openapi({
description:
"An image icon that is shown next to statuses and in the profile.",
example:
"https://cdn.versia.social/avatars/cff9aea0-0000-43fe-8b5e-e7c7ea69a488/lexi.webp",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Account/#avatar",
},
}),
avatar: z.url().meta({
description:
"An image icon that is shown next to statuses and in the profile.",
example:
"https://cdn.versia.social/avatars/cff9aea0-0000-43fe-8b5e-e7c7ea69a488/lexi.webp",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Account/#avatar",
},
}),
avatar_static: z
.string()
.url()
.openapi({
.meta({
description:
"A static version of the avatar. Equal to avatar if its value is a static image; different if avatar is an animated GIF.",
example:
@ -202,31 +191,25 @@ const BaseAccount = z
url: "https://docs.joinmastodon.org/entities/Account/#avatar_static",
},
}),
header: z
.string()
.url()
.openapi({
description:
"An image banner that is shown above the profile and in profile cards.",
example:
"https://cdn.versia.social/headers/a049f8e3-878c-4faa-ae4c-a6bcceddbd9d/femboy_2.webp",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Account/#header",
},
}),
header_static: z
.string()
.url()
.openapi({
description:
"A static version of the header. Equal to header if its value is a static image; different if header is an animated GIF.",
example:
"https://cdn.versia.social/headers/a049f8e3-878c-4faa-ae4c-a6bcceddbd9d/femboy_2.webp",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Account/#header_static",
},
}),
locked: zBoolean.openapi({
header: z.url().meta({
description:
"An image banner that is shown above the profile and in profile cards.",
example:
"https://cdn.versia.social/headers/a049f8e3-878c-4faa-ae4c-a6bcceddbd9d/femboy_2.webp",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Account/#header",
},
}),
header_static: z.url().meta({
description:
"A static version of the header. Equal to header if its value is a static image; different if header is an animated GIF.",
example:
"https://cdn.versia.social/headers/a049f8e3-878c-4faa-ae4c-a6bcceddbd9d/femboy_2.webp",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Account/#header_static",
},
}),
locked: zBoolean.meta({
description:
"Whether the account manually approves follow requests.",
example: false,
@ -234,21 +217,21 @@ const BaseAccount = z
url: "https://docs.joinmastodon.org/entities/Account/#locked",
},
}),
fields: z.array(Field).openapi({
fields: z.array(Field).meta({
description:
"Additional metadata attached to a profile as name-value pairs.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Account/#fields",
},
}),
emojis: z.array(CustomEmoji).openapi({
emojis: z.array(CustomEmoji).meta({
description:
"Custom emoji entities to be used when rendering the profile.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Account/#emojis",
},
}),
bot: zBoolean.openapi({
bot: zBoolean.meta({
description:
"Indicates that the account may perform automated actions, may not be monitored, or identifies as a robot.",
example: false,
@ -256,14 +239,14 @@ const BaseAccount = z
url: "https://docs.joinmastodon.org/entities/Account/#bot",
},
}),
group: z.literal(false).openapi({
group: z.literal(false).meta({
description: "Indicates that the account represents a Group actor.",
example: false,
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Account/#group",
},
}),
discoverable: zBoolean.nullable().openapi({
discoverable: zBoolean.nullable().meta({
description:
"Whether the account has opted into discovery features such as the profile directory.",
example: true,
@ -274,7 +257,7 @@ const BaseAccount = z
noindex: zBoolean
.nullable()
.optional()
.openapi({
.meta({
description:
"Whether the local user has opted out of being indexed by search engines.",
example: false,
@ -282,7 +265,7 @@ const BaseAccount = z
url: "https://docs.joinmastodon.org/entities/Account/#noindex",
},
}),
suspended: zBoolean.optional().openapi({
suspended: zBoolean.optional().meta({
description:
"An extra attribute returned only when an account is suspended.",
example: false,
@ -290,7 +273,7 @@ const BaseAccount = z
url: "https://docs.joinmastodon.org/entities/Account/#suspended",
},
}),
limited: zBoolean.optional().openapi({
limited: zBoolean.optional().meta({
description:
"An extra attribute returned only when an account is silenced. If true, indicates that the account should be hidden behind a warning screen.",
example: false,
@ -298,20 +281,17 @@ const BaseAccount = z
url: "https://docs.joinmastodon.org/entities/Account/#limited",
},
}),
created_at: z
.string()
.datetime()
.openapi({
description: "When the account was created.",
example: "2024-10-15T22:00:00.000Z",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Account/#created_at",
},
}),
created_at: z.iso.datetime().meta({
description: "When the account was created.",
example: "2024-10-15T22:00:00.000Z",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Account/#created_at",
},
}),
// TODO
last_status_at: z
.literal(null)
.openapi({
.meta({
description: "When the most recent status was posted.",
example: null,
externalDocs: {
@ -323,7 +303,7 @@ const BaseAccount = z
.number()
.int()
.nonnegative()
.openapi({
.meta({
description: "How many statuses are attached to this account.",
example: 42,
externalDocs: {
@ -334,7 +314,7 @@ const BaseAccount = z
.number()
.int()
.nonnegative()
.openapi({
.meta({
description: "The reported followers of this profile.",
example: 6,
externalDocs: {
@ -345,7 +325,7 @@ const BaseAccount = z
.number()
.int()
.nonnegative()
.openapi({
.meta({
description: "The reported follows of this profile.",
example: 23,
externalDocs: {
@ -353,7 +333,7 @@ const BaseAccount = z
},
}),
/* Versia Server API extension */
uri: z.string().url().openapi({
uri: z.url().meta({
description:
"The location of the user's Versia profile page, as opposed to the local representation.",
example:
@ -365,26 +345,25 @@ const BaseAccount = z
name: z.string(),
})
.optional(),
get moved() {
return Account.nullable()
.optional()
.meta({
description:
"Indicates that the profile is currently inactive and that its user has moved to a new account.",
example: null,
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Account/#moved",
},
});
},
/* Versia Server API extension */
roles: z.array(Role).openapi({
roles: z.array(Role).meta({
description: "Roles assigned to the account.",
}),
mute_expires_at: z.string().datetime().nullable().openapi({
mute_expires_at: z.iso.datetime().nullable().meta({
description: "When a timed mute will expire, if applicable.",
example: "2025-03-01T14:00:00.000Z",
}),
})
.openapi({ ref: "BaseAccount" });
export const Account = BaseAccount.extend({
moved: BaseAccount.nullable()
.optional()
.openapi({
description:
"Indicates that the profile is currently inactive and that its user has moved to a new account.",
example: null,
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Account/#moved",
},
}),
}).openapi({ ref: "Account" });
.meta({ id: "Account" });