pim-import 6.11.0 → 6.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -442,6 +442,68 @@ await importDownloads("/path/to/downloads.csv");
442
442
 
443
443
  ---
444
444
 
445
+ ## SEO Import
446
+
447
+ Imports SEO metadata (meta title and meta description) from a CSV file stored on S3 into Contentful `topicSeo` entries.
448
+
449
+ The function parses the `slug` column to extract the locale and the actual slug value. The locale is the first path segment; the slug is the last segment. For example, `de/global/bespoke/` means locale=`de` and slug=`bespoke`, so it searches for a `page` entry with `fields.slug.de = "bespoke"`.
450
+
451
+ ### CSV format
452
+
453
+ | Column | Description |
454
+ | ----------------- | ------------------------------------------------------------------------------------- |
455
+ | `slug` | Localized slug path (e.g., `de/global/bespoke/` or `en-US/us/projects/project-name/`) |
456
+ | `metaTitle` | Meta title for the detected locale |
457
+ | `metaDescription` | Meta description for the detected locale |
458
+
459
+ ### Example CSV
460
+
461
+ ```csv
462
+ slug,metaTitle,metaDescription
463
+ de/global/bespoke/,Bespoke - German,Die Meta-Beschreibung für die Bespoke-Seite
464
+ /en-US/us/projects/projects-bodegas-faustino/,Bodegas Faustino,The meta description for Bodegas Faustino
465
+ ```
466
+
467
+ See `src/pim/methods/seo-example.csv` for a complete example.
468
+
469
+ ### Usage
470
+
471
+ ```ts
472
+ import { importSeoFromCsv } from "pim-import";
473
+
474
+ const result = await importSeoFromCsv(
475
+ "path/to/seo-data.csv", // S3 path (required)
476
+ 0, // offset (default 0)
477
+ 50, // limit, -1 = all (default 50)
478
+ );
479
+ // result: {
480
+ // offset: number,
481
+ // limit: number,
482
+ // completed: boolean,
483
+ // total: number,
484
+ // processed: number,
485
+ // created: number,
486
+ // updated: number,
487
+ // skipped: number,
488
+ // errors: string[]
489
+ // }
490
+ ```
491
+
492
+ ### Behavior
493
+
494
+ - **Slug parsing**: Extracts locale (first segment) and slug (last segment) from the path
495
+ - **Slug fallback**: If a page does not have the slug translated for the requested locale (i.e., `fields.slug.{locale}` does not exist), falls back to searching with `fields.slug.en`
496
+ - **Invalid format**: Logs a warning and skips rows with malformed slug paths
497
+ - **Unknown locale**: Logs a warning and skips rows with unsupported locales
498
+ - **Page not found**: Logs a warning and skips the row
499
+ - **topicSeo exists**: Updates only `metaTitle` and `metaDescription` for the detected locale; other locales and fields remain unchanged
500
+ - **topicSeo not found**: Creates a new `topicSeo` entry, populates `metaTitle`/`metaDescription` for the detected locale, and associates it to the page's `seo` field
501
+ - **Empty CSV value**: Retains the existing value in Contentful (never overwrites with empty)
502
+ - **Published automatically**: New and updated `topicSeo` entries are published immediately
503
+ - **Errors**: Logged but processing continues for other rows
504
+
505
+ ---
506
+
445
507
  ## Contentful utilities
446
508
 
447
509
  ```ts
@@ -322,7 +322,7 @@ const reindexProducts = async (filterKey, filterValue, offset = 0, limit = 50, l
322
322
  const timeStart = new Date();
323
323
  (0, logs_1.log)(`reindexProducts - filterKey: ${filterKey} filterValue: ${filterValue} offset: ${offset} limit: ${limit} lastPimSyncDateGte: ${lastPimSyncDateGte} filters: ${filters
324
324
  .map((filter) => `${filter.key}:${filter.value}`)
325
- .join(", ")}`);
325
+ .join(", ")} lastModifiedGte: ${lastModifiedGte}`);
326
326
  const records = await getObjects(filterKey, filterValue, offset, limit, lastPimSyncDateGte, filters, lastModifiedGte);
327
327
  const defaultEnvironmentLocaleCode = await (0, contentful_cda_1.getEnvironmentDefaultLocaleCode)();
328
328
  const objectsToSave = records.objects.filter((object) => !isObjectToDelete(object, defaultEnvironmentLocaleCode));
