ditto/packages/ditto/schema.ts
2025-02-22 12:59:36 -06:00

86 lines
2.4 KiB
TypeScript

import ISO6391, { LanguageCode } from 'iso-639-1';
import { NSchema as n } from '@nostrify/nostrify';
import { z } from 'zod';
/** Validates individual items in an array, dropping any that aren't valid. */
function filteredArray<T extends z.ZodTypeAny>(schema: T) {
return z.any().array().catch([])
.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))
));
}
/** Parses a hashtag, eg `#yolo`. */
const hashtagSchema = z.string().regex(/^\w{1,30}$/);
/**
* Limits the length before trying to parse the URL.
* https://stackoverflow.com/a/417184/8811886
*/
const safeUrlSchema = z.string().max(2048).url();
/** https://github.com/colinhacks/zod/issues/1630#issuecomment-1365983831 */
const booleanParamSchema = z.enum(['true', 'false']).transform((value) => value === 'true');
/** Schema for `File` objects. */
const fileSchema = z.custom<File>((value) => value instanceof File);
const percentageSchema = z.coerce.number().int().gte(1).lte(100);
const languageSchema = z.string().transform<LanguageCode>((val, ctx) => {
val = val.toLowerCase();
if (!ISO6391.validate(val)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'Not a valid language in ISO-639-1 format',
});
return z.NEVER;
}
return val;
});
const localeSchema = z.string().transform<Intl.Locale>((val, ctx) => {
try {
return new Intl.Locale(val);
} catch {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'Invalid locale',
});
return z.NEVER;
}
});
/** White-space separated list of sizes, each in the format <number with up to 4 digits>x<number with up to 4 digits> or with "X" in upper case. */
const sizesSchema = z.string().refine((value) =>
value.split(' ').every((v) => /^[1-9]\d{0,3}[xX][1-9]\d{0,3}$/.test(v))
);
/** Ditto Cashu wallet */
const walletSchema = z.object({
pubkey_p2pk: n.id(),
mints: z.array(z.string().url()).nonempty().transform((val) => {
return [...new Set(val)];
}),
relays: z.array(z.string()).nonempty().transform((val) => {
return [...new Set(val)];
}),
/** Unit in sats */
balance: z.number(),
});
export {
booleanParamSchema,
fileSchema,
filteredArray,
hashtagSchema,
languageSchema,
localeSchema,
percentageSchema,
safeUrlSchema,
sizesSchema,
walletSchema,
};