This commit is contained in:
Your Name committed 2026-10-02 07:10:25 +07:00
1 parent 75770ca122
commit f258e3e176
16 files changed
+1207 -348

No files matched your search

+36
View File
@@ -98,9 +98,45 @@ export interface BomItem {
export interface ProductImage {
id: number
url: string
mobileUrl?: string | null
order: number
}
const OptionalProductImageMobileUrlSchema = z
.union([z.string(), z.null()])
.optional()
.transform((value) => {
if (value === undefined) return undefined
if (value === null) return null
const normalized = value.trim()
return normalized.length > 0 ? normalized : null
})
.refine((value) => value === undefined || value === null || !/^data:video\//i.test(value), 'Product image URL must reference an image')
.refine((value) => value === undefined || value === null || !/\.(?:mp4|webm|mov|ogv|ogg)(?:[?#]|$)/i.test(value), 'Product image URL must reference an image')
const ProductImageUrlSchema = z.string()
.trim()
.min(1, 'Product image URL is required')
.refine((value) => !/^data:video\//i.test(value), 'Product image URL must reference an image')
.refine((value) => !/\.(?:mp4|webm|mov|ogv|ogg)(?:[?#]|$)/i.test(value), 'Product image URL must reference an image')
export const ProductImageObjectInputSchema = z.object({
id: z.number().int().positive().optional(),
url: ProductImageUrlSchema,
mobileUrl: OptionalProductImageMobileUrlSchema,
order: z.number().int().optional(),
})
export const ProductImageInputSchema = z.union([
ProductImageUrlSchema.transform((url) => ({
url,
})),
ProductImageObjectInputSchema,
])
export type ProductImageInput = z.input<typeof ProductImageInputSchema>
export type NormalizedProductImageInput = z.output<typeof ProductImageInputSchema>
export type TranslatableString = string | Record<string, string>;
export interface AttributeDefinitionTranslation {
+47
View File
@@ -0,0 +1,47 @@
import { describe, expect, test } from 'bun:test'
import { ProductImageInputSchema } from './index'
describe('product image input contract', () => {
test('preserves the legacy string input shape', () => {
expect(ProductImageInputSchema.parse('/uploads/products/desktop.webp')).toEqual({
url: '/uploads/products/desktop.webp',
})
})
test('accepts a paired desktop and mobile image', () => {
expect(ProductImageInputSchema.parse({
id: 12,
url: '/uploads/products/desktop.webp',
mobileUrl: '/uploads/products/mobile.webp',
order: 2,
})).toEqual({
id: 12,
url: '/uploads/products/desktop.webp',
mobileUrl: '/uploads/products/mobile.webp',
order: 2,
})
})
test('normalizes an empty mobile image URL to null', () => {
expect(ProductImageInputSchema.parse({
url: '/uploads/products/desktop.webp',
mobileUrl: ' ',
}).mobileUrl).toBeNull()
})
test('rejects non-string image URLs without rejecting relative upload paths', () => {
expect(ProductImageInputSchema.safeParse({ url: 123 }).success).toBe(false)
expect(ProductImageInputSchema.safeParse({
url: '/uploads/products/desktop.webp',
mobileUrl: { path: '/uploads/products/mobile.webp' },
}).success).toBe(false)
})
test('rejects known video URLs in desktop and mobile image slots', () => {
expect(ProductImageInputSchema.safeParse('/uploads/product.mp4').success).toBe(false)
expect(ProductImageInputSchema.safeParse({
url: '/uploads/desktop.webp',
mobileUrl: '/uploads/mobile.webm?version=2',
}).success).toBe(false)
})
})