fix bug of save imageUrls
This commit is contained in:
@@ -38,6 +38,28 @@ describe('product image separation', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('recovers color name from product.color when validation stripped nested fields', () => {
|
||||
const db = serializeColorsJsonForDb(
|
||||
[{ imageUrl: 'data:image/jpeg;base64,xyz' }],
|
||||
'قرمز شرابی',
|
||||
);
|
||||
expect(db).toEqual([
|
||||
{
|
||||
name: 'قرمز شرابی',
|
||||
imageUrl: 'data:image/jpeg;base64,xyz',
|
||||
imageUrls: ['data:image/jpeg;base64,xyz'],
|
||||
},
|
||||
]);
|
||||
const entries = parseColorEntries(db, 'قرمز شرابی');
|
||||
expect(entries[0]?.imageUrls).toEqual(['data:image/jpeg;base64,xyz']);
|
||||
expect(entries.map((e) => e.name)).toEqual(['قرمز شرابی']);
|
||||
});
|
||||
|
||||
it('falls back to product.color when colorsJson rows are empty objects', () => {
|
||||
const entries = parseColorEntries([{}], 'قرمز شرابی');
|
||||
expect(entries).toEqual([{ name: 'قرمز شرابی', imageUrls: [] }]);
|
||||
});
|
||||
|
||||
it('does not merge color images into general list when only imageUrlsJson is set', () => {
|
||||
const entries = parseColorEntries([{ name: 'Red', imageUrl: 'https://cdn/r.jpg' }]);
|
||||
expect(parseGeneralImageUrls(['https://cdn/g.jpg'], entries)).toEqual([
|
||||
|
||||
@@ -28,8 +28,8 @@ export function parseColorEntries(
|
||||
return name ? [{ name, imageUrls: [] }] : [];
|
||||
}
|
||||
|
||||
return colorsJson
|
||||
.map((item): ProductColorEntry | null => {
|
||||
const parsed = colorsJson
|
||||
.map((item, index): ProductColorEntry | null => {
|
||||
if (typeof item === 'string') {
|
||||
const name = item.trim();
|
||||
return name ? { name, imageUrls: [] } : null;
|
||||
@@ -40,8 +40,7 @@ export function parseColorEntries(
|
||||
imageUrl?: unknown;
|
||||
imageUrls?: unknown;
|
||||
};
|
||||
const name = String(raw.name ?? '').trim();
|
||||
if (!name) return null;
|
||||
let name = String(raw.name ?? '').trim();
|
||||
const fromArray = normalizeUrlList(raw.imageUrls);
|
||||
const legacy =
|
||||
typeof raw.imageUrl === 'string' && raw.imageUrl.trim()
|
||||
@@ -55,12 +54,21 @@ export function parseColorEntries(
|
||||
: legacy
|
||||
? [legacy]
|
||||
: [];
|
||||
if (!name && imageUrls.length > 0) {
|
||||
name = fallbackColor?.trim() || `رنگ ${index + 1}`;
|
||||
}
|
||||
if (!name) return null;
|
||||
const imageUrl = imageUrls[0];
|
||||
return { name, imageUrl, imageUrls };
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.filter((entry): entry is ProductColorEntry => Boolean(entry));
|
||||
|
||||
if (parsed.length > 0) return parsed;
|
||||
|
||||
const fallbackName = fallbackColor?.trim();
|
||||
return fallbackName ? [{ name: fallbackName, imageUrls: [] }] : [];
|
||||
}
|
||||
|
||||
/** تصاویر عمومی محصول — فقط از imageUrlsJson، بدون merge با رنگها */
|
||||
|
||||
@@ -50,22 +50,65 @@ export function transformColorsJson(value: unknown): ProductColorJsonItemDto[] |
|
||||
return out;
|
||||
}
|
||||
|
||||
/** ذخیره در Prisma — imageUrl + imageUrls برای هر رنگ */
|
||||
export function serializeColorsJsonForDb(raw: unknown): unknown[] | undefined {
|
||||
const entries = transformColorsJson(raw);
|
||||
if (entries === undefined) return undefined;
|
||||
return entries.map((entry) => {
|
||||
const merged: string[] = [];
|
||||
const push = (u?: string) => {
|
||||
const t = u?.trim();
|
||||
if (t && !merged.includes(t)) merged.push(t);
|
||||
};
|
||||
for (const u of entry.imageUrls ?? []) push(u);
|
||||
push(entry.imageUrl);
|
||||
function mergeColorImageUrls(
|
||||
imageUrls?: string[],
|
||||
imageUrl?: string,
|
||||
): string[] {
|
||||
const merged: string[] = [];
|
||||
const push = (u?: string) => {
|
||||
const t = u?.trim();
|
||||
if (t && !merged.includes(t)) merged.push(t);
|
||||
};
|
||||
for (const u of imageUrls ?? []) push(u);
|
||||
push(imageUrl);
|
||||
return merged;
|
||||
}
|
||||
|
||||
/** ذخیره در Prisma — imageUrl + imageUrls برای هر رنگ (از body خام یا DTO) */
|
||||
export function serializeColorsJsonForDb(
|
||||
raw: unknown,
|
||||
fallbackColor?: string,
|
||||
): unknown[] | undefined {
|
||||
if (raw === undefined || raw === null) return undefined;
|
||||
if (!Array.isArray(raw)) return undefined;
|
||||
|
||||
const colorFallback = fallbackColor?.trim() || '';
|
||||
const out: Array<{ name: string; imageUrl?: string; imageUrls?: string[] }> =
|
||||
[];
|
||||
|
||||
for (let i = 0; i < raw.length; i++) {
|
||||
const item = raw[i];
|
||||
if (typeof item === 'string') {
|
||||
const name = item.trim();
|
||||
if (name) out.push({ name, imageUrls: [] });
|
||||
continue;
|
||||
}
|
||||
if (!item || typeof item !== 'object') continue;
|
||||
|
||||
const o = item as Record<string, unknown>;
|
||||
let name = String(o.name ?? '').trim();
|
||||
const imageUrls = Array.isArray(o.imageUrls)
|
||||
? o.imageUrls
|
||||
.filter((u): u is string => typeof u === 'string' && u.trim().length > 0)
|
||||
.map((u) => u.trim())
|
||||
: undefined;
|
||||
const imageUrl =
|
||||
typeof o.imageUrl === 'string' && o.imageUrl.trim()
|
||||
? o.imageUrl.trim()
|
||||
: undefined;
|
||||
const merged = mergeColorImageUrls(imageUrls, imageUrl);
|
||||
|
||||
if (!name && merged.length > 0) {
|
||||
name = colorFallback || `رنگ ${i + 1}`;
|
||||
}
|
||||
if (!name) continue;
|
||||
|
||||
const first = merged[0];
|
||||
return {
|
||||
name: entry.name,
|
||||
out.push({
|
||||
name,
|
||||
...(first ? { imageUrl: first, imageUrls: merged } : {}),
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -76,13 +76,41 @@ export class ProductsService {
|
||||
return false;
|
||||
}
|
||||
|
||||
private colorsJsonHasPayload(raw: unknown): boolean {
|
||||
if (!Array.isArray(raw) || raw.length === 0) return false;
|
||||
return raw.some((item) => {
|
||||
if (typeof item === 'string') return item.trim().length > 0;
|
||||
if (!item || typeof item !== 'object') return false;
|
||||
const o = item as Record<string, unknown>;
|
||||
if (String(o.name ?? '').trim()) return true;
|
||||
if (typeof o.imageUrl === 'string' && o.imageUrl.trim()) return true;
|
||||
return (
|
||||
Array.isArray(o.imageUrls) &&
|
||||
o.imageUrls.some((u) => typeof u === 'string' && u.trim().length > 0)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
private colorsJsonSourceForWrite(
|
||||
preserved: unknown | undefined,
|
||||
fromDto: unknown | undefined,
|
||||
): unknown | undefined {
|
||||
if (preserved !== undefined && this.colorsJsonHasPayload(preserved)) {
|
||||
return preserved;
|
||||
}
|
||||
if (fromDto !== undefined) return fromDto;
|
||||
if (preserved !== undefined) return preserved;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private colorsJsonForWrite(
|
||||
preserved: unknown | undefined,
|
||||
fromDto: unknown | undefined,
|
||||
fallbackColor?: string,
|
||||
): Prisma.InputJsonValue | undefined {
|
||||
const source = preserved !== undefined ? preserved : fromDto;
|
||||
const source = this.colorsJsonSourceForWrite(preserved, fromDto);
|
||||
if (source === undefined) return undefined;
|
||||
const serialized = serializeColorsJsonForDb(source);
|
||||
const serialized = serializeColorsJsonForDb(source, fallbackColor);
|
||||
if (serialized === undefined) return undefined;
|
||||
return serialized as Prisma.InputJsonValue;
|
||||
}
|
||||
@@ -118,6 +146,7 @@ export class ProductsService {
|
||||
colorsJson: this.colorsJsonForWrite(
|
||||
preservedColorsJson,
|
||||
dto.colorsJson,
|
||||
dto.color,
|
||||
),
|
||||
imageUrlsJson: (dto.imageUrlsJson ?? undefined) as Prisma.InputJsonValue | undefined,
|
||||
tagsJson: dto.tagsJson ?? undefined,
|
||||
@@ -204,7 +233,11 @@ export class ProductsService {
|
||||
colorsJson:
|
||||
dto.colorsJson === undefined && preservedColorsJson === undefined
|
||||
? undefined
|
||||
: this.colorsJsonForWrite(preservedColorsJson, dto.colorsJson),
|
||||
: this.colorsJsonForWrite(
|
||||
preservedColorsJson,
|
||||
dto.colorsJson,
|
||||
dto.color,
|
||||
),
|
||||
imageUrlsJson:
|
||||
dto.imageUrlsJson === undefined
|
||||
? undefined
|
||||
|
||||
Reference in New Issue
Block a user