package/dist/index.d.ts CHANGED
@@ -12,6 +12,7 @@ export { importProductByCode, setProductsRelationships, setProductRelationships,
12
12
  export { publishAllProductDrafts } from "./pim/methods/bulkPublish";
13
13
  export { migrateEntryFields } from "./pim/methods/migrateEntryFields";
14
14
  export { checkTopicDraftAndPagePublished } from "./pim/methods/checkTopicDraftAndPagePublished";
15
+ export { importSeoFromCsv } from "./pim/methods/seo";
15
16
  export { getIndex, cloneIndexSettings } from "./algolia/config";
16
17
  export { reindexFamilies, reindexFamily, removeFamilyObject, } from "./algolia/families";
17
18
  export { reindexSubFamilies, reindexSubFamily, removeSubFamilyObject, } from "./algolia/subFamilies";
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.removeSubFamilyObject = exports.reindexSubFamily = exports.reindexSubFamilies = exports.removeFamilyObject = exports.reindexFamily = exports.reindexFamilies = exports.cloneIndexSettings = exports.getIndex = exports.checkTopicDraftAndPagePublished = exports.migrateEntryFields = exports.publishAllProductDrafts = exports.purgeProductThumbCacheByProductCodes = exports.populateDestinations = exports.reimportAuditProducts = exports.getProductPageIdByCode = exports.removeAllProductModelProductRelations = exports.removeProductFromColorVariantsByProductLine = exports.setProductAutodescriptionByTopicId = exports.getProductAutodescription = exports.setProductsAutodescription = exports.generateTechSpecPdf = exports.audit = exports.getAllProductEntriesByCatalog = exports.setProductRelationships = exports.setProductsRelationships = exports.importProductByCode = exports.importFamilies = exports.importSubFamilies = exports.importSubModels = exports.importModels = exports.importLatestProducts = exports.importCategories = exports.importDictionaryProductSubLine = exports.importDictionaryProductLine = exports.importDictionaryData = exports.importDictionaryIcons = exports.importDictionaryFields = exports.savePDFToS3 = exports.getFileFromS3 = exports.saveJsonToS3 = exports.uploadS3 = exports.initS3 = exports.getEntries = exports.getTopicPage = exports.getEntryByID = exports.deleteEntries = exports.deletePages = exports.initBaseEntries = exports.initContentful = exports.initPim = void 0;
4
- exports.netlifyBuild = exports.notify = exports.importDesigner = exports.importDesigners = exports.generatePDFByUrl = exports.removeRecordsByStatus = exports.getLogFolder = exports.setLogFilename = exports.setLogPath = exports.setLogId = exports.setServerUtils = exports.log = exports.getLatestProducts = exports.getStaticDailyProducts = exports.getLocalISOTime = exports.importDownloads = exports.removePostObject = exports.reindexPosts = exports.reindexPost = exports.removePressReleaseObject = exports.reindexPressReleases = exports.reindexPressRelease = exports.removePressReviewObject = exports.reindexPressReviews = exports.reindexPressReview = exports.removeStoryObject = exports.reindexStories = exports.reindexStory = exports.removeProjectObject = exports.reindexProjects = exports.reindexProject = exports.removeInspirationObject = exports.reindexInspirations = exports.reindexInspiration = exports.removeDownloadObject = exports.reindexDownloads = exports.reindexDownload = exports.removeModelObject = exports.reindexModels = exports.reindexModel = exports.removeSubModelObject = exports.reindexSubModels = exports.reindexSubModel = exports.triggerPDFGenerator = exports.removeProductObject = exports.reindexProducts = exports.reindexProduct = void 0;
3
+ exports.reindexSubFamily = exports.reindexSubFamilies = exports.removeFamilyObject = exports.reindexFamily = exports.reindexFamilies = exports.cloneIndexSettings = exports.getIndex = exports.importSeoFromCsv = exports.checkTopicDraftAndPagePublished = exports.migrateEntryFields = exports.publishAllProductDrafts = exports.purgeProductThumbCacheByProductCodes = exports.populateDestinations = exports.reimportAuditProducts = exports.getProductPageIdByCode = exports.removeAllProductModelProductRelations = exports.removeProductFromColorVariantsByProductLine = exports.setProductAutodescriptionByTopicId = exports.getProductAutodescription = exports.setProductsAutodescription = exports.generateTechSpecPdf = exports.audit = exports.getAllProductEntriesByCatalog = exports.setProductRelationships = exports.setProductsRelationships = exports.importProductByCode = exports.importFamilies = exports.importSubFamilies = exports.importSubModels = exports.importModels = exports.importLatestProducts = exports.importCategories = exports.importDictionaryProductSubLine = exports.importDictionaryProductLine = exports.importDictionaryData = exports.importDictionaryIcons = exports.importDictionaryFields = exports.savePDFToS3 = exports.getFileFromS3 = exports.saveJsonToS3 = exports.uploadS3 = exports.initS3 = exports.getEntries = exports.getTopicPage = exports.getEntryByID = exports.deleteEntries = exports.deletePages = exports.initBaseEntries = exports.initContentful = exports.initPim = void 0;
4
+ exports.netlifyBuild = exports.notify = exports.importDesigner = exports.importDesigners = exports.generatePDFByUrl = exports.removeRecordsByStatus = exports.getLogFolder = exports.setLogFilename = exports.setLogPath = exports.setLogId = exports.setServerUtils = exports.log = exports.getLatestProducts = exports.getStaticDailyProducts = exports.getLocalISOTime = exports.importDownloads = exports.removePostObject = exports.reindexPosts = exports.reindexPost = exports.removePressReleaseObject = exports.reindexPressReleases = exports.reindexPressRelease = exports.removePressReviewObject = exports.reindexPressReviews = exports.reindexPressReview = exports.removeStoryObject = exports.reindexStories = exports.reindexStory = exports.removeProjectObject = exports.reindexProjects = exports.reindexProject = exports.removeInspirationObject = exports.reindexInspirations = exports.reindexInspiration = exports.removeDownloadObject = exports.reindexDownloads = exports.reindexDownload = exports.removeModelObject = exports.reindexModels = exports.reindexModel = exports.removeSubModelObject = exports.reindexSubModels = exports.reindexSubModel = exports.triggerPDFGenerator = exports.removeProductObject = exports.reindexProducts = exports.reindexProduct = exports.removeSubFamilyObject = void 0;
5
5
  var config_1 = require("./pim/config");
6
6
  Object.defineProperty(exports, "initPim", { enumerable: true, get: function () { return config_1.init; } });
7
7
  var contentful_1 = require("./libs/contentful");
@@ -58,6 +58,8 @@ var migrateEntryFields_1 = require("./pim/methods/migrateEntryFields");
58
58
  Object.defineProperty(exports, "migrateEntryFields", { enumerable: true, get: function () { return migrateEntryFields_1.migrateEntryFields; } });
59
59
  var checkTopicDraftAndPagePublished_1 = require("./pim/methods/checkTopicDraftAndPagePublished");
60
60
  Object.defineProperty(exports, "checkTopicDraftAndPagePublished", { enumerable: true, get: function () { return checkTopicDraftAndPagePublished_1.checkTopicDraftAndPagePublished; } });
61
+ var seo_1 = require("./pim/methods/seo");
62
+ Object.defineProperty(exports, "importSeoFromCsv", { enumerable: true, get: function () { return seo_1.importSeoFromCsv; } });
61
63
  var config_2 = require("./algolia/config");
62
64
  Object.defineProperty(exports, "getIndex", { enumerable: true, get: function () { return config_2.getIndex; } });
63
65
  Object.defineProperty(exports, "cloneIndexSettings", { enumerable: true, get: function () { return config_2.cloneIndexSettings; } });
@@ -178,7 +178,8 @@ const getAllEntriesByCodes = async (codes, contentTypeId, select, filter = "fiel
178
178
  if (!codes.length) {
179
179
  return [];
180
180
  }
181
- (0, logs_1.log)(`getAllEntriesByCodes codes: ${codes.length} contentTypeId: ${contentTypeId}`);
181
+ const uniqueCodes = Array.from(new Set(codes));
182
+ (0, logs_1.log)(`getAllEntriesByCodes uniqueCodes: ${uniqueCodes.length} contentTypeId: ${contentTypeId}`);
182
183
  const env = await (0, exports.getEnvironment)();
183
184
  const defEnvLocaleCode = await (0, exports.getEnvironmentDefaultLocaleCode)();
184
185
  let allItems = [];
@@ -188,11 +189,11 @@ const getAllEntriesByCodes = async (codes, contentTypeId, select, filter = "fiel
188
189
  const chunkLimit = 20;
189
190
  const filterKey = `${filter}[in]`;
190
191
  let codesChunks = [];
191
- if (codes.length > chunkLimit) {
192
- codesChunks = (0, utils_1.doChunk)(codes, chunkLimit);
192
+ if (uniqueCodes.length > chunkLimit) {
193
+ codesChunks = (0, utils_1.doChunk)(uniqueCodes, chunkLimit);
193
194
  }
194
195
  else {
195
- codesChunks.push(codes);
196
+ codesChunks.push(uniqueCodes);
196
197
  }
197
198
  let count = 0;
198
199
  for (const codesChunk of codesChunks) {
@@ -363,10 +363,11 @@ const importDictionaryProductSubLine = async () => {
363
363
  (0, logs_1.log)(`Import productSubLine ${productSubLine.code}`);
364
364
  let productSubLineEntry = await (0, contentful_1.getEntryByCode)(productSubLine.code, "topicProductSubLine");
365
365
  let productLineEntry = null;
366
- if (productSubLine.othersData.productLine.code) {
367
- productLineEntry = await (0, contentful_1.getEntryByCode)(productSubLine.othersData.productLine.code, "topicProductLine");
366
+ const productLineCode = productSubLine.productLine?.code;
367
+ if (productLineCode) {
368
+ productLineEntry = await (0, contentful_1.getEntryByCode)(productLineCode, "topicProductLine");
368
369
  if (!productLineEntry) {
369
- (0, logs_1.log)(`No topicProductLine with code ${productSubLine.othersData.productLine.code} found for the productSubLine ${productSubLine.code}`);
370
+ (0, logs_1.log)(`No topicProductLine with code ${productLineCode} found for the productSubLine ${productSubLine.code}`);
370
371
  }
371
372
  }
372
373
  else {
@@ -391,6 +391,11 @@ const getProductFields = async (pimDetails) => {
391
391
  const getTopicProductIdByCode = (productCode) => {
392
392
  return productCode;
393
393
  };
394
+ const extractProductCode = (code) => {
395
+ return code?.includes(PIM_DUPLICATE_SEPARATOR)
396
+ ? code.split(PIM_DUPLICATE_SEPARATOR)[0]
397
+ : code;
398
+ };
394
399
  const getProductPageIdByCode = (productCode) => {
395
400
  return `${productCode}_PAGE`;
396
401
  };
@@ -1056,17 +1061,24 @@ const getAllProductEntriesByCatalog = async (catalog, limit = 100, select = "")
1056
1061
  return allItems;
1057
1062
  };
1058
1063
  exports.getAllProductEntriesByCatalog = getAllProductEntriesByCatalog;
1064
+ const updateEntryInArray = (entries, updatedEntry) => {
1065
+ const index = entries.findIndex((e) => e.sys.id === updatedEntry.sys.id);
1066
+ if (index !== -1) {
1067
+ entries[index] = updatedEntry;
1068
+ }
1069
+ };
1059
1070
  const productAudit = async (audit, productEntries, catalog, defaultEnvironmentLocaleCode, current, allAudit, productPageEntries) => {
1060
- (0, logs_1.log)(`Search product entry with id ${audit.product}...`);
1071
+ const productCode = extractProductCode(audit.product);
1072
+ (0, logs_1.log)(`Search product entry with id ${productCode}...`);
1061
1073
  let productEntry = productEntries.find((currentEntry) => currentEntry?.fields?.code?.[defaultEnvironmentLocaleCode] ===
1062
- audit.product);
1074
+ productCode);
1063
1075
  if (!productEntry) {
1064
1076
  let logMessage = "";
1065
1077
  if (catalog) {
1066
- logMessage = `The ${audit.product} product was not found in the CMS with catalog ${catalog}`;
1078
+ logMessage = `The ${productCode} product was not found in the CMS with catalog ${catalog}`;
1067
1079
  }
1068
1080
  else {
1069
- logMessage = `The ${audit.product} product was not found in the CMS`;
1081
+ logMessage = `The ${productCode} product was not found in the CMS`;
1070
1082
  }
1071
1083
  (0, logs_1.log)(logMessage, "WARN");
1072
1084
  if (logs_1.serverUtils) {
@@ -1077,7 +1089,7 @@ const productAudit = async (audit, productEntries, catalog, defaultEnvironmentLo
1077
1089
  return { current, continue: true };
1078
1090
  }
1079
1091
  (0, logs_1.log)(`Founded product with id ${productEntry.sys.id}`);
1080
- const pageEntryFromId = (0, exports.getProductPageIdByCode)(audit.product);
1092
+ const pageEntryFromId = (0, exports.getProductPageIdByCode)(productCode);
1081
1093
  (0, logs_1.log)(`Search product page entry with id ${pageEntryFromId}...`);
1082
1094
  let pageEntryFrom = productPageEntries.find((currentEntry) => currentEntry.sys.id === pageEntryFromId);
1083
1095
  if (pageEntryFrom) {
@@ -1091,31 +1103,35 @@ const productAudit = async (audit, productEntries, catalog, defaultEnvironmentLo
1091
1103
  return entryCatalog.sys.id;
1092
1104
  });
1093
1105
  if (catalog && !productCatalogs.includes(catalog)) {
1094
- (0, logs_1.log)(`Product ${audit.product} does not belong to the ${catalog} catalog`);
1106
+ (0, logs_1.log)(`Product ${productCode} does not belong to the ${catalog} catalog`);
1095
1107
  }
1096
1108
  else if (["PRODUCT_UNPUBLISH", "PRODUCT_WEBOFF"].includes(audit.what)) {
1097
1109
  (0, logs_1.log)(`Set the product status as archive...`);
1098
1110
  productEntry = await (0, contentful_1.archiveEntry)(productEntry, true);
1111
+ updateEntryInArray(productEntries, productEntry);
1099
1112
  if (!pageEntryFrom) {
1100
1113
  (0, logs_1.log)(`${pageEntryFromId} page from not found`);
1101
1114
  }
1102
1115
  else if (pageEntryFrom.isArchived()) {
1103
- (0, logs_1.log)(`Can't create redirect ${audit.product} to ${audit.upgradeProduct} because the page ${pageEntryFrom.sys.id} is archived.`, "WARN");
1116
+ (0, logs_1.log)(`Can't create redirect ${productCode} to ${audit.upgradeProduct} because the page ${pageEntryFrom.sys.id} is archived.`, "WARN");
1104
1117
  }
1105
1118
  else if (pageEntryFrom?.fields) {
1106
1119
  if (audit.upgradeProduct && audit.upgradeProduct !== "0") {
1107
- (0, logs_1.log)(`Creating redirect from ${audit.product} to ${audit.upgradeProduct}`);
1120
+ (0, logs_1.log)(`Creating redirect from ${productCode} to ${audit.upgradeProduct}`);
1108
1121
  const pageEntryToId = (0, exports.getProductPageIdByCode)(audit.upgradeProduct);
1109
1122
  const pageEntryTo = await (0, contentful_1.getEntryByID)(pageEntryToId, "page", "sys.id");
1110
1123
  if (pageEntryTo) {
1111
1124
  pageEntryFrom.fields = await (0, contentful_1.addToRelationFields)(pageEntryFrom, "redirectTo", pageEntryTo.sys.id);
1112
1125
  pageEntryFrom = await pageEntryFrom.update();
1126
+ updateEntryInArray(productPageEntries, pageEntryFrom);
1127
+ (0, logs_1.log)(`Created redirect from ${productCode} to ${audit.upgradeProduct}`);
1113
1128
  }
1114
1129
  else {
1115
- (0, logs_1.log)(`Can't create redirect ${audit.product} to ${audit.upgradeProduct} because the page ${pageEntryToId} not found.`, "WARN");
1130
+ (0, logs_1.log)(`Can't create redirect ${productCode} to ${audit.upgradeProduct} because the page ${pageEntryToId} not found.`, "WARN");
1116
1131
  }
1117
1132
  }
1118
1133
  pageEntryFrom = await (0, contentful_1.archiveEntry)(pageEntryFrom);
1134
+ updateEntryInArray(productPageEntries, pageEntryFrom);
1119
1135
  }
1120
1136
  }
1121
1137
  else if (audit.what === "REMOVE_PRODUCT_RELATIONS" &&
@@ -1125,6 +1141,7 @@ const productAudit = async (audit, productEntries, catalog, defaultEnvironmentLo
1125
1141
  try {
1126
1142
  (0, logs_1.log)(`Publish existing changes of entry ${productEntry.sys.id}.`);
1127
1143
  productEntry = await productEntry.publish();
1144
+ updateEntryInArray(productEntries, productEntry);
1128
1145
  }
1129
1146
  catch (err) {
1130
1147
  (0, logs_1.log)(`Cannot publish changes of entry ${productEntry.sys.id}.`);
@@ -1138,33 +1155,28 @@ const productAudit = async (audit, productEntries, catalog, defaultEnvironmentLo
1138
1155
  }
1139
1156
  else {
1140
1157
  let edit = false;
1141
- if (item?.catalogCode) {
1142
- productEntry.fields = await (0, contentful_1.removeFromRelationFields)(productEntry, "catalogs", item.catalogCode, true);
1143
- (0, logs_1.log)(`edit catalogs`);
1144
- edit = true;
1145
- }
1146
1158
  if (item?.categoryCode) {
1147
1159
  const categoriesFieldKey = "categories" + (0, utils_1.capitalizeFirstLetter)(item.where);
1160
+ (0, logs_1.log)(`Removing ${item.categoryCode} from ${categoriesFieldKey}...`);
1148
1161
  productEntry.fields = await (0, contentful_1.removeFromRelationFields)(productEntry, categoriesFieldKey, item.categoryCode, true);
1149
- (0, logs_1.log)(`edit ${categoriesFieldKey}`);
1150
1162
  edit = true;
1151
1163
  }
1152
1164
  if (item?.subfamilyCode) {
1153
1165
  const subFamiliesFieldKey = "subFamilies" + (0, utils_1.capitalizeFirstLetter)(item.where);
1166
+ (0, logs_1.log)(`Removing ${item.subfamilyCode} from ${subFamiliesFieldKey}...`);
1154
1167
  productEntry.fields = await (0, contentful_1.removeFromRelationFields)(productEntry, subFamiliesFieldKey, item.subfamilyCode, true);
1155
- (0, logs_1.log)(`edit ${subFamiliesFieldKey}`);
1156
1168
  edit = true;
1157
1169
  }
1158
1170
  if (item?.models) {
1159
1171
  const modelsFieldKey = "models" + (0, utils_1.capitalizeFirstLetter)(item.where);
1172
+ (0, logs_1.log)(`Removing ${item.models} from ${modelsFieldKey}...`);
1160
1173
  productEntry.fields = await (0, contentful_1.removeFromRelationFields)(productEntry, modelsFieldKey, item.models, true);
1161
- (0, logs_1.log)(`edit ${modelsFieldKey}`);
1162
1174
  edit = true;
1163
1175
  }
1164
1176
  if (item?.subModels) {
1165
1177
  const subModelsFieldKey = "subModels" + (0, utils_1.capitalizeFirstLetter)(item.where);
1178
+ (0, logs_1.log)(`Removing ${item.subModels} from ${subModelsFieldKey}...`);
1166
1179
  productEntry.fields = await (0, contentful_1.removeFromRelationFields)(productEntry, subModelsFieldKey, item.subModels, true);
1167
- (0, logs_1.log)(`edit ${subModelsFieldKey}`);
1168
1180
  edit = true;
1169
1181
  }
1170
1182
  const objectFieldsRelations = {
@@ -1215,9 +1227,11 @@ const productAudit = async (audit, productEntries, catalog, defaultEnvironmentLo
1215
1227
  try {
1216
1228
  (0, logs_1.log)(`update ${productEntry.sys.id}`);
1217
1229
  productEntry = await productEntry.update();
1230
+ updateEntryInArray(productEntries, productEntry);
1218
1231
  if (productEntry.isPublished()) {
1219
1232
  try {
1220
1233
  productEntry = await productEntry.publish();
1234
+ updateEntryInArray(productEntries, productEntry);
1221
1235
  }
1222
1236
  catch (err) {
1223
1237
  (0, logs_1.log)(`Cannot publish entry ${productEntry.sys.id}.`);
@@ -1232,13 +1246,12 @@ const productAudit = async (audit, productEntries, catalog, defaultEnvironmentLo
1232
1246
  }
1233
1247
  else {
1234
1248
  (0, logs_1.log)(`No valid editable criteria found`);
1235
- console.log(productEntry.sys.id, "audit", audit);
1236
1249
  }
1237
1250
  }
1238
1251
  }
1239
1252
  }
1240
1253
  else {
1241
- (0, logs_1.log)(`No catalogs field found in the audit ${audit.product} product`, "WARN");
1254
+ (0, logs_1.log)(`No catalogs field found in the audit ${productCode} product`, "WARN");
1242
1255
  }
1243
1256
  }
1244
1257
  else {
@@ -1247,16 +1260,16 @@ const productAudit = async (audit, productEntries, catalog, defaultEnvironmentLo
1247
1260
  return { current, continue: false };
1248
1261
  };
1249
1262
  const familyAudit = async (audit, familyEntries, catalog, defaultEnvironmentLocaleCode, current, allAudit) => {
1250
- (0, logs_1.log)(`Search family entry with id ${audit.product}...`);
1251
- let familyEntry = familyEntries.find((currentEntry) => currentEntry?.fields?.code?.[defaultEnvironmentLocaleCode] ===
1252
- audit.product);
1263
+ const familyCode = extractProductCode(audit.product);
1264
+ (0, logs_1.log)(`Search family entry with id ${familyCode}...`);
1265
+ let familyEntry = familyEntries.find((currentEntry) => currentEntry?.fields?.code?.[defaultEnvironmentLocaleCode] === familyCode);
1253
1266
  if (!familyEntry) {
1254
1267
  let logMessage = "";
1255
1268
  if (catalog) {
1256
- logMessage = `The ${audit.product} family was not found in the CMS with catalog ${catalog}`;
1269
+ logMessage = `The ${familyCode} family was not found in the CMS with catalog ${catalog}`;
1257
1270
  }
1258
1271
  else {
1259
- logMessage = `The ${audit.product} family was not found in the CMS`;
1272
+ logMessage = `The ${familyCode} family was not found in the CMS`;
1260
1273
  }
1261
1274
  (0, logs_1.log)(logMessage, "WARN");
1262
1275
  if (logs_1.serverUtils) {
@@ -1272,12 +1285,12 @@ const familyAudit = async (audit, familyEntries, catalog, defaultEnvironmentLoca
1272
1285
  return entryCatalog.sys.id;
1273
1286
  });
1274
1287
  if (catalog && !familyCatalogs.includes(catalog)) {
1275
- (0, logs_1.log)(`Family ${audit.product} does not belong to the ${catalog} catalog`);
1288
+ (0, logs_1.log)(`Family ${familyCode} does not belong to the ${catalog} catalog`);
1276
1289
  }
1277
1290
  else if (audit.what === "REMOVE_FAMILY_RELATIONS") {
1278
1291
  for (const item of audit?.catalogs) {
1279
1292
  if (!item?.where) {
1280
- (0, logs_1.log)(`catalogs.where field not exists. Family: ${audit.product} `, "WARN");
1293
+ (0, logs_1.log)(`catalogs.where field not exists. Family: ${familyCode} `, "WARN");
1281
1294
  }
1282
1295
  else {
1283
1296
  if (item.where === "DESIGNERS" && item?.codes) {
@@ -1297,16 +1310,17 @@ const familyAudit = async (audit, familyEntries, catalog, defaultEnvironmentLoca
1297
1310
  return { current, continue: false };
1298
1311
  };
1299
1312
  const subFamilyAudit = async (audit, subfamilyEntries, catalog, defaultEnvironmentLocaleCode, current, allAudit) => {
1300
- (0, logs_1.log)(`Search subFamily entry with id ${audit.product}...`);
1313
+ const subFamilyCode = extractProductCode(audit.product);
1314
+ (0, logs_1.log)(`Search subFamily entry with id ${subFamilyCode}...`);
1301
1315
  let subFamilyEntry = subfamilyEntries.find((currentEntry) => currentEntry?.fields?.code?.[defaultEnvironmentLocaleCode] ===
1302
- audit.product);
1316
+ subFamilyCode);
1303
1317
  if (!subFamilyEntry) {
1304
1318
  let logMessage = "";
1305
1319
  if (catalog) {
1306
- logMessage = `The ${audit.product} subFamily was not found in the CMS with catalog ${catalog}`;
1320
+ logMessage = `The ${subFamilyCode} subFamily was not found in the CMS with catalog ${catalog}`;
1307
1321
  }
1308
1322
  else {
1309
- logMessage = `The ${audit.product} subFamily was not found in the CMS`;
1323
+ logMessage = `The ${subFamilyCode} subFamily was not found in the CMS`;
1310
1324
  }
1311
1325
  (0, logs_1.log)(logMessage, "WARN");
1312
1326
  if (logs_1.serverUtils) {
@@ -1320,7 +1334,7 @@ const subFamilyAudit = async (audit, subfamilyEntries, catalog, defaultEnvironme
1320
1334
  (0, logs_1.log)(`Get subFamily catalogs...`);
1321
1335
  const subFamilyCatalog = subFamilyEntry?.fields?.catalog?.[defaultEnvironmentLocaleCode].sys.id;
1322
1336
  if (catalog && subFamilyCatalog !== catalog) {
1323
- (0, logs_1.log)(`SubFamily ${audit.product} does not belong to the ${catalog} catalog`);
1337
+ (0, logs_1.log)(`SubFamily ${subFamilyCode} does not belong to the ${catalog} catalog`);
1324
1338
  }
1325
1339
  else if (audit.what === "REMOVE_SUBFAMILY_RELATIONS") {
1326
1340
  for (const item of audit?.catalogs) {
@@ -1365,7 +1379,7 @@ const audit = async (lastModified, catalog, offset = 0, limit = 150, s3FilePath
1365
1379
  const filename = `${lastModified}-all-audit.json`;
1366
1380
  const path = `audit${process.env.FPI_CTF_ENVIRONMENT
1367
1381
  ? "/" + process.env.FPI_CTF_ENVIRONMENT
1368
- : ""}`;
1382
+ : ""}/${catalog}`;
1369
1383
  await (0, s3_1.saveJsonToS3)(allAudit, filename, path);
1370
1384
  const s3Path = `${path}/${filename}`;
1371
1385
  if (logs_1.serverUtils) {
@@ -1395,19 +1409,17 @@ const audit = async (lastModified, catalog, offset = 0, limit = 150, s3FilePath
1395
1409
  ];
1396
1410
  const productCodes = allAudit
1397
1411
  .filter((item) => productAuditWhat.includes(item.what))
1398
- .map((audit) => audit.product);
1412
+ .map((audit) => extractProductCode(audit.product));
1399
1413
  const familyCodes = allAudit
1400
1414
  .filter((item) => item.what === "REMOVE_FAMILY_RELATIONS")
1401
- .map((audit) => audit.product);
1415
+ .map((audit) => extractProductCode(audit.product));
1402
1416
  const subFamilyCodes = allAudit
1403
1417
  .filter((item) => item.what === "REMOVE_SUBFAMILY_RELATIONS")
1404
- .map((audit) => audit.product);
1418
+ .map((audit) => extractProductCode(audit.product));
1405
1419
  const otherFilters = [];
1406
1420
  if (catalog) {
1407
1421
  otherFilters.push({ key: "fields.catalogs.sys.id[in]", value: catalog });
1408
1422
  }
1409
- (0, logs_1.log)(`Get ${productCodes.length} product entry from Contentful`);
1410
- console.log("productCodes", productCodes);
1411
1423
  const productEntries = await (0, contentful_1.getAllEntriesByCodes)(productCodes, "topicProduct", "sys,fields", "fields.code", otherFilters);
1412
1424
  (0, logs_1.log)(`Founded ${productEntries.length} topicProduct`);
1413
1425
  let familyEntries = [];
@@ -1425,7 +1437,7 @@ const audit = async (lastModified, catalog, offset = 0, limit = 150, s3FilePath
1425
1437
  !subFamilyEntries.length) {
1426
1438
  (0, logs_1.log)(`No items found between offset: ${offset} and limit: ${limit}. Total: ${total}`);
1427
1439
  const nextOffset = offset + limit;
1428
- const completed = limit === -1 || offset >= total;
1440
+ const completed = limit === -1 || nextOffset >= total;
1429
1441
  if (completed) {
1430
1442
  (0, logs_1.log)(`Audit completed`);
1431
1443
  }
@@ -1433,7 +1445,9 @@ const audit = async (lastModified, catalog, offset = 0, limit = 150, s3FilePath
1433
1445
  const secs = (0, utils_1.secondBetweenTwoDate)(timeStart, tEnd);
1434
1446
  (0, logs_1.log)(`Execution time: ${secs} seconds`);
1435
1447
  if (logs_1.serverUtils) {
1436
- logs_1.serverUtils.log(`Audit completed`);
1448
+ if (completed) {
1449
+ logs_1.serverUtils.log(`Audit completed`);
1450
+ }
1437
1451
  logs_1.serverUtils.updateProgress(100);
1438
1452
  }
1439
1453
  return {
@@ -1970,7 +1984,7 @@ const reimportAuditProducts = async (lastModified, catalog, offset = 0, limit =
1970
1984
  if (!entries.length) {
1971
1985
  (0, logs_1.log)(`No products found between offset: ${offset} and limit: ${limit}. Total: ${total}`);
1972
1986
  const nextOffset = offset + limit;
1973
- const completed = limit === -1 || offset >= total;
1987
+ const completed = limit === -1 || nextOffset >= total;
1974
1988
  if (completed) {
1975
1989
  (0, logs_1.log)(`Audit completed`);
1976
1990
  }
@@ -2010,7 +2024,7 @@ const reimportAuditProducts = async (lastModified, catalog, offset = 0, limit =
2010
2024
  const productCatalogs = productEntry?.fields?.catalogs?.[defaultEnvironmentLocaleCode].map((entryCatalog) => {
2011
2025
  return entryCatalog.sys.id;
2012
2026
  });
2013
- if (catalog && !productCatalogs.includes(catalog)) {
2027
+ if (catalog && !productCatalogs?.includes(catalog)) {
2014
2028
  (0, logs_1.log)(`Product ${audit.product} does not belong to the ${catalog} catalog`);
2015
2029
  }
2016
2030
  else {
@@ -0,0 +1,13 @@
1
+ interface ImportResult {
2
+ offset: number;
3
+ limit: number;
4
+ completed: boolean;
5
+ total: number;
6
+ processed: number;
7
+ created: number;
8
+ updated: number;
9
+ skipped: number;
10
+ errors: string[];
11
+ }
12
+ export declare const importSeoFromCsv: (s3CsvPath: string, offset?: number, limit?: number) => Promise<ImportResult>;
13
+ export default importSeoFromCsv;
@@ -0,0 +1,224 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.importSeoFromCsv = void 0;
7
+ const contentful_1 = require("../../libs/contentful");
8
+ const s3_1 = require("../../libs/s3");
9
+ const logs_1 = require("../../libs/logs");
10
+ const csv_parser_1 = __importDefault(require("csv-parser"));
11
+ const stream_1 = require("stream");
12
+ const LOCALES = contentful_1.cfLocales;
13
+ const parseCsvFromS3 = async (s3Path) => {
14
+ const csvString = await (0, s3_1.getFileFromS3)(s3Path);
15
+ return new Promise((resolve, reject) => {
16
+ const results = [];
17
+ stream_1.Readable.from(csvString)
18
+ .pipe((0, csv_parser_1.default)())
19
+ .on("data", (data) => results.push(data))
20
+ .on("end", () => resolve(results))
21
+ .on("error", reject);
22
+ });
23
+ };
24
+ const parseLocalizedSlug = (slugPath) => {
25
+ const cleanPath = slugPath.startsWith("/") ? slugPath.slice(1) : slugPath;
26
+ const segments = cleanPath.split("/").filter(Boolean);
27
+ if (segments.length < 2) {
28
+ return null;
29
+ }
30
+ const locale = segments[0];
31
+ const slug = segments[segments.length - 1];
32
+ if (!locale || !slug) {
33
+ return null;
34
+ }
35
+ return { locale, slug };
36
+ };
37
+ const isValidLocale = (locale) => {
38
+ return LOCALES.includes(locale);
39
+ };
40
+ const findPageBySlug = async (locale, slug) => {
41
+ const env = await (0, contentful_1.getEnvironment)();
42
+ const slugFieldKey = `fields.slug.${locale}`;
43
+ let opts = {
44
+ content_type: "page",
45
+ [slugFieldKey]: slug,
46
+ limit: 1,
47
+ include: 1,
48
+ };
49
+ let { items } = await env.getEntries(opts);
50
+ if (items.length === 0 && locale !== "en") {
51
+ (0, logs_1.log)(`Slug not found for locale "${locale}", falling back to "en"`);
52
+ opts = {
53
+ content_type: "page",
54
+ "fields.slug.en": slug,
55
+ limit: 1,
56
+ include: 1,
57
+ };
58
+ ({ items } = await env.getEntries(opts));
59
+ }
60
+ return items.length > 0 ? items[0] : null;
61
+ };
62
+ const createTopicSeo = async (metaTitle, metaDescription) => {
63
+ const env = await (0, contentful_1.getEnvironment)();
64
+ const hasMetaTitle = Object.keys(metaTitle).length > 0;
65
+ const hasMetaDescription = Object.keys(metaDescription).length > 0;
66
+ const fields = {
67
+ metaTitle: hasMetaTitle ? metaTitle : { en: "" },
68
+ metaDescription: hasMetaDescription ? metaDescription : { en: "" },
69
+ };
70
+ const entryData = { fields };
71
+ const topicSeo = await env.createEntry("topicSeo", entryData);
72
+ await topicSeo.publish();
73
+ return topicSeo;
74
+ };
75
+ const updateTopicSeo = async (topicSeoEntry, metaTitle, metaDescription) => {
76
+ const currentFields = topicSeoEntry.fields;
77
+ const newMetaTitle = {};
78
+ for (const locale of LOCALES) {
79
+ if (metaTitle[locale] && metaTitle[locale].trim() !== "") {
80
+ newMetaTitle[locale] = metaTitle[locale];
81
+ }
82
+ else if (currentFields.metaTitle?.[locale]) {
83
+ newMetaTitle[locale] = currentFields.metaTitle[locale];
84
+ }
85
+ }
86
+ const newMetaDescription = {};
87
+ for (const locale of LOCALES) {
88
+ if (metaDescription[locale] && metaDescription[locale].trim() !== "") {
89
+ newMetaDescription[locale] = metaDescription[locale];
90
+ }
91
+ else if (currentFields.metaDescription?.[locale]) {
92
+ newMetaDescription[locale] = currentFields.metaDescription[locale];
93
+ }
94
+ }
95
+ const updateData = {
96
+ fields: {
97
+ metaTitle: newMetaTitle,
98
+ metaDescription: newMetaDescription,
99
+ },
100
+ };
101
+ const updatedEntry = await (0, contentful_1.updateEntry)(topicSeoEntry, updateData, true);
102
+ return updatedEntry;
103
+ };
104
+ const associateTopicSeoToPage = async (pageEntry, topicSeoEntry) => {
105
+ pageEntry.fields.seo = {
106
+ en: {
107
+ sys: {
108
+ type: "Link",
109
+ linkType: "Entry",
110
+ id: topicSeoEntry.sys.id,
111
+ },
112
+ },
113
+ };
114
+ const updatedPage = await pageEntry.update();
115
+ await updatedPage.publish();
116
+ return updatedPage;
117
+ };
118
+ const importSeoFromCsv = async (s3CsvPath, offset = 0, limit = 50) => {
119
+ const errors = [];
120
+ let processed = 0;
121
+ let created = 0;
122
+ let updated = 0;
123
+ let skipped = 0;
124
+ (0, logs_1.log)(`Importing SEO from CSV: ${s3CsvPath} (offset: ${offset}, limit: ${limit})`);
125
+ let csvData;
126
+ try {
127
+ csvData = await parseCsvFromS3(s3CsvPath);
128
+ }
129
+ catch (err) {
130
+ throw new Error(`Failed to parse CSV from S3: ${s3CsvPath}. ${err?.message || err}`);
131
+ }
132
+ const total = csvData.length;
133
+ (0, logs_1.log)(`CSV loaded: ${total} rows`);
134
+ let count = 0;
135
+ for (const row of csvData) {
136
+ if (offset <= count && (limit === -1 || processed < limit)) {
137
+ const slugPath = row.slug?.trim();
138
+ if (!slugPath) {
139
+ errors.push(`Row ${count}: slug is empty, skipping`);
140
+ (0, logs_1.log)(`Row ${count}: slug is empty, skipping`, "WARN");
141
+ count++;
142
+ processed++;
143
+ continue;
144
+ }
145
+ const parsed = parseLocalizedSlug(slugPath);
146
+ if (!parsed) {
147
+ errors.push(`Row ${count}: invalid slug path format "${slugPath}", skipping`);
148
+ (0, logs_1.log)(`Row ${count}: invalid slug path format "${slugPath}", skipping`, "WARN");
149
+ count++;
150
+ processed++;
151
+ continue;
152
+ }
153
+ const { locale, slug } = parsed;
154
+ if (!isValidLocale(locale)) {
155
+ errors.push(`Row ${count}: unknown locale "${locale}" in slug path "${slugPath}", skipping`);
156
+ (0, logs_1.log)(`Row ${count}: unknown locale "${locale}" in slug path "${slugPath}", skipping`, "WARN");
157
+ count++;
158
+ processed++;
159
+ continue;
160
+ }
161
+ (0, logs_1.log)(`Processing row ${count}: locale="${locale}", slug="${slug}"`);
162
+ try {
163
+ const pageEntry = await findPageBySlug(locale, slug);
164
+ if (!pageEntry) {
165
+ errors.push(`Page not found for slug="${slug}" (locale=${locale})`);
166
+ (0, logs_1.log)(`Page not found for slug="${slug}" (locale=${locale})`, "WARN");
167
+ processed++;
168
+ count++;
169
+ continue;
170
+ }
171
+ const existingTopicSeo = pageEntry.fields?.seo?.en?.sys?.id;
172
+ const metaTitle = {};
173
+ const metaDescription = {};
174
+ if (row.metaTitle && row.metaTitle.trim() !== "") {
175
+ metaTitle[locale] = row.metaTitle.trim();
176
+ }
177
+ if (row.metaDescription && row.metaDescription.trim() !== "") {
178
+ metaDescription[locale] = row.metaDescription.trim();
179
+ }
180
+ if (existingTopicSeo) {
181
+ const env = await (0, contentful_1.getEnvironment)();
182
+ const topicSeoEntry = await env.getEntry(existingTopicSeo);
183
+ if (topicSeoEntry) {
184
+ await updateTopicSeo(topicSeoEntry, metaTitle, metaDescription);
185
+ (0, logs_1.log)(`Updated topicSeo ${existingTopicSeo} for page ${pageEntry.sys.id}`);
186
+ updated++;
187
+ }
188
+ else {
189
+ errors.push(`topicSeo ${existingTopicSeo} not found for page slug="${slug}" (locale=${locale})`);
190
+ (0, logs_1.log)(`topicSeo ${existingTopicSeo} not found for page slug="${slug}" (locale=${locale})`, "WARN");
191
+ skipped++;
192
+ }
193
+ }
194
+ else {
195
+ const newTopicSeo = await createTopicSeo(metaTitle, metaDescription);
196
+ await associateTopicSeoToPage(pageEntry, newTopicSeo);
197
+ (0, logs_1.log)(`Created and associated topicSeo ${newTopicSeo.sys.id} to page ${pageEntry.sys.id}`);
198
+ created++;
199
+ }
200
+ }
201
+ catch (err) {
202
+ const errorMsg = `Error processing slug="${slug}" (locale=${locale}): ${err?.message || err}`;
203
+ errors.push(errorMsg);
204
+ (0, logs_1.log)(errorMsg, "ERROR");
205
+ }
206
+ processed++;
207
+ }
208
+ count++;
209
+ }
210
+ const nextOffset = offset + processed;
211
+ return {
212
+ offset: nextOffset,
213
+ limit,
214
+ completed: nextOffset >= total,
215
+ total,
216
+ processed,
217
+ created,
218
+ updated,
219
+ skipped,
220
+ errors,
221
+ };
222
+ };
223
+ exports.importSeoFromCsv = importSeoFromCsv;
224
+ exports.default = exports.importSeoFromCsv;
@@ -1,34 +1,71 @@
1
1
  export interface DProductSubLine {
2
+ parentName: string;
2
3
  code: string;
3
- image?: null;
4
- imageAlternative?: null;
4
+ image: string | null;
5
+ imageAlternative: string | null;
5
6
  value_en: string;
6
- value_en_US: string;
7
7
  value_it: string;
8
8
  value_es: string;
9
9
  value_de: string;
10
10
  value_fr: string;
11
- value_sv: string;
12
- value_no: string;
13
- value_da: string;
14
- value_ru: string;
15
- othersData: OthersData;
16
- }
17
- export interface OthersData {
11
+ value_sv: string | null;
12
+ value_no: string | null;
13
+ value_da: string | null;
14
+ value_ru: string | null;
15
+ value_en_US: string | null;
16
+ value_ja: string | null;
17
+ value_zh: string | null;
18
+ value_ko: string | null;
19
+ note_en: string | null;
20
+ note_it: string | null;
21
+ note_es: string | null;
22
+ note_de: string | null;
23
+ note_fr: string | null;
24
+ note_sv: string | null;
25
+ note_no: string | null;
26
+ note_da: string | null;
27
+ note_ru: string | null;
28
+ note_en_US: string | null;
29
+ note_zh: string | null;
30
+ note_ja: string | null;
31
+ note_ko: string | null;
32
+ imgRelUrl: string | null;
33
+ imgAltRelUrl: string | null;
34
+ priority: number | null;
18
35
  productLine: ProductLine;
19
36
  }
20
37
  export interface ProductLine {
38
+ parentName: string;
21
39
  code: string;
22
- image?: null;
23
- imageAlternative?: null;
40
+ image: string | null;
41
+ imageAlternative: string | null;
24
42
  value_en: string;
25
43
  value_it: string;
26
44
  value_es: string;
27
45
  value_de: string;
28
46
  value_fr: string;
29
- value_sv: string;
30
- value_no: string;
31
- value_da: string;
32
- value_ru: string;
33
- value_en_US?: null;
47
+ value_sv: string | null;
48
+ value_no: string | null;
49
+ value_da: string | null;
50
+ value_ru: string | null;
51
+ value_en_US: string | null;
52
+ value_ja: string | null;
53
+ value_zh: string | null;
54
+ value_ko: string | null;
55
+ note_en: string | null;
56
+ note_it: string | null;
57
+ note_es: string | null;
58
+ note_de: string | null;
59
+ note_fr: string | null;
60
+ note_sv: string | null;
61
+ note_no: string | null;
62
+ note_da: string | null;
63
+ note_ru: string | null;
64
+ note_en_US: string | null;
65
+ note_zh: string | null;
66
+ note_ja: string | null;
67
+ note_ko: string | null;
68
+ imgRelUrl: string | null;
69
+ imgAltRelUrl: string | null;
70
+ priority: number | null;
34
71
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pim-import",
3
- "version": "6.11.0",
3
+ "version": "6.12.0",
4
4
  "description": "",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",