From 5243ef3528cd2a8ac1fa5a9c99e6b88972892fad Mon Sep 17 00:00:00 2001 From: Arjan Singh Date: Tue, 16 Sep 2025 20:44:31 -0700 Subject: [PATCH] [feat] add asset metadata validation utilities --- .../assets/composables/useAssetBrowser.ts | 14 ++-- src/platform/assets/schemas/assetSchema.ts | 2 +- .../assets/utils/assetMetadataUtils.ts | 27 ++++++++ .../assets/utils/assetMetadataUtils.test.ts | 65 +++++++++++++++++++ 4 files changed, 103 insertions(+), 5 deletions(-) create mode 100644 src/platform/assets/utils/assetMetadataUtils.ts create mode 100644 tests-ui/tests/platform/assets/utils/assetMetadataUtils.test.ts diff --git a/src/platform/assets/composables/useAssetBrowser.ts b/src/platform/assets/composables/useAssetBrowser.ts index 46874b40a..22af0cf4e 100644 --- a/src/platform/assets/composables/useAssetBrowser.ts +++ b/src/platform/assets/composables/useAssetBrowser.ts @@ -3,6 +3,10 @@ import { computed, ref } from 'vue' import { t } from '@/i18n' import type { UUID } from '@/lib/litegraph/src/utils/uuid' import type { AssetItem } from '@/platform/assets/schemas/assetSchema' +import { + getAssetBaseModel, + getAssetDescription +} from '@/platform/assets/utils/assetMetadataUtils' import { formatSize } from '@/utils/formatUtil' type AssetBadge = { @@ -37,7 +41,7 @@ export function useAssetBrowser(assets: AssetItem[] = []) { // Extract description from metadata or create from tags const typeTag = asset.tags.find((tag) => tag !== 'models') const description = - asset.user_metadata?.description || + getAssetDescription(asset) || `${typeTag || t('assetBrowser.unknown')} model` // Format file size @@ -52,9 +56,10 @@ export function useAssetBrowser(assets: AssetItem[] = []) { } // Base model badge from metadata - if (asset.user_metadata?.base_model) { + const baseModel = getAssetBaseModel(asset) + if (baseModel) { badges.push({ - label: asset.user_metadata.base_model, + label: baseModel, type: 'base' }) } @@ -126,9 +131,10 @@ export function useAssetBrowser(assets: AssetItem[] = []) { const filterByQuery = (query: string) => (asset: AssetItem) => { if (!query) return true const lowerQuery = query.toLowerCase() + const description = getAssetDescription(asset) return ( asset.name.toLowerCase().includes(lowerQuery) || - asset.user_metadata?.description?.toLowerCase().includes(lowerQuery) || + (description && description.toLowerCase().includes(lowerQuery)) || asset.tags.some((tag) => tag.toLowerCase().includes(lowerQuery)) ) } diff --git a/src/platform/assets/schemas/assetSchema.ts b/src/platform/assets/schemas/assetSchema.ts index 1927cd9c2..fab41649a 100644 --- a/src/platform/assets/schemas/assetSchema.ts +++ b/src/platform/assets/schemas/assetSchema.ts @@ -12,7 +12,7 @@ const zAsset = z.object({ created_at: z.string(), updated_at: z.string(), last_access_time: z.string(), - user_metadata: z.record(z.any()).optional(), + user_metadata: z.record(z.unknown()).optional(), // API allows arbitrary key-value pairs preview_id: z.string().nullable().optional() }) diff --git a/src/platform/assets/utils/assetMetadataUtils.ts b/src/platform/assets/utils/assetMetadataUtils.ts new file mode 100644 index 000000000..2d32fa07f --- /dev/null +++ b/src/platform/assets/utils/assetMetadataUtils.ts @@ -0,0 +1,27 @@ +import type { AssetItem } from '@/platform/assets/schemas/assetSchema' + +/** + * Type-safe utilities for extracting metadata from assets + */ + +/** + * Safely extracts string description from asset metadata + * @param asset - The asset to extract description from + * @returns The description string or null if not present/not a string + */ +export function getAssetDescription(asset: AssetItem): string | null { + return typeof asset.user_metadata?.description === 'string' + ? asset.user_metadata.description + : null +} + +/** + * Safely extracts string base_model from asset metadata + * @param asset - The asset to extract base_model from + * @returns The base_model string or null if not present/not a string + */ +export function getAssetBaseModel(asset: AssetItem): string | null { + return typeof asset.user_metadata?.base_model === 'string' + ? asset.user_metadata.base_model + : null +} diff --git a/tests-ui/tests/platform/assets/utils/assetMetadataUtils.test.ts b/tests-ui/tests/platform/assets/utils/assetMetadataUtils.test.ts new file mode 100644 index 000000000..54551f595 --- /dev/null +++ b/tests-ui/tests/platform/assets/utils/assetMetadataUtils.test.ts @@ -0,0 +1,65 @@ +import { describe, expect, it } from 'vitest' + +import type { AssetItem } from '@/platform/assets/schemas/assetSchema' +import { + getAssetBaseModel, + getAssetDescription +} from '@/platform/assets/utils/assetMetadataUtils' + +describe('assetMetadataUtils', () => { + const mockAsset: AssetItem = { + id: 'test-id', + name: 'test-model', + asset_hash: 'hash123', + size: 1024, + mime_type: 'application/octet-stream', + tags: ['models', 'checkpoints'], + created_at: '2024-01-01T00:00:00Z', + updated_at: '2024-01-01T00:00:00Z', + last_access_time: '2024-01-01T00:00:00Z' + } + + describe('getAssetDescription', () => { + it('should return string description when present', () => { + const asset = { + ...mockAsset, + user_metadata: { description: 'A test model' } + } + expect(getAssetDescription(asset)).toBe('A test model') + }) + + it('should return null when description is not a string', () => { + const asset = { + ...mockAsset, + user_metadata: { description: 123 } + } + expect(getAssetDescription(asset)).toBeNull() + }) + + it('should return null when no metadata', () => { + expect(getAssetDescription(mockAsset)).toBeNull() + }) + }) + + describe('getAssetBaseModel', () => { + it('should return string base_model when present', () => { + const asset = { + ...mockAsset, + user_metadata: { base_model: 'SDXL' } + } + expect(getAssetBaseModel(asset)).toBe('SDXL') + }) + + it('should return null when base_model is not a string', () => { + const asset = { + ...mockAsset, + user_metadata: { base_model: 123 } + } + expect(getAssetBaseModel(asset)).toBeNull() + }) + + it('should return null when no metadata', () => { + expect(getAssetBaseModel(mockAsset)).toBeNull() + }) + }) +})