This commit is contained in:
Your Name committed 2026-10-03 09:35:02 +07:00
1 parent 27cc56abaf
commit 0c3faaba29
11 files changed
+2286 -482

No files matched your search

+183 -24
View File
@@ -18,20 +18,94 @@ function parseId(value: string, label: string) {
}
function buildDefinitionPayload(payload: any) {
const baseLabel = typeof payload.label === 'string'
? payload.label
: (payload.label?.th || payload.label?.en || Object.values(payload.label || {})[0] || payload.translations?.th?.label || payload.translations?.en?.label || (typeof payload.translations?.th === 'string' ? payload.translations.th : '') || (typeof payload.translations?.en === 'string' ? payload.translations.en : '') || payload.code);
const baseDesc = typeof payload.description === 'string'
? payload.description
: (payload.description?.th || payload.description?.en || Object.values(payload.description || {})[0] || payload.translations?.th?.description || payload.translations?.en?.description || null);
return {
code: payload.code,
label: payload.label,
description: payload.description ?? null,
type: normalizeAttributeType(payload.type),
unit: payload.unit ?? null,
code: String(payload.code || '').trim(),
label: String(baseLabel || payload.code || '').trim(),
description: baseDesc ? String(baseDesc).trim() : null,
type: normalizeAttributeType(payload.type || 'TEXT'),
unit: payload.unit ? String(payload.unit).trim() : null,
options: payload.options ?? null,
isRequired: Boolean(payload.isRequired),
isFilterable: Boolean(payload.isFilterable),
sortOrder: typeof payload.sortOrder === 'number' ? payload.sortOrder : 0,
categoryId: payload.categoryId ?? null,
sortOrder: typeof payload.sortOrder === 'number' ? payload.sortOrder : (parseInt(payload.sortOrder) || 0),
categoryId: payload.categoryId ? parseInt(payload.categoryId) : null,
};
}
function extractTranslationsMap(payload: any): Record<string, { label: string; description?: string | null }> {
const result: Record<string, { label: string; description?: string | null }> = {};
for (const lang of SUPPORTED_LANGS) {
let label = '';
let description: string | null = null;
if (payload.translations?.[lang]) {
if (typeof payload.translations[lang] === 'string') {
label = payload.translations[lang].trim();
} else if (typeof payload.translations[lang] === 'object') {
label = (payload.translations[lang].label || '').trim();
if (payload.translations[lang].description) {
description = String(payload.translations[lang].description).trim();
}
}
}
if (!label && payload.label && typeof payload.label === 'object' && payload.label[lang]) {
label = String(payload.label[lang]).trim();
}
if (!description && payload.description && typeof payload.description === 'object' && payload.description[lang]) {
description = String(payload.description[lang]).trim();
}
if (label) {
result[lang] = { label, description };
}
}
// Fallback Thai label
if (!result['th'] && typeof payload.label === 'string' && payload.label.trim()) {
result['th'] = {
label: payload.label.trim(),
description: typeof payload.description === 'string' ? payload.description.trim() : null,
};
}
return result;
}
async function syncAttributeTranslations(definitionId: number, translationsMap: Record<string, { label: string; description?: string | null }>) {
for (const [langCode, val] of Object.entries(translationsMap)) {
if (!val.label) continue;
await prisma.attributeDefinitionTranslation.upsert({
where: {
attributeDefinitionId_langCode: {
attributeDefinitionId: definitionId,
langCode,
},
},
update: {
label: val.label,
description: val.description ?? null,
},
create: {
attributeDefinitionId: definitionId,
langCode,
label: val.label,
description: val.description ?? null,
},
});
}
}
async function assertProductAndDefinitionMatch(productId: number, attributeDefinitionId: number) {
const [product, definition] = await Promise.all([
prisma.product.findUnique({ where: { id: productId } }),
@@ -84,31 +158,106 @@ export const productAttributesRoute = new Elysia()
return status(400, { message: err.message });
}
})
.post("/product-attributes/definitions/bulk-import", async ({ body, status }) => {
try {
const payload = body as any;
const list = Array.isArray(payload)
? payload
: (Array.isArray(payload?.definitions) ? payload.definitions : (Array.isArray(payload?.attributes) ? payload.attributes : null));
if (!list || !Array.isArray(list) || list.length === 0) {
return status(400, { message: "Invalid payload: Expected a JSON array of attribute definition objects" });
}
let createdCount = 0;
let updatedCount = 0;
const results: any[] = [];
for (const item of list) {
if (!item || (!item.code && !item.name && !item.id)) continue;
const code = String(item.code || item.name || '').trim().toLowerCase().replace(/[\s-]+/g, '_');
if (!code && !item.id) continue;
const defPayload = buildDefinitionPayload({
...item,
code: code || item.code,
});
const translationsMap = extractTranslationsMap(item);
let definition = null;
// Check if exists by ID or by Code
if (item.id && typeof item.id === 'number') {
definition = await prisma.attributeDefinition.findUnique({ where: { id: item.id } });
}
if (!definition && code) {
definition = await prisma.attributeDefinition.findUnique({ where: { code } });
}
if (definition) {
definition = await prisma.attributeDefinition.update({
where: { id: definition.id },
data: defPayload,
include: { category: true, translations: true },
});
updatedCount++;
} else {
definition = await prisma.attributeDefinition.create({
data: defPayload,
include: { category: true, translations: true },
});
createdCount++;
}
// Sync all translations
await syncAttributeTranslations(definition.id, translationsMap);
const updated = await prisma.attributeDefinition.findUnique({
where: { id: definition.id },
include: { category: true, translations: true },
});
results.push(localizeAttributeDefinition(updated, 'all'));
}
return {
success: true,
count: results.length,
createdCount,
updatedCount,
definitions: results,
};
} catch (err: any) {
console.error("Bulk import attribute definitions error:", err);
return status(500, { message: err?.message || "Failed to bulk import attribute definitions" });
}
}, {
body: t.Any(),
})
.post("/product-attributes/definitions", async ({ body, status }) => {
try {
const payload = body as any;
const defPayload = buildDefinitionPayload(payload);
const translationsMap = extractTranslationsMap(payload);
const definition = await prisma.attributeDefinition.create({
data: {
...buildDefinitionPayload(payload),
translations: payload.translations && typeof payload.translations === 'object'
? {
create: Object.entries(payload.translations)
.filter(([lang, value]) => SUPPORTED_LANGS.includes(lang as any) && value && typeof value === 'object' && (value as any).label)
.map(([lang, value]) => ({
langCode: lang,
label: (value as any).label,
description: (value as any).description ?? null,
})),
}
: undefined,
},
data: defPayload,
include: {
category: true,
translations: true,
},
});
return { success: true, definition };
await syncAttributeTranslations(definition.id, translationsMap);
const refreshed = await prisma.attributeDefinition.findUnique({
where: { id: definition.id },
include: { category: true, translations: true },
});
return { success: true, definition: refreshed };
} catch (err: any) {
if (err.code === 'P2002') return status(400, { message: "Attribute definition code already exists" });
return status(500, { message: err?.message || "Failed to create attribute definition" });
@@ -120,16 +269,26 @@ export const productAttributesRoute = new Elysia()
try {
const id = parseId(params.id, "attribute definition ID");
const payload = body as any;
const defPayload = buildDefinitionPayload(payload);
const translationsMap = extractTranslationsMap(payload);
const definition = await prisma.attributeDefinition.update({
where: { id },
data: buildDefinitionPayload(payload),
data: defPayload,
include: {
category: true,
translations: true,
},
});
return { success: true, definition };
await syncAttributeTranslations(definition.id, translationsMap);
const refreshed = await prisma.attributeDefinition.findUnique({
where: { id: definition.id },
include: { category: true, translations: true },
});
return { success: true, definition: refreshed };
} catch (err: any) {
if (err.code === 'P2025') return status(404, { message: "Attribute definition not found" });
if (err.code === 'P2002') return status(400, { message: "Attribute definition code already exists" });