diff --git a/packages/pl-api/lib/client.ts b/packages/pl-api/lib/client.ts index ad955093b..22f9413d5 100644 --- a/packages/pl-api/lib/client.ts +++ b/packages/pl-api/lib/client.ts @@ -227,7 +227,7 @@ class PlApiClient { } } - #paginatedGet = async >(input: URL | RequestInfo, body: RequestBody, schema: T): Promise>> => { + #paginatedGet = async (input: URL | RequestInfo, body: RequestBody, schema: v.BaseSchema>): Promise> => { const getMore = (input: string | null) => input ? async () => { const response = await this.request(input); @@ -2441,7 +2441,7 @@ class PlApiClient { const enqueue = (fn: () => any) => ws.readyState === WebSocket.CONNECTING ? queue.push(fn) : fn(); ws.onmessage = (event) => { - const message = streamingEventSchema.parse(JSON.parse(event.data as string)); + const message = v.parse(streamingEventSchema, JSON.parse(event.data as string)); listeners.filter(({ listener, stream }) => (!stream || message.stream.includes(stream)) && listener(message)); }; @@ -2687,7 +2687,7 @@ class PlApiClient { response = await this.request('/api/v1/instance'); } - const instance = v.parse(instanceSchema.readonly(), response.json); + const instance = v.parse(v.pipe(instanceSchema, v.readonly()), response.json); this.#setInstance(instance); return instance; diff --git a/packages/pl-api/lib/directory-client.ts b/packages/pl-api/lib/directory-client.ts index 69ad428e1..2a41b061c 100644 --- a/packages/pl-api/lib/directory-client.ts +++ b/packages/pl-api/lib/directory-client.ts @@ -1,3 +1,5 @@ +import * as v from 'valibot'; + import { directoryCategorySchema, directoryLanguageSchema, directoryServerSchema, directoryStatisticsPeriodSchema } from './entities'; import { filteredArray } from './entities/utils'; import request from './request'; @@ -23,25 +25,25 @@ class PlApiDirectoryClient { async getStatistics() { const response = await this.request('/statistics'); - return filteredArray(directoryStatisticsPeriodSchema).parse(response.json); + return v.parse(filteredArray(directoryStatisticsPeriodSchema), response.json); } async getCategories(params?: Params) { const response = await this.request('/categories', { params }); - return filteredArray(directoryCategorySchema).parse(response.json); + return v.parse(filteredArray(directoryCategorySchema), response.json); } async getLanguages(params?: Params) { const response = await this.request('/categories', { params }); - return filteredArray(directoryLanguageSchema).parse(response.json); + return v.parse(filteredArray(directoryLanguageSchema), response.json); } async getServers(params?: Params) { const response = await this.request('/servers', { params }); - return filteredArray(directoryServerSchema).parse(response.json); + return v.parse(filteredArray(directoryServerSchema), response.json); } } diff --git a/packages/pl-api/lib/entities/account.ts b/packages/pl-api/lib/entities/account.ts index 91cd28da9..212bd3ba8 100644 --- a/packages/pl-api/lib/entities/account.ts +++ b/packages/pl-api/lib/entities/account.ts @@ -9,7 +9,7 @@ import { coerceObject, dateSchema, filteredArray } from './utils'; const filterBadges = (tags?: string[]) => tags?.filter(tag => tag.startsWith('badge:')).map(tag => v.parse(roleSchema, { id: tag, name: tag.replace(/^badge:/, '') })); -const preprocessAccount = (account: any) => { +const preprocessAccount = v.transform((account: any) => { if (!account?.acct) return null; const username = account.username || account.acct.split('@')[0]; @@ -59,7 +59,7 @@ const preprocessAccount = (account: any) => { ])), ...account.source } : undefined, }; -}; +}); const fieldSchema = v.object({ name: v.string(), @@ -128,11 +128,11 @@ const baseAccountSchema = v.object({ const accountWithMovedAccountSchema = v.object({ ...baseAccountSchema.entries, - moved: v.fallback(v.nullable(z.lazy((): typeof baseAccountSchema => accountWithMovedAccountSchema as any)), null), + moved: v.fallback(v.nullable(v.lazy((): typeof baseAccountSchema => accountWithMovedAccountSchema as any)), null), }); /** @see {@link https://docs.joinmastodon.org/entities/Account/} */ -const untypedAccountSchema = z.preprocess(preprocessAccount, accountWithMovedAccountSchema); +const untypedAccountSchema = v.pipe(v.any(), preprocessAccount, accountWithMovedAccountSchema); type WithMoved = { moved: Account | null; @@ -140,9 +140,9 @@ type WithMoved = { type Account = v.InferOutput & WithMoved; -const accountSchema: z.ZodType = untypedAccountSchema as any; +const accountSchema: v.BaseSchema> = untypedAccountSchema as any; -const untypedCredentialAccountSchema = z.preprocess(preprocessAccount, v.object({ +const untypedCredentialAccountSchema = v.pipe(v.any(), preprocessAccount, v.object({ ...accountWithMovedAccountSchema.entries, source: v.fallback(v.nullable(v.object({ note: v.fallback(v.string(), ''), @@ -150,7 +150,7 @@ const untypedCredentialAccountSchema = z.preprocess(preprocessAccount, v.object( privacy: v.picklist(['public', 'unlisted', 'private', 'direct']), sensitive: v.fallback(v.boolean(), false), language: v.fallback(v.nullable(v.string()), null), - follow_requests_count: z.number().int().nonnegative().catch(0), + follow_requests_count: v.fallback(v.pipe(v.number(), v.integer(), v.minValue(0)), 0), show_role: v.fallback(v.nullable(v.optional(v.boolean())), undefined), no_rich_text: v.fallback(v.nullable(v.optional(v.boolean())), undefined), @@ -173,16 +173,16 @@ const untypedCredentialAccountSchema = z.preprocess(preprocessAccount, v.object( type CredentialAccount = v.InferOutput & WithMoved; -const credentialAccountSchema: z.ZodType = untypedCredentialAccountSchema as any; +const credentialAccountSchema: v.BaseSchema> = untypedCredentialAccountSchema as any; -const untypedMutedAccountSchema = z.preprocess(preprocessAccount, v.object({ +const untypedMutedAccountSchema = v.pipe(v.any(), preprocessAccount, v.object({ ...accountWithMovedAccountSchema.entries, mute_expires_at: v.fallback(v.nullable(dateSchema), null), })); type MutedAccount = v.InferOutput & WithMoved; -const mutedAccountSchema: z.ZodType = untypedMutedAccountSchema as any; +const mutedAccountSchema: v.BaseSchema> = untypedMutedAccountSchema as any; export { accountSchema, diff --git a/packages/pl-api/lib/entities/admin/account.ts b/packages/pl-api/lib/entities/admin/account.ts index 2df73feb2..065e33ca5 100644 --- a/packages/pl-api/lib/entities/admin/account.ts +++ b/packages/pl-api/lib/entities/admin/account.ts @@ -7,59 +7,63 @@ import { dateSchema, filteredArray } from '../utils'; import { adminIpSchema } from './ip'; /** @see {@link https://docs.joinmastodon.org/entities/Admin_Account/} */ -const adminAccountSchema = z.preprocess((account: any) => { - if (!account.account) { +const adminAccountSchema = v.pipe( + v.any(), + v.transform((account: any) => { + if (!account.account) { /** * Convert Pleroma account schema * @see {@link https://docs.pleroma.social/backend/development/API/admin_api/#get-apiv1pleromaadminusers} */ - return { - id: account.id, - account: null, - username: account.nickname, - domain: account.nickname.split('@')[1] || null, - created_at: account.created_at, - email: account.email, - invite_request: account.registration_reason, - role: account.roles?.is_admin - ? v.parse(roleSchema, { name: 'Admin' }) - : account.roles?.moderator - ? v.parse(roleSchema, { name: 'Moderator ' }) : - null, - confirmed: account.is_confirmed, - approved: account.is_approved, - disabled: !account.is_active, + return { + id: account.id, + account: null, + username: account.nickname, + domain: account.nickname.split('@')[1] || null, + created_at: account.created_at, + email: account.email, + invite_request: account.registration_reason, + role: account.roles?.is_admin + ? v.parse(roleSchema, { name: 'Admin' }) + : account.roles?.moderator + ? v.parse(roleSchema, { name: 'Moderator ' }) : + null, + confirmed: account.is_confirmed, + approved: account.is_approved, + disabled: !account.is_active, - actor_type: account.actor_type, - display_name: account.display_name, - suggested: account.is_suggested, - }; - } - return account; -}, v.object({ - id: v.string(), - username: v.string(), - domain: v.fallback(v.nullable(v.string()), null), - created_at: dateSchema, - email: v.fallback(v.nullable(v.string()), null), - ip: v.fallback(v.nullable(v.pipe(v.string(), v.ip())), null), - ips: filteredArray(adminIpSchema), - locale: v.fallback(v.nullable(v.string()), null), - invite_request: v.fallback(v.nullable(v.string()), null), - role: v.fallback(v.nullable(roleSchema), null), - confirmed: v.fallback(v.boolean(), false), - approved: v.fallback(v.boolean(), false), - disabled: v.fallback(v.boolean(), false), - silenced: v.fallback(v.boolean(), false), - suspended: v.fallback(v.boolean(), false), - account: v.fallback(v.nullable(accountSchema), null), - created_by_application_id: v.fallback(v.optional(v.string()), undefined), - invited_by_account_id: v.fallback(v.optional(v.string()), undefined), + actor_type: account.actor_type, + display_name: account.display_name, + suggested: account.is_suggested, + }; + } + return account; + }), + v.object({ + id: v.string(), + username: v.string(), + domain: v.fallback(v.nullable(v.string()), null), + created_at: dateSchema, + email: v.fallback(v.nullable(v.string()), null), + ip: v.fallback(v.nullable(v.pipe(v.string(), v.ip())), null), + ips: filteredArray(adminIpSchema), + locale: v.fallback(v.nullable(v.string()), null), + invite_request: v.fallback(v.nullable(v.string()), null), + role: v.fallback(v.nullable(roleSchema), null), + confirmed: v.fallback(v.boolean(), false), + approved: v.fallback(v.boolean(), false), + disabled: v.fallback(v.boolean(), false), + silenced: v.fallback(v.boolean(), false), + suspended: v.fallback(v.boolean(), false), + account: v.fallback(v.nullable(accountSchema), null), + created_by_application_id: v.fallback(v.optional(v.string()), undefined), + invited_by_account_id: v.fallback(v.optional(v.string()), undefined), - actor_type: v.fallback(v.nullable(v.string()), null), - display_name: v.fallback(v.nullable(v.string()), null), - suggested: v.fallback(v.nullable(v.boolean()), null), -})); + actor_type: v.fallback(v.nullable(v.string()), null), + display_name: v.fallback(v.nullable(v.string()), null), + suggested: v.fallback(v.nullable(v.boolean()), null), + }), +); type AdminAccount = v.InferOutput; diff --git a/packages/pl-api/lib/entities/admin/announcement.ts b/packages/pl-api/lib/entities/admin/announcement.ts index 0f44336b2..5cef6c4a1 100644 --- a/packages/pl-api/lib/entities/admin/announcement.ts +++ b/packages/pl-api/lib/entities/admin/announcement.ts @@ -4,13 +4,17 @@ import * as v from 'valibot'; import { announcementSchema } from '../announcement'; /** @see {@link https://docs.pleroma.social/backend/development/API/admin_api/#get-apiv1pleromaadminannouncements} */ -const adminAnnouncementSchema = z.preprocess((announcement: any) => ({ - ...announcement, - ...pick(announcement.pleroma, 'raw_content'), -}), v.object({ - ...announcementSchema.entries, - raw_content: v.fallback(v.string(), ''), -})); +const adminAnnouncementSchema = v.pipe( + v.any(), + v.transform((announcement: any) => ({ + ...announcement, + ...pick(announcement.pleroma, 'raw_content'), + })), + v.object({ + ...announcementSchema.entries, + raw_content: v.fallback(v.string(), ''), + }), +); type AdminAnnouncement = v.InferOutput; diff --git a/packages/pl-api/lib/entities/admin/ip-block.ts b/packages/pl-api/lib/entities/admin/ip-block.ts index bf470a426..5347035d8 100644 --- a/packages/pl-api/lib/entities/admin/ip-block.ts +++ b/packages/pl-api/lib/entities/admin/ip-block.ts @@ -5,7 +5,7 @@ import { dateSchema } from '../utils'; /** @see {@link https://docs.joinmastodon.org/entities/Admin_IpBlock/} */ const adminIpBlockSchema = v.object({ id: v.string(), - ip: z.string().ip(), + ip: v.pipe(v.string(), v.ip()), severity: v.picklist(['sign_up_requires_approval', 'sign_up_block', 'no_access']), comment: v.fallback(v.string(), ''), created_at: dateSchema, diff --git a/packages/pl-api/lib/entities/admin/ip.ts b/packages/pl-api/lib/entities/admin/ip.ts index 83ebb5654..8d99befa4 100644 --- a/packages/pl-api/lib/entities/admin/ip.ts +++ b/packages/pl-api/lib/entities/admin/ip.ts @@ -4,7 +4,7 @@ import { dateSchema } from '../utils'; /** @see {@link https://docs.joinmastodon.org/entities/Admin_Ip/} */ const adminIpSchema = v.object({ - ip: z.string().ip(), + ip: v.pipe(v.string(), v.ip()), used_at: dateSchema, }); diff --git a/packages/pl-api/lib/entities/admin/relay.ts b/packages/pl-api/lib/entities/admin/relay.ts index ce19ea3ca..efe2df0a8 100644 --- a/packages/pl-api/lib/entities/admin/relay.ts +++ b/packages/pl-api/lib/entities/admin/relay.ts @@ -1,10 +1,14 @@ import * as v from 'valibot'; -const adminRelaySchema = z.preprocess((data: any) => ({ id: data.actor, ...data }), v.object({ - actor: v.fallback(v.string(), ''), - id: v.string(), - followed_back: v.fallback(v.boolean(), false), -})); +const adminRelaySchema = v.pipe( + v.any(), + v.transform((data: any) => ({ id: data.actor, ...data })), + v.object({ + actor: v.fallback(v.string(), ''), + id: v.string(), + followed_back: v.fallback(v.boolean(), false), + }), +); type AdminRelay = v.InferOutput diff --git a/packages/pl-api/lib/entities/admin/report.ts b/packages/pl-api/lib/entities/admin/report.ts index 4ae7c4f3c..0a577ecd2 100644 --- a/packages/pl-api/lib/entities/admin/report.ts +++ b/packages/pl-api/lib/entities/admin/report.ts @@ -8,38 +8,42 @@ import { dateSchema, filteredArray } from '../utils'; import { adminAccountSchema } from './account'; /** @see {@link https://docs.joinmastodon.org/entities/Admin_Report/} */ -const adminReportSchema = z.preprocess((report: any) => { - if (report.actor) { +const adminReportSchema = v.pipe( + v.any(), + v.transform((report: any) => { + if (report.actor) { /** * Convert Pleroma report schema * @see {@link https://docs.pleroma.social/backend/development/API/admin_api/#get-apiv1pleromaadminreports} */ - return { - action_taken: report.state !== 'open', - comment: report.content, - updated_at: report.created_at, - account: report.actor, - target_account: report.account, - ...(pick(report, ['id', 'assigned_account', 'created_at', 'rules', 'statuses'])), - }; - } - return report; -}, v.object({ - id: v.string(), - action_taken: v.fallback(v.optional(v.boolean()), undefined), - action_taken_at: v.fallback(v.nullable(dateSchema), null), - category: v.fallback(v.optional(v.string()), undefined), - comment: v.fallback(v.optional(v.string()), undefined), - forwarded: v.fallback(v.optional(v.boolean()), undefined), - created_at: v.fallback(v.optional(dateSchema), undefined), - updated_at: v.fallback(v.optional(dateSchema), undefined), - account: adminAccountSchema, - target_account: adminAccountSchema, - assigned_account: v.fallback(v.nullable(adminAccountSchema), null), - action_taken_by_account: v.fallback(v.nullable(adminAccountSchema), null), - statuses: filteredArray(statusWithoutAccountSchema), - rules: filteredArray(ruleSchema), -})); + return { + action_taken: report.state !== 'open', + comment: report.content, + updated_at: report.created_at, + account: report.actor, + target_account: report.account, + ...(pick(report, ['id', 'assigned_account', 'created_at', 'rules', 'statuses'])), + }; + } + return report; + }), + v.object({ + id: v.string(), + action_taken: v.fallback(v.optional(v.boolean()), undefined), + action_taken_at: v.fallback(v.nullable(dateSchema), null), + category: v.fallback(v.optional(v.string()), undefined), + comment: v.fallback(v.optional(v.string()), undefined), + forwarded: v.fallback(v.optional(v.boolean()), undefined), + created_at: v.fallback(v.optional(dateSchema), undefined), + updated_at: v.fallback(v.optional(dateSchema), undefined), + account: adminAccountSchema, + target_account: adminAccountSchema, + assigned_account: v.fallback(v.nullable(adminAccountSchema), null), + action_taken_by_account: v.fallback(v.nullable(adminAccountSchema), null), + statuses: filteredArray(statusWithoutAccountSchema), + rules: filteredArray(ruleSchema), + }), +); type AdminReport = v.InferOutput; diff --git a/packages/pl-api/lib/entities/announcement-reaction.ts b/packages/pl-api/lib/entities/announcement-reaction.ts index 9fb1a0da7..fcb5881d9 100644 --- a/packages/pl-api/lib/entities/announcement-reaction.ts +++ b/packages/pl-api/lib/entities/announcement-reaction.ts @@ -3,7 +3,7 @@ import * as v from 'valibot'; /** @see {@link https://docs.joinmastodon.org/entities/announcement/} */ const announcementReactionSchema = v.object({ name: v.fallback(v.string(), ''), - count: z.number().int().nonnegative().catch(0), + count: v.fallback(v.pipe(v.number(), v.integer(), v.minValue(0)), 0), me: v.fallback(v.boolean(), false), url: v.fallback(v.nullable(v.string()), null), static_url: v.fallback(v.nullable(v.string()), null), diff --git a/packages/pl-api/lib/entities/announcement.ts b/packages/pl-api/lib/entities/announcement.ts index 0be54d135..b62950d38 100644 --- a/packages/pl-api/lib/entities/announcement.ts +++ b/packages/pl-api/lib/entities/announcement.ts @@ -16,11 +16,12 @@ const announcementSchema = v.object({ read: v.fallback(v.boolean(), false), published_at: dateSchema, reactions: filteredArray(announcementReactionSchema), - statuses: z.preprocess( - (statuses: any) => Array.isArray(statuses) + statuses: v.pipe( + v.any(), + v.transform((statuses: any) => Array.isArray(statuses) ? Object.fromEntries(statuses.map((status: any) => [status.url, status.account?.acct]) || []) - : statuses, - v.record(v.string(), v.string(), v.string()), + : statuses), + v.record(v.string(), v.string()), ), mentions: filteredArray(mentionSchema), tags: filteredArray(tagSchema), diff --git a/packages/pl-api/lib/entities/emoji-reaction.ts b/packages/pl-api/lib/entities/emoji-reaction.ts index 0e46529fc..2fbccf22d 100644 --- a/packages/pl-api/lib/entities/emoji-reaction.ts +++ b/packages/pl-api/lib/entities/emoji-reaction.ts @@ -24,11 +24,15 @@ const customEmojiReactionSchema = v.object({ * Pleroma emoji reaction. * @see {@link https://docs.pleroma.social/backend/development/API/differences_in_mastoapi_responses/#statuses} */ -const emojiReactionSchema = z.preprocess((reaction: any) => reaction ? { - static_url: reaction.url, - account_ids: reaction.accounts?.map((account: any) => account?.id), - ...reaction, -} : null, v.union([baseEmojiReactionSchema, customEmojiReactionSchema]); +const emojiReactionSchema = v.pipe( + v.any(), + v.transform((reaction: any) => reaction ? { + static_url: reaction.url, + account_ids: reaction.accounts?.map((account: any) => account?.id), + ...reaction, + } : null), + v.union([baseEmojiReactionSchema, customEmojiReactionSchema]), +); type EmojiReaction = v.InferOutput; diff --git a/packages/pl-api/lib/entities/filter.ts b/packages/pl-api/lib/entities/filter.ts index 5301800f5..cbc34e06b 100644 --- a/packages/pl-api/lib/entities/filter.ts +++ b/packages/pl-api/lib/entities/filter.ts @@ -16,29 +16,33 @@ const filterStatusSchema = v.object({ }); /** @see {@link https://docs.joinmastodon.org/entities/Filter/} */ -const filterSchema = z.preprocess((filter: any) => { - if (filter.phrase) { - return { - ...filter, - title: filter.phrase, - keywords: [{ - id: '1', - keyword: filter.phrase, - whole_word: filter.whole_word, - }], - filter_action: filter.irreversible ? 'hide' : 'warn', - }; - } - return filter; -}, v.object({ - id: v.string(), - title: v.string(), - context: v.array(v.picklist(['home', 'notifications', 'public', 'thread', 'account'])), - expires_at: v.fallback(v.nullable(z.string().datetime({ offset: true })), null), - filter_action: v.fallback(v.picklist(['warn', 'hide']), 'warn'), - keywords: filteredArray(filterKeywordSchema), - statuses: filteredArray(filterStatusSchema), -})); +const filterSchema = v.pipe( + v.any(), + v.transform((filter: any) => { + if (filter.phrase) { + return { + ...filter, + title: filter.phrase, + keywords: [{ + id: '1', + keyword: filter.phrase, + whole_word: filter.whole_word, + }], + filter_action: filter.irreversible ? 'hide' : 'warn', + }; + } + return filter; + }), + v.object({ + id: v.string(), + title: v.string(), + context: v.array(v.picklist(['home', 'notifications', 'public', 'thread', 'account'])), + expires_at: v.fallback(v.nullable(z.string().datetime({ offset: true })), null), + filter_action: v.fallback(v.picklist(['warn', 'hide']), 'warn'), + keywords: filteredArray(filterKeywordSchema), + statuses: filteredArray(filterStatusSchema), + }), +); type Filter = v.InferOutput; diff --git a/packages/pl-api/lib/entities/group-member.ts b/packages/pl-api/lib/entities/group-member.ts index 4eb249ea4..7f4f80f26 100644 --- a/packages/pl-api/lib/entities/group-member.ts +++ b/packages/pl-api/lib/entities/group-member.ts @@ -13,7 +13,7 @@ type GroupRole =`${GroupRoles}`; const groupMemberSchema = v.object({ id: v.string(), account: accountSchema, - role: z.nativeEnum(GroupRoles), + role: v.enum(GroupRoles), }); type GroupMember = v.InferOutput; diff --git a/packages/pl-api/lib/entities/group-relationship.ts b/packages/pl-api/lib/entities/group-relationship.ts index 79489787d..96c2a0436 100644 --- a/packages/pl-api/lib/entities/group-relationship.ts +++ b/packages/pl-api/lib/entities/group-relationship.ts @@ -5,7 +5,7 @@ import { GroupRoles } from './group-member'; const groupRelationshipSchema = v.object({ id: v.string(), member: v.fallback(v.boolean(), false), - role: z.nativeEnum(GroupRoles).catch(GroupRoles.USER), + role: v.fallback(v.enum(GroupRoles), GroupRoles.USER), requested: v.fallback(v.boolean(), false), }); diff --git a/packages/pl-api/lib/entities/group.ts b/packages/pl-api/lib/entities/group.ts index 1e0884696..6f811bb5e 100644 --- a/packages/pl-api/lib/entities/group.ts +++ b/packages/pl-api/lib/entities/group.ts @@ -18,7 +18,7 @@ const groupSchema = v.object({ membership_required: v.fallback(v.boolean(), false), members_count: v.fallback(v.number(), 0), owner: v.fallback(v.nullable(v.object({ id: v.string() })), null), - note: z.string().transform(note => note === '

