Improve schema and update tags

- Make constants in schema explicit
- Enumerate tag-categories
- More i18n utilities
- Better anonymous user support without special field
- Remove most tuples and unchecked type-casting
This commit is contained in:
Bad Manners 2024-07-26 15:48:57 -03:00
parent 579e5879e1
commit 17ef8c652c
34 changed files with 223 additions and 221 deletions

View file

@ -16,6 +16,8 @@ export const WEBSITE_LIST = [
] as const;
export const GAME_PLATFORMS = ["web", "windows", "linux", "macos", "android", "ios"] as const;
export const DEFAULT_LANG = "eng";
export const DEFAULT_AUTHOR = "bad-manners";
export const ANONYMOUS_USER = "anonymous";
// Validators
@ -27,13 +29,34 @@ const inkbunnyPostUrlRegex = /^(?:https:\/\/)(?:www\.)?inkbunny\.net\/s\/([1-9]\
const sofurryPostUrlRegex = /^(?:https:\/\/)www\.sofurry\.com\/view\/([1-9]\d*)\/?$/;
const mastodonPostUrlRegex = /^(?:https:\/\/)((?:[a-zA-Z0-9_-]+\.)+[a-z]+)\/@([a-zA-Z][a-zA-Z0-9_-]+)\/([1-9]\d*)\/?$/;
const refineAuthors = (value: { id: any } | any[]) => "id" in value || value.length > 0;
const refineCopyrightedCharacters = (value: Record<string, any>) => !("" in value) || Object.keys(value).length == 1;
const refineAuthors = [
(value: { id: any } | any[]) => "id" in value || value.length > 0,
`"authors" cannot be empty`,
] as const;
const refineCopyrightedCharacters = [
(value: Record<string, any>) => !("" in value) || Object.keys(value).length == 1,
`"copyrightedCharacters" cannot mix empty catch-all key with other keys`,
] as const;
// Transformers
export const adjustDateForUTCOffset = (date: Date) =>
new Date(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(), 0, 0, 0);
export const parseMastodonPostUrl = (url: string, ctx: z.RefinementCtx) => {
const match = mastodonPostUrlRegex.exec(url);
if (!match) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `"mastodon" post contains an invalid URL`,
});
return z.NEVER;
}
return {
instance: match[1]!,
user: match[2]!,
postId: match[3]!,
};
};
// Types
@ -46,27 +69,15 @@ const mastodonPost = z
user: z.string(),
postId: z.string(),
})
.or(
z.string().transform((mastodonPost, ctx) => {
const match = mastodonPostUrlRegex.exec(mastodonPost);
if (!match) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `"mastodon" post contains an invalid URL`,
});
return z.NEVER;
}
return {
instance: match[1],
user: match[2],
postId: match[3],
};
}),
);
.or(z.string().transform(parseMastodonPostUrl));
const authors = z
.union([reference("users"), z.array(reference("users"))])
.default(DEFAULT_AUTHOR)
.refine(...refineAuthors);
const copyrightedCharacters = z
.record(z.string(), reference("users"))
.default({})
.refine(refineCopyrightedCharacters, `"copyrightedCharacters" cannot mix empty catch-all key with other keys`);
.refine(...refineCopyrightedCharacters);
export type Lang = z.output<typeof lang>;
export type Website = z.infer<typeof website>;
@ -89,10 +100,7 @@ const storiesCollection = defineCollection({
pubDate: z.date().transform(adjustDateForUTCOffset).optional(),
isDraft: z.boolean().default(false),
shortTitle: z.string().optional(),
authors: z
.union([reference("users"), z.array(reference("users"))])
.default("bad-manners")
.refine(refineAuthors, `"authors" cannot be empty`),
authors,
summary: z.string().optional(),
thumbnail: image().optional(),
thumbnailWidth: z.number().int().optional(),
@ -108,13 +116,14 @@ const storiesCollection = defineCollection({
relatedGames: z.array(reference("games")).default([]),
posts: z
.object({
eka: z.string().regex(ekaPostUrlRegex).optional(),
furaffinity: z.string().regex(furaffinityPostUrlRegex).optional(),
weasyl: z.string().regex(weasylPostUrlRegex).optional(),
inkbunny: z.string().regex(inkbunnyPostUrlRegex).optional(),
sofurry: z.string().regex(sofurryPostUrlRegex).optional(),
mastodon: mastodonPost.optional(),
eka: z.string().regex(ekaPostUrlRegex),
furaffinity: z.string().regex(furaffinityPostUrlRegex),
weasyl: z.string().regex(weasylPostUrlRegex),
inkbunny: z.string().regex(inkbunnyPostUrlRegex),
sofurry: z.string().regex(sofurryPostUrlRegex),
mastodon: mastodonPost,
})
.partial()
.default({}),
}),
});
@ -131,10 +140,7 @@ const gamesCollection = defineCollection({
// Optional
pubDate: z.date().transform(adjustDateForUTCOffset).optional(),
isDraft: z.boolean().default(false),
authors: z
.union([reference("users"), z.array(reference("users"))])
.default("bad-manners")
.refine(refineAuthors, `"authors" cannot be empty`),
authors,
thumbnail: image().optional(),
thumbnailWidth: z.number().int().optional(),
thumbnailHeight: z.number().int().optional(),
@ -146,13 +152,14 @@ const gamesCollection = defineCollection({
relatedGames: z.array(reference("games")).default([]),
posts: z
.object({
eka: z.string().regex(ekaPostUrlRegex).optional(),
furaffinity: z.string().regex(furaffinityPostUrlRegex).optional(),
weasyl: z.string().regex(weasylPostUrlRegex).optional(),
inkbunny: z.string().regex(inkbunnyPostUrlRegex).optional(),
sofurry: z.string().regex(sofurryPostUrlRegex).optional(),
mastodon: mastodonPost.optional(),
eka: z.string().regex(ekaPostUrlRegex),
furaffinity: z.string().regex(furaffinityPostUrlRegex),
weasyl: z.string().regex(weasylPostUrlRegex),
inkbunny: z.string().regex(inkbunnyPostUrlRegex),
sofurry: z.string().regex(sofurryPostUrlRegex),
mastodon: mastodonPost,
})
.partial()
.default({}),
}),
});
@ -169,9 +176,8 @@ const usersCollection = defineCollection({
links: z.record(website, z.union([z.string().url(), z.tuple([z.string().url(), z.string()])])),
// Optional
preferredLink: website.nullish(),
nameLang: z.record(lang, z.string()).default({}),
lang: z.record(lang, z.string()).default({}),
avatar: image().optional(),
isAnonymous: z.boolean().default(false),
})
.refine(
({ links, preferredLink }) => !preferredLink || preferredLink in links,
@ -187,7 +193,7 @@ const seriesCollection = defineCollection({
schema: z.object({
// Required
name: z.string(),
url: z.string().regex(/^(\/[a-z0-9_-]+)*\/?$/, `"url" must be a local URL`),
url: z.string().regex(/^(\/[a-z0-9_-]+)+\/?$/, `"url" must be a local URL`),
}),
});
@ -199,10 +205,7 @@ const tagCategoriesCollection = defineCollection({
index: z.number().int(),
tags: z.array(
z.object({
name: z.union([
z.string(),
z.intersection(z.object({ [DEFAULT_LANG]: z.string() }), z.record(lang, z.string())),
]),
name: z.union([z.string(), z.object({ [DEFAULT_LANG]: z.string() }).and(z.record(lang, z.string()))]),
description: z.string().optional(),
related: z.array(z.string()).optional(),
}),