Files
web_sport/packages/validation/src/taxonomy-admin.ts
T
2026-08-13 23:20:23 +07:00

76 lines
2.6 KiB
TypeScript

import { z } from 'zod';
import { LOCALES } from '@sport/types';
import { anyIdSchema, slugSchema } from './common';
import { offsetPageQuerySchema } from './pagination';
/**
* Per-locale copy for a taxonomy row.
*
* A partial record, like products and content: publishing an English-only
* brand is legitimate, and demanding every locale would mean inventing
* Vietnamese copy before anything could be saved.
*/
const translationSchema = z.object({
name: z.string().trim().min(1).max(160),
slug: slugSchema,
description: z.string().trim().max(2000).nullish(),
metaTitle: z.string().trim().max(255).nullish(),
metaDescription: z.string().trim().max(500).nullish(),
});
const baseSchema = {
/** The fallback name, used when a locale has no translation. */
name: z.string().trim().min(1).max(160),
slug: slugSchema,
description: z.string().trim().max(2000).nullish(),
isActive: z.boolean().default(true),
metaTitle: z.string().trim().max(255).nullish(),
metaDescription: z.string().trim().max(500).nullish(),
translations: z.partialRecord(z.enum(LOCALES), translationSchema).default({}),
};
export const brandInputSchema = z.object({
...baseSchema,
logoId: anyIdSchema.nullish(),
});
export const categoryInputSchema = z.object({
...baseSchema,
/**
* Null means a root category. The service rejects a parent that is the
* category itself or one of its own descendants — that would detach the
* subtree from the tree and orphan every product under it.
*/
parentId: anyIdSchema.nullish(),
position: z.int().min(0).max(9999).default(0),
imageId: anyIdSchema.nullish(),
});
export const collectionInputSchema = z
.object({
...baseSchema,
type: z.enum(['MANUAL', 'AUTOMATED']).default('MANUAL'),
bannerId: anyIdSchema.nullish(),
startsAt: z.iso.datetime().nullish(),
endsAt: z.iso.datetime().nullish(),
position: z.int().min(0).max(9999).default(0),
})
.refine(
(value) =>
!value.startsAt || !value.endsAt || new Date(value.startsAt) < new Date(value.endsAt),
{ message: 'The end date must be after the start date', path: ['endsAt'] },
);
export const taxonomyListQuerySchema = offsetPageQuerySchema.extend({
q: z.string().trim().max(120).optional(),
/** Admin default is "everything"; the storefront only ever sees active rows. */
activeOnly: z.coerce.boolean().optional(),
});
export type BrandInput = z.output<typeof brandInputSchema>;
export type CategoryInput = z.output<typeof categoryInputSchema>;
export type CollectionInput = z.output<typeof collectionInputSchema>;
export type TaxonomyListQuery = z.output<typeof taxonomyListQuerySchema>;