' ? '' : note).catch(''), + note: v.fallback(v.pipe(v.string(), v.transform(note => note === '

' ? '' : note)), ''), relationship: v.fallback(v.nullable(groupRelationshipSchema), null), // Dummy field to be overwritten later statuses_visibility: v.fallback(v.string(), 'public'), uri: v.fallback(v.string(), ''), diff --git a/packages/pl-api/lib/entities/instance.ts b/packages/pl-api/lib/entities/instance.ts index d289ec4d8..6a1413781 100644 --- a/packages/pl-api/lib/entities/instance.ts +++ b/packages/pl-api/lib/entities/instance.ts @@ -184,9 +184,9 @@ const pleromaSchema = coerceObject({ }), }), fields_limits: coerceObject({ - max_fields: z.number().nonnegative().catch(4), - name_length: z.number().nonnegative().catch(255), - value_length: z.number().nonnegative().catch(2047), + max_fields: v.fallback(v.pipe(v.number(), v.integer(), v.minValue(0)), 4), + name_length: v.fallback(v.pipe(v.number(), v.integer(), v.minValue(0)), 255), + value_length: v.fallback(v.pipe(v.number(), v.integer(), v.minValue(0)), 2047), }), markup: coerceObject({ allow_headings: v.fallback(v.boolean(), false), @@ -293,43 +293,40 @@ const instanceV1Schema = coerceObject({ }); /** @see {@link https://docs.joinmastodon.org/entities/Instance/} */ -const instanceSchema = z.preprocess((data: any) => { +const instanceSchema = v.pipe( + v.any(), + v.transform((data: any) => { // Detect GoToSocial - if (typeof data.configuration?.accounts?.allow_custom_css === 'boolean') { - data.version = `0.0.0 (compatible; GoToSocial ${data.version})`; - } + if (typeof data.configuration?.accounts?.allow_custom_css === 'boolean') { + data.version = `0.0.0 (compatible; GoToSocial ${data.version})`; + } - const apiVersions = getApiVersions(data); + const apiVersions = getApiVersions(data); - if (data.domain) return { account_domain: data.domain, ...data, api_versions: apiVersions }; + if (data.domain) return { account_domain: data.domain, ...data, api_versions: apiVersions }; - return instanceV1ToV2({ ...data, api_versions: apiVersions }); -}, coerceObject({ - account_domain: v.fallback(v.string(), ''), - api_versions: v.fallback(v.record(v.string(), v.number()), {}), - configuration: configurationSchema, - contact: contactSchema, - description: v.fallback(v.string(), ''), - domain: v.fallback(v.string(), ''), - feature_quote: v.fallback(v.boolean(), false), - fedibird_capabilities: v.fallback(v.array(v.string()), []), - languages: v.fallback(v.array(v.string()), []), - pleroma: pleromaSchema, - registrations: registrations, - rules: filteredArray(ruleSchema), - stats: statsSchema, - thumbnail: thumbnailSchema, - title: v.fallback(v.string(), ''), - usage: usageSchema, - version: v.fallback(v.string(), '0.0.0'), -}).transform((instance) => { - const version = fixVersion(instance.version); - - return { - ...instance, - version, - }; -})); + return instanceV1ToV2({ ...data, api_versions: apiVersions }); + }), + coerceObject({ + account_domain: v.fallback(v.string(), ''), + api_versions: v.fallback(v.record(v.string(), v.number()), {}), + configuration: configurationSchema, + contact: contactSchema, + description: v.fallback(v.string(), ''), + domain: v.fallback(v.string(), ''), + feature_quote: v.fallback(v.boolean(), false), + fedibird_capabilities: v.fallback(v.array(v.string()), []), + languages: v.fallback(v.array(v.string()), []), + pleroma: pleromaSchema, + registrations: registrations, + rules: filteredArray(ruleSchema), + stats: statsSchema, + thumbnail: thumbnailSchema, + title: v.fallback(v.string(), ''), + usage: usageSchema, + version: v.pipe(v.fallback(v.string(), '0.0.0'), v.transform(fixVersion)), + }), +); type Instance = v.InferOutput; diff --git a/packages/pl-api/lib/entities/location.ts b/packages/pl-api/lib/entities/location.ts index 37015600d..ff723fa95 100644 --- a/packages/pl-api/lib/entities/location.ts +++ b/packages/pl-api/lib/entities/location.ts @@ -13,7 +13,7 @@ const locationSchema = v.object({ type: v.fallback(v.string(), ''), timezone: v.fallback(v.string(), ''), geom: v.fallback(v.nullable(v.object({ - coordinates: v.fallback(v.nullable(z.tuple([v.number(), v.number()])), null), + coordinates: v.fallback(v.nullable(v.tuple([v.number(), v.number()])), null), srid: v.fallback(v.string(), ''), })), null), }); diff --git a/packages/pl-api/lib/entities/marker.ts b/packages/pl-api/lib/entities/marker.ts index 9576a9803..c8749a6dd 100644 --- a/packages/pl-api/lib/entities/marker.ts +++ b/packages/pl-api/lib/entities/marker.ts @@ -2,15 +2,19 @@ import * as v from 'valibot'; import { dateSchema } from './utils'; -const markerSchema = z.preprocess((marker: any) => marker ? ({ - unread_count: marker.pleroma?.unread_count, - ...marker, -}) : null, v.object({ - last_read_id: v.string(), - version: v.pipe(v.number(), v.integer()), - updated_at: dateSchema, - unread_count: v.fallback(v.optional(v.pipe(v.number(), v.integer())), undefined), -})); +const markerSchema = v.pipe( + v.any(), + v.transform((marker: any) => marker ? ({ + unread_count: marker.pleroma?.unread_count, + ...marker, + }) : null), + v.object({ + last_read_id: v.string(), + version: v.pipe(v.number(), v.integer()), + updated_at: dateSchema, + unread_count: v.fallback(v.optional(v.pipe(v.number(), v.integer())), undefined), + }), +); /** @see {@link https://docs.joinmastodon.org/entities/Marker/} */ type Marker = v.InferOutput; diff --git a/packages/pl-api/lib/entities/media-attachment.ts b/packages/pl-api/lib/entities/media-attachment.ts index e66cf1fd6..47e9ee8f9 100644 --- a/packages/pl-api/lib/entities/media-attachment.ts +++ b/packages/pl-api/lib/entities/media-attachment.ts @@ -3,16 +3,10 @@ import * as v from 'valibot'; import { mimeSchema } from './utils'; -const blurhashSchema = z.string().superRefine((value, ctx) => { - const r = isBlurhashValid(value); - - if (!r.result) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: r.errorReason, - }); - } -}); +const blurhashSchema = v.pipe(v.string(), v.check( + (value) => isBlurhashValid(value).result, + 'invalid blurhash', // .errorReason +)); const baseAttachmentSchema = v.object({ id: v.string(), @@ -40,8 +34,8 @@ const imageAttachmentSchema = v.object({ original: v.fallback(v.optional(imageMetaSchema), undefined), small: v.fallback(v.optional(imageMetaSchema), undefined), focus: v.fallback(v.optional(v.object({ - x: z.number().min(-1).max(1), - y: z.number().min(-1).max(1), + x: v.pipe(v.number(), v.minValue(-1), v.maxValue(1)), + y: v.pipe(v.number(), v.minValue(-1), v.maxValue(1)), })), undefined), }), {}), }); @@ -54,7 +48,7 @@ const videoAttachmentSchema = v.object({ original: v.fallback(v.optional(v.object({ ...imageMetaSchema.entries, frame_rate: v.fallback(v.nullable(v.pipe(v.string(), v.regex(/\d+\/\d+$/))), null), - duration: v.fallback(v.nullable(z.number().nonnegative()), null), + duration: v.fallback(v.nullable(v.pipe(v.number(), v.minValue(0))), null), })), undefined), small: v.fallback(v.optional(imageMetaSchema), undefined), // WIP: add rest @@ -83,7 +77,7 @@ const audioAttachmentSchema = v.object({ })), undefined), original: v.fallback(v.optional(v.object({ duration: v.fallback(v.optional(v.number()), undefined), - bitrate: z.number().nonnegative().optional().catch(undefined), + bitrate: v.fallback(v.optional(v.pipe(v.number(), v.minValue(0))), undefined), })), undefined), }), {}), }); @@ -94,21 +88,25 @@ const unknownAttachmentSchema = v.object({ }); /** @see {@link https://docs.joinmastodon.org/entities/MediaAttachment} */ -const mediaAttachmentSchema = z.preprocess((data: any) => { - if (!data) return null; +const mediaAttachmentSchema = v.pipe( + v.any(), + v.transform((data: any) => { + if (!data) return null; - return { - mime_type: data.pleroma?.mime_type, - preview_url: data.url, - ...data, - }; -}, v.variant('type', [ - imageAttachmentSchema, - videoAttachmentSchema, - gifvAttachmentSchema, - audioAttachmentSchema, - unknownAttachmentSchema, -])); + return { + mime_type: data.pleroma?.mime_type, + preview_url: data.url, + ...data, + }; + }), + v.variant('type', [ + imageAttachmentSchema, + videoAttachmentSchema, + gifvAttachmentSchema, + audioAttachmentSchema, + unknownAttachmentSchema, + ]), +); type MediaAttachment = v.InferOutput; diff --git a/packages/pl-api/lib/entities/mention.ts b/packages/pl-api/lib/entities/mention.ts index 19cb4132b..43b15c25a 100644 --- a/packages/pl-api/lib/entities/mention.ts +++ b/packages/pl-api/lib/entities/mention.ts @@ -1,18 +1,21 @@ import * as v from 'valibot'; /** @see {@link https://docs.joinmastodon.org/entities/Status/#Mention} */ -const mentionSchema = v.object({ - id: v.string(), - username: v.fallback(v.string(), ''), - url: v.fallback(v.pipe(v.string(), v.url()), ''), - acct: v.string(), -}).transform((mention) => { - if (!mention.username) { - mention.username = mention.acct.split('@')[0]; - } +const mentionSchema = v.pipe( + v.object({ + id: v.string(), + username: v.fallback(v.string(), ''), + url: v.fallback(v.pipe(v.string(), v.url()), ''), + acct: v.string(), + }), + v.transform((mention) => { + if (!mention.username) { + mention.username = mention.acct.split('@')[0]; + } - return mention; -}); + return mention; + }), +); type Mention = v.InferOutput; diff --git a/packages/pl-api/lib/entities/notification.ts b/packages/pl-api/lib/entities/notification.ts index 2a687dc7a..03916463c 100644 --- a/packages/pl-api/lib/entities/notification.ts +++ b/packages/pl-api/lib/entities/notification.ts @@ -84,25 +84,28 @@ const eventParticipationRequestNotificationSchema = v.object({ }); /** @see {@link https://docs.joinmastodon.org/entities/Notification/} */ -const notificationSchema: z.ZodType = z.preprocess((notification: any) => ({ - group_key: `ungrouped-${notification.id}`, - ...pick(notification.pleroma || {}, ['is_muted', 'is_seen']), - ...notification, - type: notification.type === 'pleroma:report' - ? 'admin.report' - : notification.type?.replace(/^pleroma:/, ''), -}), v.variant('type', [ - accountNotificationSchema, - mentionNotificationSchema, - statusNotificationSchema, - reportNotificationSchema, - severedRelationshipNotificationSchema, - moderationWarningNotificationSchema, - moveNotificationSchema, - emojiReactionNotificationSchema, - chatMentionNotificationSchema, - eventParticipationRequestNotificationSchema, -])) as any; +const notificationSchema: v.BaseSchema> = v.pipe( + v.any(), + v.transform((notification: any) => ({ + group_key: `ungrouped-${notification.id}`, + ...pick(notification.pleroma || {}, ['is_muted', 'is_seen']), + ...notification, + type: notification.type === 'pleroma:report' + ? 'admin.report' + : notification.type?.replace(/^pleroma:/, ''), + })), + v.variant('type', [ + accountNotificationSchema, + mentionNotificationSchema, + statusNotificationSchema, + reportNotificationSchema, + severedRelationshipNotificationSchema, + moderationWarningNotificationSchema, + moveNotificationSchema, + emojiReactionNotificationSchema, + chatMentionNotificationSchema, + eventParticipationRequestNotificationSchema, + ])) as any; type Notification = v.InferOutput< | typeof accountNotificationSchema diff --git a/packages/pl-api/lib/entities/oauth-token.ts b/packages/pl-api/lib/entities/oauth-token.ts index cd97b9aac..1370fea50 100644 --- a/packages/pl-api/lib/entities/oauth-token.ts +++ b/packages/pl-api/lib/entities/oauth-token.ts @@ -1,14 +1,18 @@ import * as v from 'valibot'; /** @see {@link https://docs.pleroma.social/backend/development/API/pleroma_api/#get-apioauth_tokens} */ -const oauthTokenSchema = z.preprocess((token: any) => ({ - ...token, - valid_until: token?.valid_until?.padEnd(27, 'Z'), -}), v.object({ - app_name: v.string(), - id: v.number(), - valid_until: z.string().datetime({ offset: true }), -})); +const oauthTokenSchema = v.pipe( + v.any(), + v.transform((token: any) => ({ + ...token, + valid_until: token?.valid_until?.padEnd(27, 'Z'), + })), + v.object({ + app_name: v.string(), + id: v.number(), + valid_until: z.string().datetime({ offset: true }), + }), +); type OauthToken = v.InferOutput; diff --git a/packages/pl-api/lib/entities/poll.ts b/packages/pl-api/lib/entities/poll.ts index 3a76c1ef5..8ee66237f 100644 --- a/packages/pl-api/lib/entities/poll.ts +++ b/packages/pl-api/lib/entities/poll.ts @@ -17,10 +17,10 @@ const pollSchema = v.object({ expires_at: v.fallback(v.nullable(z.string().datetime()), null), id: v.string(), multiple: v.fallback(v.boolean(), false), - options: v.array(pollOptionSchema).min(2), + options: v.pipe(v.array(pollOptionSchema), v.minLength(2)), voters_count: v.fallback(v.number(), 0), votes_count: v.fallback(v.number(), 0), - own_votes: v.fallback(v.nullable(v.array(v.number())).nonempty(), null), + own_votes: v.fallback(v.nullable(v.pipe(v.array(v.number()), v.minLength(1))), null), voted: v.fallback(v.boolean(), false), non_anonymous: v.fallback(v.boolean(), false), diff --git a/packages/pl-api/lib/entities/rule.ts b/packages/pl-api/lib/entities/rule.ts index 998a160a5..a94f2d117 100644 --- a/packages/pl-api/lib/entities/rule.ts +++ b/packages/pl-api/lib/entities/rule.ts @@ -7,10 +7,14 @@ const baseRuleSchema = v.object({ }); /** @see {@link https://docs.joinmastodon.org/entities/Rule/} */ -const ruleSchema = z.preprocess((data: any) => ({ - ...data, - hint: data.hint || data.subtext, -}), baseRuleSchema); +const ruleSchema = v.pipe( + v.any(), + v.transform((data: any) => ({ + ...data, + hint: data.hint || data.subtext, + })), + baseRuleSchema, +); type Rule = v.InferOutput; diff --git a/packages/pl-api/lib/entities/scrobble.ts b/packages/pl-api/lib/entities/scrobble.ts index a2766e704..fb4d6c9ea 100644 --- a/packages/pl-api/lib/entities/scrobble.ts +++ b/packages/pl-api/lib/entities/scrobble.ts @@ -2,19 +2,23 @@ import * as v from 'valibot'; import { accountSchema } from './account'; -const scrobbleSchema = z.preprocess((scrobble: any) => scrobble ? { - external_link: scrobble.externalLink, - ...scrobble, -} : null, v.object({ - id: v.pipe(v.unknown(), v.transform(String)), - account: accountSchema, - created_at: z.string().datetime({ offset: true }), - title: v.string(), - artist: v.fallback(v.string(), ''), - album: v.fallback(v.string(), ''), - external_link: v.fallback(v.nullable(v.string()), null), - length: v.fallback(v.nullable(v.number()), null), -})); +const scrobbleSchema = v.pipe( + v.any(), + v.transform((scrobble: any) => scrobble ? { + external_link: scrobble.externalLink, + ...scrobble, + } : null), + v.object({ + id: v.pipe(v.unknown(), v.transform(String)), + account: accountSchema, + created_at: z.string().datetime({ offset: true }), + title: v.string(), + artist: v.fallback(v.string(), ''), + album: v.fallback(v.string(), ''), + external_link: v.fallback(v.nullable(v.string()), null), + length: v.fallback(v.nullable(v.number()), null), + }), +); type Scrobble = v.InferOutput; diff --git a/packages/pl-api/lib/entities/status.ts b/packages/pl-api/lib/entities/status.ts index c867c45d0..acd6d3140 100644 --- a/packages/pl-api/lib/entities/status.ts +++ b/packages/pl-api/lib/entities/status.ts @@ -134,19 +134,19 @@ const preprocess = (status: any) => { return status; }; -const statusSchema: z.ZodType = z.preprocess(preprocess, v.object({ +const statusSchema: v.BaseSchema> = v.pipe(v.any(), v.transform(preprocess), v.object({ ...baseStatusSchema.entries, - reblog: v.fallback(v.nullable(z.lazy(() => statusSchema)), null), + reblog: v.fallback(v.nullable(v.lazy(() => statusSchema)), null), - quote: v.fallback(v.nullable(z.lazy(() => statusSchema)), null), + quote: v.fallback(v.nullable(v.lazy(() => statusSchema)), null), })) as any; -const statusWithoutAccountSchema = z.preprocess(preprocess, v.object({ +const statusWithoutAccountSchema = v.pipe(v.any(), v.transform(preprocess), v.object({ ...(v.omit(baseStatusSchema, ['account']).entries), account: v.fallback(v.nullable(accountSchema), null), - reblog: v.fallback(v.nullable(z.lazy(() => statusSchema)), null), + reblog: v.fallback(v.nullable(v.lazy(() => statusSchema)), null), - quote: v.fallback(v.nullable(z.lazy(() => statusSchema)), null), + quote: v.fallback(v.nullable(v.lazy(() => statusSchema)), null), })); type Status = v.InferOutput & { diff --git a/packages/pl-api/lib/entities/streaming-event.ts b/packages/pl-api/lib/entities/streaming-event.ts index 45a9f6531..d41e6586f 100644 --- a/packages/pl-api/lib/entities/streaming-event.ts +++ b/packages/pl-api/lib/entities/streaming-event.ts @@ -31,7 +31,7 @@ const baseStreamingEventSchema = v.object({ const statusStreamingEventSchema = v.object({ ...baseStreamingEventSchema.entries, event: v.picklist(['update', 'status.update']), - payload: z.preprocess((payload: any) => JSON.parse(payload), statusSchema), + payload: v.pipe(v.any(), v.transform((payload: any) => JSON.parse(payload)), statusSchema), }); const stringStreamingEventSchema = v.object({ @@ -43,7 +43,7 @@ const stringStreamingEventSchema = v.object({ const notificationStreamingEventSchema = v.object({ ...baseStreamingEventSchema.entries, event: v.literal('notification'), - payload: z.preprocess((payload: any) => JSON.parse(payload), notificationSchema), + payload: v.pipe(v.any(), v.transform((payload: any) => JSON.parse(payload)), notificationSchema), }); const emptyStreamingEventSchema = v.object({ @@ -54,37 +54,37 @@ const emptyStreamingEventSchema = v.object({ const conversationStreamingEventSchema = v.object({ ...baseStreamingEventSchema.entries, event: v.literal('conversation'), - payload: z.preprocess((payload: any) => JSON.parse(payload), conversationSchema), + payload: v.pipe(v.any(), v.transform((payload: any) => JSON.parse(payload)), conversationSchema), }); const announcementStreamingEventSchema = v.object({ ...baseStreamingEventSchema.entries, event: v.literal('announcement'), - payload: z.preprocess((payload: any) => JSON.parse(payload), announcementSchema), + payload: v.pipe(v.any(), v.transform((payload: any) => JSON.parse(payload)), announcementSchema), }); const announcementReactionStreamingEventSchema = v.object({ ...baseStreamingEventSchema.entries, event: v.literal('announcement.reaction'), - payload: z.preprocess((payload: any) => JSON.parse(payload), announcementReactionSchema), + payload: v.pipe(v.any(), v.transform((payload: any) => JSON.parse(payload)), announcementReactionSchema), }); const chatUpdateStreamingEventSchema = v.object({ ...baseStreamingEventSchema.entries, event: v.literal('chat_update'), - payload: z.preprocess((payload: any) => JSON.parse(payload), chatSchema), + payload: v.pipe(v.any(), v.transform((payload: any) => JSON.parse(payload)), chatSchema), }); const followRelationshipsUpdateStreamingEventSchema = v.object({ ...baseStreamingEventSchema.entries, event: v.literal('follow_relationships_update'), - payload: z.preprocess((payload: any) => JSON.parse(payload), followRelationshipUpdateSchema), + payload: v.pipe(v.any(), v.transform((payload: any) => JSON.parse(payload)), followRelationshipUpdateSchema), }); const respondStreamingEventSchema = v.object({ ...baseStreamingEventSchema.entries, event: v.literal('respond'), - payload: z.preprocess((payload: any) => JSON.parse(payload), v.object({ + payload: v.pipe(v.any(), v.transform((payload: any) => JSON.parse(payload)), v.object({ type: v.string(), result: v.picklist(['success', 'ignored', 'error']), })), @@ -93,26 +93,30 @@ const respondStreamingEventSchema = v.object({ const markerStreamingEventSchema = v.object({ ...baseStreamingEventSchema.entries, event: v.literal('marker'), - payload: z.preprocess((payload: any) => JSON.parse(payload), markersSchema), + payload: v.pipe(v.any(), v.transform((payload: any) => JSON.parse(payload)), markersSchema), }); /** @see {@link https://docs.joinmastodon.org/methods/streaming/#events} */ -const streamingEventSchema: z.ZodType = z.preprocess((event: any) => ({ - ...event, - event: event.event?.replace(/^pleroma:/, ''), -}), v.variant('event', [ - statusStreamingEventSchema, - stringStreamingEventSchema, - notificationStreamingEventSchema, - emptyStreamingEventSchema, - conversationStreamingEventSchema, - announcementStreamingEventSchema, - announcementReactionStreamingEventSchema, - chatUpdateStreamingEventSchema, - followRelationshipsUpdateStreamingEventSchema, - respondStreamingEventSchema, - markerStreamingEventSchema, -])) as any; +const streamingEventSchema: v.BaseSchema> = v.pipe( + v.any(), + v.transform((event: any) => ({ + ...event, + event: event.event?.replace(/^pleroma:/, ''), + })), + v.variant('event', [ + statusStreamingEventSchema, + stringStreamingEventSchema, + notificationStreamingEventSchema, + emptyStreamingEventSchema, + conversationStreamingEventSchema, + announcementStreamingEventSchema, + announcementReactionStreamingEventSchema, + chatUpdateStreamingEventSchema, + followRelationshipsUpdateStreamingEventSchema, + respondStreamingEventSchema, + markerStreamingEventSchema, + ]), +) as any; type StreamingEvent = v.InferOutput< | typeof statusStreamingEventSchema diff --git a/packages/pl-api/lib/entities/suggestion.ts b/packages/pl-api/lib/entities/suggestion.ts index c45513455..9b9f12168 100644 --- a/packages/pl-api/lib/entities/suggestion.ts +++ b/packages/pl-api/lib/entities/suggestion.ts @@ -3,37 +3,41 @@ import * as v from 'valibot'; import { accountSchema } from './account'; /** @see {@link https://docs.joinmastodon.org/entities/Suggestion} */ -const suggestionSchema = z.preprocess((suggestion: any) => { +const suggestionSchema = v.pipe( + v.any(), + v.transform((suggestion: any) => { /** * Support `/api/v1/suggestions` * @see {@link https://docs.joinmastodon.org/methods/suggestions/#v1} */ - if (!suggestion) return null; + if (!suggestion) return null; - if (suggestion?.acct) return { - source: 'staff', - sources: ['featured'], - account: suggestion, - }; + if (suggestion?.acct) return { + source: 'staff', + sources: ['featured'], + account: suggestion, + }; - if (!suggestion.sources) { - suggestion.sources = []; - switch (suggestion.source) { - case 'staff': - suggestion.sources.push('staff'); - break; - case 'global': - suggestion.sources.push('most_interactions'); - break; + if (!suggestion.sources) { + suggestion.sources = []; + switch (suggestion.source) { + case 'staff': + suggestion.sources.push('staff'); + break; + case 'global': + suggestion.sources.push('most_interactions'); + break; + } } - } - return suggestion; -}, v.object({ - source: v.fallback(v.nullable(v.string()), null), - sources: v.fallback(v.array(v.string()), []), - account: accountSchema, -})); + return suggestion; + }), + v.object({ + source: v.fallback(v.nullable(v.string()), null), + sources: v.fallback(v.array(v.string()), []), + account: accountSchema, + }), +); type Suggestion = v.InferOutput; diff --git a/packages/pl-api/lib/entities/tag.ts b/packages/pl-api/lib/entities/tag.ts index 60fe97ce7..c1518bbde 100644 --- a/packages/pl-api/lib/entities/tag.ts +++ b/packages/pl-api/lib/entities/tag.ts @@ -8,7 +8,7 @@ const historySchema = v.object({ /** @see {@link https://docs.joinmastodon.org/entities/tag} */ const tagSchema = v.object({ - name: z.string().min(1), + name: v.pipe(v.string(), v.minLength(1)), url: v.fallback(v.pipe(v.string(), v.url()), ''), history: v.fallback(v.nullable(historySchema), null), following: v.fallback(v.optional(v.boolean()), undefined), diff --git a/packages/pl-api/lib/entities/translation.ts b/packages/pl-api/lib/entities/translation.ts index 40eeee6a0..07ffa270d 100644 --- a/packages/pl-api/lib/entities/translation.ts +++ b/packages/pl-api/lib/entities/translation.ts @@ -15,27 +15,31 @@ const translationMediaAttachment = v.object({ }); /** @see {@link https://docs.joinmastodon.org/entities/Translation/} */ -const translationSchema = z.preprocess((translation: any) => { +const translationSchema = v.pipe( + v.any(), + v.transform((translation: any) => { /** * handle Akkoma * @see {@link https://akkoma.dev/AkkomaGang/akkoma/src/branch/develop/lib/pleroma/web/mastodon_api/controllers/status_controller.ex#L504} */ - if (translation?.text) return { - content: translation.text, - detected_source_language: translation.detected_language, - provider: '', - }; + if (translation?.text) return { + content: translation.text, + detected_source_language: translation.detected_language, + provider: '', + }; - return translation; -}, v.object({ - id: v.fallback(v.nullable(v.string()), null), - content: v.fallback(v.string(), ''), - spoiler_text: v.fallback(v.string(), ''), - poll: v.fallback(v.optional(translationPollSchema), undefined), - media_attachments: filteredArray(translationMediaAttachment), - detected_source_language: v.string(), - provider: v.string(), -})); + return translation; + }), + v.object({ + id: v.fallback(v.nullable(v.string()), null), + content: v.fallback(v.string(), ''), + spoiler_text: v.fallback(v.string(), ''), + poll: v.fallback(v.optional(translationPollSchema), undefined), + media_attachments: filteredArray(translationMediaAttachment), + detected_source_language: v.string(), + provider: v.string(), + }), +); type Translation = v.InferOutput; diff --git a/packages/pl-api/lib/entities/trends-link.ts b/packages/pl-api/lib/entities/trends-link.ts index f43e73f0c..0ed03dbd2 100644 --- a/packages/pl-api/lib/entities/trends-link.ts +++ b/packages/pl-api/lib/entities/trends-link.ts @@ -4,25 +4,29 @@ import { blurhashSchema } from './media-attachment'; import { historySchema } from './tag'; /** @see {@link https://docs.joinmastodon.org/entities/PreviewCard/#trends-link} */ -const trendsLinkSchema = z.preprocess((link: any) => ({ ...link, id: link.url }), v.object({ - id: v.fallback(v.string(), ''), - url: v.fallback(v.pipe(v.string(), v.url()), ''), - title: v.fallback(v.string(), ''), - description: v.fallback(v.string(), ''), - type: v.fallback(v.picklist(['link', 'photo', 'video', 'rich']), 'link'), - author_name: v.fallback(v.string(), ''), - author_url: v.fallback(v.string(), ''), - provider_name: v.fallback(v.string(), ''), - provider_url: v.fallback(v.string(), ''), - html: v.fallback(v.string(), ''), - width: v.fallback(v.nullable(v.number()), null), - height: v.fallback(v.nullable(v.number()), null), - image: v.fallback(v.nullable(v.string()), null), - image_description: v.fallback(v.nullable(v.string()), null), - embed_url: v.fallback(v.string(), ''), - blurhash: v.fallback(v.nullable(blurhashSchema), null), - history: v.fallback(v.nullable(historySchema), null), -})); +const trendsLinkSchema = v.pipe( + v.any(), + v.transform((link: any) => ({ ...link, id: link.url })), + v.object({ + id: v.fallback(v.string(), ''), + url: v.fallback(v.pipe(v.string(), v.url()), ''), + title: v.fallback(v.string(), ''), + description: v.fallback(v.string(), ''), + type: v.fallback(v.picklist(['link', 'photo', 'video', 'rich']), 'link'), + author_name: v.fallback(v.string(), ''), + author_url: v.fallback(v.string(), ''), + provider_name: v.fallback(v.string(), ''), + provider_url: v.fallback(v.string(), ''), + html: v.fallback(v.string(), ''), + width: v.fallback(v.nullable(v.number()), null), + height: v.fallback(v.nullable(v.number()), null), + image: v.fallback(v.nullable(v.string()), null), + image_description: v.fallback(v.nullable(v.string()), null), + embed_url: v.fallback(v.string(), ''), + blurhash: v.fallback(v.nullable(blurhashSchema), null), + history: v.fallback(v.nullable(historySchema), null), + }), +); type TrendsLink = v.InferOutput; diff --git a/packages/pl-api/lib/entities/utils.ts b/packages/pl-api/lib/entities/utils.ts index 4bbadbc5c..eaee8fe94 100644 --- a/packages/pl-api/lib/entities/utils.ts +++ b/packages/pl-api/lib/entities/utils.ts @@ -4,14 +4,16 @@ import * as v from 'valibot'; const dateSchema = z.string().datetime({ offset: true }).catch(new Date().toUTCString()); /** Validates individual items in an array, dropping any that aren't valid. */ -const filteredArray = (schema: T) => - z.any().array().catch([]) - .transform((arr) => ( +const filteredArray = (schema: v.BaseSchema>) => + v.pipe( + v.fallback(v.array(v.any()), []), + v.transform((arr) => ( arr.map((item) => { - const parsed = schema.safeParse(item); - return parsed.success ? parsed.data : undefined; - }).filter((item): item is v.InferOutput => Boolean(item)) - )); + const parsed = v.safeParse(schema, item); + return parsed.success ? parsed.output : undefined; + }).filter((item): item is T => Boolean(item)) + )), + ); /** Validates the string as an emoji. */ const emojiSchema = v.pipe(v.string(), v.emoji()); diff --git a/packages/pl-fe/src/api/hooks/accounts/useRelationship.ts b/packages/pl-fe/src/api/hooks/accounts/useRelationship.ts index d47f3e68e..5f3749014 100644 --- a/packages/pl-fe/src/api/hooks/accounts/useRelationship.ts +++ b/packages/pl-fe/src/api/hooks/accounts/useRelationship.ts @@ -1,4 +1,4 @@ -import { z } from 'zod'; +import * as v from 'valibot'; import { Entities } from 'pl-fe/entity-store/entities'; import { useEntity } from 'pl-fe/entity-store/hooks'; @@ -19,7 +19,7 @@ const useRelationship = (accountId: string | undefined, opts: UseRelationshipOpt () => client.accounts.getRelationships([accountId!]), { enabled: enabled && !!accountId, - schema: z.any().transform(arr => arr[0]), + schema: v.pipe(v.any(), v.transform(arr => arr[0])), }, ); diff --git a/packages/pl-fe/src/api/hooks/groups/useDemoteGroupMember.ts b/packages/pl-fe/src/api/hooks/groups/useDemoteGroupMember.ts index 1a1e4d81f..7d51d6238 100644 --- a/packages/pl-fe/src/api/hooks/groups/useDemoteGroupMember.ts +++ b/packages/pl-fe/src/api/hooks/groups/useDemoteGroupMember.ts @@ -1,4 +1,4 @@ -import { z } from 'zod'; +import * as v from 'valibot'; import { Entities } from 'pl-fe/entity-store/entities'; import { useCreateEntity } from 'pl-fe/entity-store/hooks'; @@ -13,7 +13,7 @@ const useDemoteGroupMember = (group: Pick, groupMember: Pick client.experimental.groups.demoteGroupUsers(group.id, account_ids, role), - { schema: z.any().transform((arr) => arr[0]), transform: normalizeGroupMember }, + { schema: v.pipe(v.any(), v.transform(arr => arr[0])), transform: normalizeGroupMember }, ); return createEntity; diff --git a/packages/pl-fe/src/api/hooks/groups/useGroupRelationship.ts b/packages/pl-fe/src/api/hooks/groups/useGroupRelationship.ts index 1241f1d5d..9f77f5af4 100644 --- a/packages/pl-fe/src/api/hooks/groups/useGroupRelationship.ts +++ b/packages/pl-fe/src/api/hooks/groups/useGroupRelationship.ts @@ -1,4 +1,4 @@ -import { z } from 'zod'; +import * as v from 'valibot'; import { Entities } from 'pl-fe/entity-store/entities'; import { useEntity } from 'pl-fe/entity-store/hooks'; @@ -14,7 +14,7 @@ const useGroupRelationship = (groupId: string | undefined) => { () => client.experimental.groups.getGroupRelationships([groupId!]), { enabled: !!groupId, - schema: z.any().transform(arr => arr[0]), + schema: v.pipe(v.any(), v.transform(arr => arr[0])), }, ); diff --git a/packages/pl-fe/src/api/hooks/groups/usePromoteGroupMember.ts b/packages/pl-fe/src/api/hooks/groups/usePromoteGroupMember.ts index 641d77922..5824e3a46 100644 --- a/packages/pl-fe/src/api/hooks/groups/usePromoteGroupMember.ts +++ b/packages/pl-fe/src/api/hooks/groups/usePromoteGroupMember.ts @@ -1,4 +1,4 @@ -import { z } from 'zod'; +import * as v from 'valibot'; import { Entities } from 'pl-fe/entity-store/entities'; import { useCreateEntity } from 'pl-fe/entity-store/hooks'; @@ -13,7 +13,7 @@ const usePromoteGroupMember = (group: Pick, groupMember: Pick client.experimental.groups.promoteGroupUsers(group.id, account_ids, role), - { schema: z.any().transform((arr) => arr[0]), transform: normalizeGroupMember }, + { schema: v.pipe(v.any(), v.transform(arr => arr[0])), transform: normalizeGroupMember }, ); return createEntity; diff --git a/packages/pl-fe/src/entity-store/hooks/types.ts b/packages/pl-fe/src/entity-store/hooks/types.ts index 95a4e0baf..64b1e9e8f 100644 --- a/packages/pl-fe/src/entity-store/hooks/types.ts +++ b/packages/pl-fe/src/entity-store/hooks/types.ts @@ -1,7 +1,7 @@ import type { Entity } from '../types'; -import type z from 'zod'; +import type { BaseSchema, BaseIssue } from 'valibot'; -type EntitySchema = z.ZodType; +type EntitySchema = BaseSchema>; /** * Tells us where to find/store the entity in the cache. diff --git a/packages/pl-fe/src/entity-store/hooks/useBatchedEntities.ts b/packages/pl-fe/src/entity-store/hooks/useBatchedEntities.ts index 968ed38b3..746452753 100644 --- a/packages/pl-fe/src/entity-store/hooks/useBatchedEntities.ts +++ b/packages/pl-fe/src/entity-store/hooks/useBatchedEntities.ts @@ -54,7 +54,7 @@ const useBatchedEntities = ( dispatch(entitiesFetchRequest(entityType, listKey)); try { const response = await entityFn(filteredIds); - const entities = filteredArray(schema).parse(response); + const entities = v.parse(filteredArray(schema), response); dispatch(entitiesFetchSuccess(entities, entityType, listKey, 'end', { next: null, prev: null, diff --git a/packages/pl-fe/src/entity-store/hooks/useCreateEntity.ts b/packages/pl-fe/src/entity-store/hooks/useCreateEntity.ts index 9ed369471..68ede31e7 100644 --- a/packages/pl-fe/src/entity-store/hooks/useCreateEntity.ts +++ b/packages/pl-fe/src/entity-store/hooks/useCreateEntity.ts @@ -32,7 +32,7 @@ const useCreateEntity = => { const result = await setPromise(entityFn(data)); const schema = opts.schema || z.custom(); - let entity: TEntity | TTransformedEntity = schema.parse(result); + let entity: TEntity | TTransformedEntity = v.parse(schema, result); if (opts.transform) entity = opts.transform(entity); // TODO: optimistic updating diff --git a/packages/pl-fe/src/entity-store/hooks/useEntities.ts b/packages/pl-fe/src/entity-store/hooks/useEntities.ts index 071b87063..30629a0da 100644 --- a/packages/pl-fe/src/entity-store/hooks/useEntities.ts +++ b/packages/pl-fe/src/entity-store/hooks/useEntities.ts @@ -1,5 +1,5 @@ import { useEffect } from 'react'; -import z from 'zod'; +import * as v from 'valibot'; import { useAppDispatch } from 'pl-fe/hooks/useAppDispatch'; import { useAppSelector } from 'pl-fe/hooks/useAppSelector'; @@ -64,7 +64,7 @@ const useEntities = { try { const response = await setPromise(entityFn()); - let entity: TEntity | TTransformedEntity = schema.parse(response); + let entity: TEntity | TTransformedEntity = v.parse(schema, response); if (opts.transform) entity = opts.transform(entity); dispatch(importEntities([entity], entityType)); } catch (e) { diff --git a/packages/pl-fe/src/entity-store/hooks/useEntityLookup.ts b/packages/pl-fe/src/entity-store/hooks/useEntityLookup.ts index bf598d8b8..683077c97 100644 --- a/packages/pl-fe/src/entity-store/hooks/useEntityLookup.ts +++ b/packages/pl-fe/src/entity-store/hooks/useEntityLookup.ts @@ -36,7 +36,7 @@ const useEntityLookup = { try { const response = await setPromise(entityFn()); - const entity = schema.parse(response); + const entity = v.parse(schema, response); const transformedEntity = opts.transform ? opts.transform(entity) : entity; setFetchedEntity(transformedEntity as TTransformedEntity); dispatch(importEntities([transformedEntity], entityType)); diff --git a/packages/pl-fe/src/schemas/utils.ts b/packages/pl-fe/src/schemas/utils.ts index 775a349c6..ed75567ee 100644 --- a/packages/pl-fe/src/schemas/utils.ts +++ b/packages/pl-fe/src/schemas/utils.ts @@ -1,17 +1,18 @@ import * as v from 'valibot'; -import z from 'zod'; import type { CustomEmoji } from 'pl-api'; /** Validates individual items in an array, dropping any that aren't valid. */ -const filteredArray = (schema: T) => - z.any().array().catch([]) - .transform((arr) => ( +const filteredArray = (schema: v.BaseSchema>) => + v.pipe( + v.fallback(v.array(v.any()), []), + v.transform((arr) => ( arr.map((item) => { - const parsed = schema.safeParse(item); - return parsed.success ? parsed.data : undefined; - }).filter((item): item is z.infer => Boolean(item)) - )); + const parsed = v.safeParse(schema, item); + return parsed.success ? parsed.output : undefined; + }).filter((item): item is T => Boolean(item)) + )), + ); /** Map a list of CustomEmoji to their shortcodes. */ const makeCustomEmojiMap = (customEmojis: CustomEmoji[]) =>