pl-api: Mostly finish migration to valibot

Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
marcin mikołajczak 2024-10-16 01:08:56 +02:00
parent 6633b28645
commit 2f7e149f75
44 changed files with 457 additions and 394 deletions

View file

@ -227,7 +227,7 @@ class PlApiClient {
}
}
#paginatedGet = async <T extends v.BaseSchema<any, any, any>>(input: URL | RequestInfo, body: RequestBody, schema: T): Promise<PaginatedResponse<v.InferOutput<T>>> => {
#paginatedGet = async <T>(input: URL | RequestInfo, body: RequestBody, schema: v.BaseSchema<any, T, v.BaseIssue<unknown>>): Promise<PaginatedResponse<T>> => {
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;

View file

@ -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);
}
}

View file

@ -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<typeof accountWithMovedAccountSchema> & WithMoved;
const accountSchema: z.ZodType<Account> = untypedAccountSchema as any;
const accountSchema: v.BaseSchema<any, Account, v.BaseIssue<unknown>> = 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<typeof untypedCredentialAccountSchema> & WithMoved;
const credentialAccountSchema: z.ZodType<CredentialAccount> = untypedCredentialAccountSchema as any;
const credentialAccountSchema: v.BaseSchema<any, CredentialAccount, v.BaseIssue<unknown>> = 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<typeof untypedMutedAccountSchema> & WithMoved;
const mutedAccountSchema: z.ZodType<MutedAccount> = untypedMutedAccountSchema as any;
const mutedAccountSchema: v.BaseSchema<any, MutedAccount, v.BaseIssue<unknown>> = untypedMutedAccountSchema as any;
export {
accountSchema,

View file

@ -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<typeof adminAccountSchema>;

View file

@ -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<typeof adminAnnouncementSchema>;

View file

@ -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,

View file

@ -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,
});

View file

@ -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<typeof adminRelaySchema>

View file

@ -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<typeof adminReportSchema>;

View file

@ -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),

View file

@ -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),

View file

@ -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<typeof emojiReactionSchema>;

View file

@ -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<typeof filterSchema>;

View file

@ -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<typeof groupMemberSchema>;

View file

@ -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),
});

View file

@ -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 === '<p></p>' ? '' : note).catch(''),
note: v.fallback(v.pipe(v.string(), v.transform(note => note === '<p></p>' ? '' : 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(), ''),

View file

@ -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<typeof instanceSchema>;

View file

@ -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),
});

View file

@ -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<typeof markerSchema>;

View file

@ -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<typeof mediaAttachmentSchema>;

View file

@ -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<typeof mentionSchema>;

View file

@ -84,25 +84,28 @@ const eventParticipationRequestNotificationSchema = v.object({
});
/** @see {@link https://docs.joinmastodon.org/entities/Notification/} */
const notificationSchema: z.ZodType<Notification> = 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<any, Notification, v.BaseIssue<unknown>> = 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

View file

@ -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<typeof oauthTokenSchema>;

View file

@ -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),

View file

@ -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<typeof ruleSchema>;

View file

@ -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<typeof scrobbleSchema>;

View file

@ -134,19 +134,19 @@ const preprocess = (status: any) => {
return status;
};
const statusSchema: z.ZodType<Status> = z.preprocess(preprocess, v.object({
const statusSchema: v.BaseSchema<any, Status, v.BaseIssue<unknown>> = 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<typeof baseStatusSchema> & {

View file

@ -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<StreamingEvent> = 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<any, StreamingEvent, v.BaseIssue<unknown>> = 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

View file

@ -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<typeof suggestionSchema>;

View file

@ -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),

View file

@ -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<typeof translationSchema>;

View file

@ -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<typeof trendsLinkSchema>;

View file

@ -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 = <T extends z.ZodTypeAny>(schema: T) =>
z.any().array().catch([])
.transform((arr) => (
const filteredArray = <T>(schema: v.BaseSchema<any, T, v.BaseIssue<unknown>>) =>
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<T> => 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());

View file

@ -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])),
},
);

View file

@ -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<Group, 'id'>, groupMember: Pick<GroupM
const { createEntity } = useCreateEntity(
[Entities.GROUP_MEMBERSHIPS, groupMember.id],
({ account_ids, role }: { account_ids: string[]; role: GroupRole }) => 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;

View file

@ -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])),
},
);

View file

@ -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<Group, 'id'>, groupMember: Pick<Group
const { createEntity } = useCreateEntity(
[Entities.GROUP_MEMBERSHIPS, groupMember.id],
({ account_ids, role }: { account_ids: string[]; role: GroupRole }) => 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;

View file

@ -1,7 +1,7 @@
import type { Entity } from '../types';
import type z from 'zod';
import type { BaseSchema, BaseIssue } from 'valibot';
type EntitySchema<TEntity extends Entity = Entity> = z.ZodType<TEntity, z.ZodTypeDef, any>;
type EntitySchema<TEntity extends Entity = Entity> = BaseSchema<any, TEntity, BaseIssue<unknown>>;
/**
* Tells us where to find/store the entity in the cache.

View file

@ -54,7 +54,7 @@ const useBatchedEntities = <TEntity extends Entity>(
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,

View file

@ -32,7 +32,7 @@ const useCreateEntity = <TEntity extends Entity = Entity, TTransformedEntity ext
): Promise<void> => {
const result = await setPromise(entityFn(data));
const schema = opts.schema || z.custom<TEntity>();
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

View file

@ -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 = <TEntity extends Entity, TTransformedEntity extends Entity =
dispatch(entitiesFetchRequest(entityType, listKey));
try {
const response = await req();
const entities = filteredArray(schema).parse(response);
const entities = v.parse(filteredArray(schema), response);
const transformedEntities = opts.transform && entities.map(opts.transform);
dispatch(entitiesFetchSuccess(transformedEntities || entities, entityType, listKey, pos, {

View file

@ -47,7 +47,7 @@ const useEntity = <TEntity extends Entity, TTransformedEntity extends Entity = T
const fetchEntity = async () => {
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) {

View file

@ -36,7 +36,7 @@ const useEntityLookup = <TEntity extends Entity, TTransformedEntity extends Enti
const fetchEntity = async () => {
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));

View file

@ -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 = <T extends z.ZodTypeAny>(schema: T) =>
z.any().array().catch([])
.transform((arr) => (
const filteredArray = <T>(schema: v.BaseSchema<any, T, v.BaseIssue<unknown>>) =>
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<T> => 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[]) =>