From f2f0d8644b6aba6c27fadd2c83a377e1dd1f7198 Mon Sep 17 00:00:00 2001 From: vadim Date: Sat, 4 Apr 2026 01:07:22 +0700 Subject: [PATCH 1/9] CDX-339 Add support for format query parameter --- spec/src/modules/catalog/catalog-files.js | 354 ++++++++++++++++++++++ src/modules/catalog.js | 52 +++- 2 files changed, 390 insertions(+), 16 deletions(-) diff --git a/spec/src/modules/catalog/catalog-files.js b/spec/src/modules/catalog/catalog-files.js index 2090369a..53f07ce0 100644 --- a/spec/src/modules/catalog/catalog-files.js +++ b/spec/src/modules/catalog/catalog-files.js @@ -258,6 +258,65 @@ describe('ConstructorIO - Catalog', () => { }); }); + it('Should replace a catalog of items with format parameter set to csv', (done) => { + const { catalog } = new ConstructorIO({ + ...validOptions, + fetch: fetchSpy, + }); + + const data = { + items: itemsBuffer, + section: 'Products', + format: 'csv', + }; + + catalog.replaceCatalog(data).then((res) => { + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + + expect(requestedUrlParams).to.have.property('format').to.equal('csv'); + expect(res).to.have.property('task_id'); + expect(res).to.have.property('task_status_path'); + done(); + }); + }); + + it('Should replace a catalog of items with format parameter set to jsonl', (done) => { + const { catalog } = new ConstructorIO({ + ...validOptions, + fetch: fetchSpy, + }); + + const data = { + items: itemsBuffer, + section: 'Products', + format: 'jsonl', + }; + + catalog.replaceCatalog(data).then((res) => { + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + + expect(requestedUrlParams).to.have.property('format').to.equal('jsonl'); + expect(res).to.have.property('task_id'); + expect(res).to.have.property('task_status_path'); + done(); + }); + }); + + it('Should be rejected when invalid format parameter is provided', () => { + const { catalog } = new ConstructorIO({ + ...validOptions, + fetch: fetchSpy, + }); + + const data = { + items: itemsBuffer, + section: 'Products', + format: 'xml', + }; + + return expect(catalog.replaceCatalog(data)).to.eventually.be.rejectedWith('format must be one of csv, jsonl'); + }); + if (!skipNetworkTimeoutTests) { it('Should be rejected when network request timeout is provided and reached', () => { const { catalog } = new ConstructorIO(validOptions); @@ -374,6 +433,65 @@ describe('ConstructorIO - Catalog', () => { }); }); + it('Should replace a catalog using tar archive with format parameter set to csv', (done) => { + const { catalog } = new ConstructorIO({ + ...validOptions, + fetch: fetchSpy, + }); + + const data = { + tarArchive: tarArchiveBuffer, + section: 'Products', + format: 'csv', + }; + + catalog.replaceCatalogUsingTarArchive(data).then((res) => { + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + + expect(requestedUrlParams).to.have.property('format').to.equal('csv'); + expect(res).to.have.property('task_id'); + expect(res).to.have.property('task_status_path'); + done(); + }); + }); + + it('Should replace a catalog using tar archive with format parameter set to jsonl', (done) => { + const { catalog } = new ConstructorIO({ + ...validOptions, + fetch: fetchSpy, + }); + + const data = { + tarArchive: tarArchiveBuffer, + section: 'Products', + format: 'jsonl', + }; + + catalog.replaceCatalogUsingTarArchive(data).then((res) => { + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + + expect(requestedUrlParams).to.have.property('format').to.equal('jsonl'); + expect(res).to.have.property('task_id'); + expect(res).to.have.property('task_status_path'); + done(); + }); + }); + + it('Should be rejected when invalid format parameter is provided', () => { + const { catalog } = new ConstructorIO({ + ...validOptions, + fetch: fetchSpy, + }); + + const data = { + tarArchive: tarArchiveBuffer, + section: 'Products', + format: 'xml', + }; + + return expect(catalog.replaceCatalogUsingTarArchive(data)).to.eventually.be.rejectedWith('format must be one of csv, jsonl'); + }); + if (!skipNetworkTimeoutTests) { it('Should be rejected when network request timeout is provided and reached', () => { const { catalog } = new ConstructorIO(validOptions); @@ -569,6 +687,65 @@ describe('ConstructorIO - Catalog', () => { }); }); + it('Should update a catalog of items with format parameter set to csv', (done) => { + const { catalog } = new ConstructorIO({ + ...validOptions, + fetch: fetchSpy, + }); + + const data = { + items: itemsBuffer, + section: 'Products', + format: 'csv', + }; + + catalog.updateCatalog(data).then((res) => { + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + + expect(requestedUrlParams).to.have.property('format').to.equal('csv'); + expect(res).to.have.property('task_id'); + expect(res).to.have.property('task_status_path'); + done(); + }); + }); + + it('Should update a catalog of items with format parameter set to jsonl', (done) => { + const { catalog } = new ConstructorIO({ + ...validOptions, + fetch: fetchSpy, + }); + + const data = { + items: itemsBuffer, + section: 'Products', + format: 'jsonl', + }; + + catalog.updateCatalog(data).then((res) => { + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + + expect(requestedUrlParams).to.have.property('format').to.equal('jsonl'); + expect(res).to.have.property('task_id'); + expect(res).to.have.property('task_status_path'); + done(); + }); + }); + + it('Should be rejected when invalid format parameter is provided', () => { + const { catalog } = new ConstructorIO({ + ...validOptions, + fetch: fetchSpy, + }); + + const data = { + items: itemsBuffer, + section: 'Products', + format: 'xml', + }; + + return expect(catalog.updateCatalog(data)).to.eventually.be.rejectedWith('format must be one of csv, jsonl'); + }); + if (!skipNetworkTimeoutTests) { it('Should be rejected when network request timeout is provided and reached', () => { const { catalog } = new ConstructorIO(validOptions); @@ -686,6 +863,65 @@ describe('ConstructorIO - Catalog', () => { }); }); + it('Should update a catalog using tar archive with format parameter set to csv', (done) => { + const { catalog } = new ConstructorIO({ + ...validOptions, + fetch: fetchSpy, + }); + + const data = { + tarArchive: tarArchiveBuffer, + section: 'Products', + format: 'csv', + }; + + catalog.updateCatalogUsingTarArchive(data).then((res) => { + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + + expect(requestedUrlParams).to.have.property('format').to.equal('csv'); + expect(res).to.have.property('task_id'); + expect(res).to.have.property('task_status_path'); + done(); + }); + }); + + it('Should update a catalog using tar archive with format parameter set to jsonl', (done) => { + const { catalog } = new ConstructorIO({ + ...validOptions, + fetch: fetchSpy, + }); + + const data = { + tarArchive: tarArchiveBuffer, + section: 'Products', + format: 'jsonl', + }; + + catalog.updateCatalogUsingTarArchive(data).then((res) => { + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + + expect(requestedUrlParams).to.have.property('format').to.equal('jsonl'); + expect(res).to.have.property('task_id'); + expect(res).to.have.property('task_status_path'); + done(); + }); + }); + + it('Should be rejected when invalid format parameter is provided', () => { + const { catalog } = new ConstructorIO({ + ...validOptions, + fetch: fetchSpy, + }); + + const data = { + tarArchive: tarArchiveBuffer, + section: 'Products', + format: 'xml', + }; + + return expect(catalog.updateCatalogUsingTarArchive(data)).to.eventually.be.rejectedWith('format must be one of csv, jsonl'); + }); + if (!skipNetworkTimeoutTests) { it('Should be rejected when network request timeout is provided and reached', () => { const { catalog } = new ConstructorIO(validOptions); @@ -915,6 +1151,65 @@ describe('ConstructorIO - Catalog', () => { return expect(catalog.patchCatalog(data)).to.eventually.be.rejectedWith('onMissing must be one of FAIL, IGNORE, or CREATE'); }); + it('Should patch a catalog of items with format parameter set to csv', (done) => { + const { catalog } = new ConstructorIO({ + ...validOptions, + fetch: fetchSpy, + }); + + const data = { + items: itemsBuffer, + section: 'Products', + format: 'csv', + }; + + catalog.patchCatalog(data).then((res) => { + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + + expect(requestedUrlParams).to.have.property('format').to.equal('csv'); + expect(res).to.have.property('task_id'); + expect(res).to.have.property('task_status_path'); + done(); + }); + }); + + it('Should patch a catalog of items with format parameter set to jsonl', (done) => { + const { catalog } = new ConstructorIO({ + ...validOptions, + fetch: fetchSpy, + }); + + const data = { + items: itemsBuffer, + section: 'Products', + format: 'jsonl', + }; + + catalog.patchCatalog(data).then((res) => { + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + + expect(requestedUrlParams).to.have.property('format').to.equal('jsonl'); + expect(res).to.have.property('task_id'); + expect(res).to.have.property('task_status_path'); + done(); + }); + }); + + it('Should be rejected when invalid format parameter is provided', () => { + const { catalog } = new ConstructorIO({ + ...validOptions, + fetch: fetchSpy, + }); + + const data = { + items: itemsBuffer, + section: 'Products', + format: 'xml', + }; + + return expect(catalog.patchCatalog(data)).to.eventually.be.rejectedWith('format must be one of csv, jsonl'); + }); + if (!skipNetworkTimeoutTests) { it('Should be rejected when network request timeout is provided and reached', () => { const { catalog } = new ConstructorIO(validOptions); @@ -1014,6 +1309,65 @@ describe('ConstructorIO - Catalog', () => { return expect(catalog.patchCatalogUsingTarArchive(data)).to.eventually.be.rejectedWith('onMissing must be one of FAIL, IGNORE, or CREATE'); }); + it('Should patch a catalog using tar archive with format parameter set to csv', (done) => { + const { catalog } = new ConstructorIO({ + ...validOptions, + fetch: fetchSpy, + }); + + const data = { + tarArchive: tarArchiveBuffer, + section: 'Products', + format: 'csv', + }; + + catalog.patchCatalogUsingTarArchive(data).then((res) => { + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + + expect(requestedUrlParams).to.have.property('format').to.equal('csv'); + expect(res).to.have.property('task_id'); + expect(res).to.have.property('task_status_path'); + done(); + }); + }); + + it('Should patch a catalog using tar archive with format parameter set to jsonl', (done) => { + const { catalog } = new ConstructorIO({ + ...validOptions, + fetch: fetchSpy, + }); + + const data = { + tarArchive: tarArchiveBuffer, + section: 'Products', + format: 'jsonl', + }; + + catalog.patchCatalogUsingTarArchive(data).then((res) => { + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + + expect(requestedUrlParams).to.have.property('format').to.equal('jsonl'); + expect(res).to.have.property('task_id'); + expect(res).to.have.property('task_status_path'); + done(); + }); + }); + + it('Should be rejected when invalid format parameter is provided', () => { + const { catalog } = new ConstructorIO({ + ...validOptions, + fetch: fetchSpy, + }); + + const data = { + tarArchive: tarArchiveBuffer, + section: 'Products', + format: 'xml', + }; + + return expect(catalog.patchCatalogUsingTarArchive(data)).to.eventually.be.rejectedWith('format must be one of csv, jsonl'); + }); + if (!skipNetworkTimeoutTests) { it('Should be rejected when network request timeout is provided and reached', () => { const { catalog } = new ConstructorIO(validOptions); diff --git a/src/modules/catalog.js b/src/modules/catalog.js index 17f850aa..c733c002 100644 --- a/src/modules/catalog.js +++ b/src/modules/catalog.js @@ -60,7 +60,7 @@ async function createQueryParamsAndFormData(parameters) { if (parameters) { const { section, notification_email, notificationEmail = notification_email, force, item_groups, onMissing } = parameters; - let { items, variations, itemGroups = item_groups } = parameters; + let { items, variations, format = 'csv', itemGroups = item_groups } = parameters; try { // Convert items to buffer if passed as stream @@ -106,24 +106,32 @@ async function createQueryParamsAndFormData(parameters) { queryParams.on_missing = onMissing; } + if (format) { + if (!['csv', 'jsonl'].includes(format.toLowerCase())) { + throw new Error('format must be one of csv, jsonl'); + } + + queryParams.format = format; + } + // Pull items from parameters if (items) { formData.append('items', items, { - filename: 'items.csv', + filename: 'items.' + format, }); } // Pull variations from parameters if (variations) { formData.append('variations', variations, { - filename: 'variations.csv', + filename: 'variations.' + format, }); } // Pull item groups from parameters if (itemGroups) { formData.append('item_groups', itemGroups, { - filename: 'item_groups.csv', + filename: 'item_groups.' + format, }); } } @@ -2553,9 +2561,10 @@ class Catalog { * @param {string} parameters.section - The section to update * @param {string} [parameters.notificationEmail] - An email address to receive an email notification if the task fails * @param {boolean} [parameters.force=false] - Process the catalog even if it will invalidate a large number of existing items - * @param {file} [parameters.items] - The CSV file with all new items - * @param {file} [parameters.variations] - The CSV file with all new variations - * @param {file} [parameters.itemGroups] - The CSV file with all new itemGroups + * @param {file} [parameters.items] - The file with all new items (csv or jsonl, depending on the format parameter) + * @param {file} [parameters.variations] - The file with all new variations (csv or jsonl, depending on the format parameter) + * @param {file} [parameters.itemGroups] - The file with all new itemGroups (csv or jsonl, depending on the format parameter) + * @param {string} [parameters.format] - File format of the uploaded items and variations files. Can be either csv or jsonl. * @param {object} [networkParameters] - Parameters relevant to the network request * @param {number} [networkParameters.timeout] - Request timeout (in milliseconds) * @returns {Promise} @@ -2567,6 +2576,7 @@ class Catalog { * items: itemsFileBufferOrStream, * variations: variationsFileBufferOrStream, * itemGroups: itemGroupsFileBufferOrStream, + * format: 'csv' * }); */ async replaceCatalog(parameters = {}, networkParameters = {}) { @@ -2605,9 +2615,10 @@ class Catalog { * @param {string} parameters.section - The section to update * @param {string} [parameters.notificationEmail] - An email address to receive an email notification if the task fails * @param {boolean} [parameters.force=false] - Process the catalog even if it will invalidate a large number of existing items - * @param {file} [parameters.items] - The CSV file with all new items - * @param {file} [parameters.variations] - The CSV file with all new variations - * @param {file} [parameters.itemGroups] - The CSV file with all new itemGroups + * @param {file} [parameters.items] - The file with all new items (csv or jsonl, depending on the format parameter) + * @param {file} [parameters.variations] - The file with all new variations (csv or jsonl, depending on the format parameter) + * @param {file} [parameters.itemGroups] - The file with all new itemGroups (csv or jsonl, depending on the format parameter) + * @param {string} [parameters.format] - File format of the uploaded items and variations files. Can be either csv or jsonl. * @param {object} [networkParameters] - Parameters relevant to the network request * @param {number} [networkParameters.timeout] - Request timeout (in milliseconds) * @returns {Promise} @@ -2619,6 +2630,7 @@ class Catalog { * items: itemsFileBufferOrStream, * variations: variationsFileBufferOrStream, * itemGroups: itemGroupsFileBufferOrStream, + * format: 'csv' * }); */ async updateCatalog(parameters = {}, networkParameters = {}) { @@ -2658,9 +2670,10 @@ class Catalog { * @param {string} [parameters.notificationEmail] - An email address to receive an email notification if the task fails * @param {boolean} [parameters.force=false] - Process the catalog even if it will invalidate a large number of existing items * @param {string} [parameters.onMissing="FAIL"] - Defines the strategy for handling items which are present in the file and missing in the system. IGNORE silently prevents adding them to the system, CREATE creates them, FAIL fails the ingestion in case of their presence - * @param {file} [parameters.items] - The CSV file with all new items - * @param {file} [parameters.variations] - The CSV file with all new variations - * @param {file} [parameters.itemGroups] - The CSV file with all new itemGroups + * @param {file} [parameters.items] - The file with all new items (csv or jsonl, depending on the format parameter) + * @param {file} [parameters.variations] - The file with all new variations (csv or jsonl, depending on the format parameter) + * @param {file} [parameters.itemGroups] - The file with all new itemGroups (csv or jsonl, depending on the format parameter) + * @param {string} [parameters.format] - File format of the uploaded items and variations files. Can be either csv or jsonl. * @param {object} [networkParameters] - Parameters relevant to the network request * @param {number} [networkParameters.timeout] - Request timeout (in milliseconds) * @returns {Promise} @@ -2672,6 +2685,7 @@ class Catalog { * items: itemsFileBufferOrStream, * variations: variationsFileBufferOrStream, * itemGroups: itemGroupsFileBufferOrStream, + * format: 'csv' * }); */ async patchCatalog(parameters = {}, networkParameters = {}) { @@ -2710,7 +2724,8 @@ class Catalog { * @param {string} parameters.section - The section to update * @param {string} [parameters.notificationEmail] - An email address to receive an email notification if the task fails * @param {boolean} [parameters.force=false] - Process the catalog even if it will invalidate a large number of existing items - * @param {file} [parameters.tarArchive] - The tar file that includes csv files + * @param {file} [parameters.tarArchive] - The tar file that includes catalog files (csv or jsonl, depending on the format parameter) + * @param {string} [parameters.format] - File format of the uploaded items and variations files. Can be either csv or jsonl. * @param {object} [networkParameters] - Parameters relevant to the network request * @param {number} [networkParameters.timeout] - Request timeout (in milliseconds) * @returns {Promise} @@ -2720,6 +2735,7 @@ class Catalog { * section: 'Products', * notificationEmail: 'notifications@example.com', * tarArchive: tarArchiveBufferOrStream, + * format: 'csv' * }); */ async replaceCatalogUsingTarArchive(parameters = {}, networkParameters = {}) { @@ -2766,7 +2782,8 @@ class Catalog { * @param {string} parameters.section - The section to update * @param {string} [parameters.notificationEmail] - An email address to receive an email notification if the task fails * @param {boolean} [parameters.force=false] - Process the catalog even if it will invalidate a large number of existing items - * @param {file} [parameters.tarArchive] - The tar file that includes csv files + * @param {file} [parameters.tarArchive] - The tar file that includes catalog files (csv or jsonl, depending on the format parameter) + * @param {string} [parameters.format] - File format of the uploaded items and variations files. Can be either csv or jsonl. * @param {object} [networkParameters] - Parameters relevant to the network request * @param {number} [networkParameters.timeout] - Request timeout (in milliseconds) * @returns {Promise} @@ -2776,6 +2793,7 @@ class Catalog { * section: 'Products', * notificationEmail: 'notifications@example.com', * tarArchive: tarArchiveBufferOrStream, + * format: 'csv' * }); */ async updateCatalogUsingTarArchive(parameters = {}, networkParameters = {}) { @@ -2825,7 +2843,8 @@ class Catalog { * @param {string} [parameters.notificationEmail] - An email address to receive an email notification if the task fails * @param {boolean} [parameters.force=false] - Process the catalog even if it will invalidate a large number of existing items * @param {string} [parameters.onMissing="FAIL"] - Defines the strategy for handling items which are present in the file and missing in the system. IGNORE silently prevents adding them to the system, CREATE creates them, FAIL fails the ingestion in case of their presence - * @param {file} [parameters.tarArchive] - The tar file that includes csv files + * @param {file} [parameters.tarArchive] - The tar file that includes catalog files (csv or jsonl, depending on the format parameter) + * @param {string} [parameters.format] - File format of the uploaded items and variations files. Can be either csv or jsonl. * @param {object} [networkParameters] - Parameters relevant to the network request * @param {number} [networkParameters.timeout] - Request timeout (in milliseconds) * @returns {Promise} @@ -2835,6 +2854,7 @@ class Catalog { * section: 'Products', * notificationEmail: 'notifications@example.com', * tarArchive: tarArchiveBufferOrStream, + * format: 'csv' * }); */ async patchCatalogUsingTarArchive(parameters = {}, networkParameters = {}) { From 3aa73a2d89c22524bac3de1cf2d682dafe421250 Mon Sep 17 00:00:00 2001 From: vadim Date: Sat, 4 Apr 2026 01:09:26 +0700 Subject: [PATCH 2/9] lint --- src/modules/catalog.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/modules/catalog.js b/src/modules/catalog.js index c733c002..8f17ec15 100644 --- a/src/modules/catalog.js +++ b/src/modules/catalog.js @@ -59,8 +59,8 @@ async function createQueryParamsAndFormData(parameters) { const formData = new FormData(); if (parameters) { - const { section, notification_email, notificationEmail = notification_email, force, item_groups, onMissing } = parameters; - let { items, variations, format = 'csv', itemGroups = item_groups } = parameters; + const { section, notification_email, notificationEmail = notification_email, force, item_groups, onMissing, format = 'csv' } = parameters; + let { items, variations, itemGroups = item_groups } = parameters; try { // Convert items to buffer if passed as stream @@ -117,21 +117,21 @@ async function createQueryParamsAndFormData(parameters) { // Pull items from parameters if (items) { formData.append('items', items, { - filename: 'items.' + format, + filename: `items.${format}`, }); } // Pull variations from parameters if (variations) { formData.append('variations', variations, { - filename: 'variations.' + format, + filename: `variations.${format}`, }); } // Pull item groups from parameters if (itemGroups) { formData.append('item_groups', itemGroups, { - filename: 'item_groups.' + format, + filename: `item_groups.${format}`, }); } } From 9061847fb3a7f23c38af645857c6bfe4ac217755 Mon Sep 17 00:00:00 2001 From: Enes Kutay SEZEN Date: Mon, 6 Apr 2026 17:25:18 +0300 Subject: [PATCH 3/9] Update types --- src/types/catalog.d.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/types/catalog.d.ts b/src/types/catalog.d.ts index ff1af734..3b3e97d0 100644 --- a/src/types/catalog.d.ts +++ b/src/types/catalog.d.ts @@ -173,6 +173,7 @@ export interface ReplaceCatalogParameters { items?: File | fs.ReadStream | Duplex variations?: File | fs.ReadStream | Duplex; itemGroups?: File | fs.ReadStream | Duplex; + format?: 'csv' | 'jsonl'; } export interface UpdateCatalogParameters extends ReplaceCatalogParameters {} @@ -185,6 +186,7 @@ export interface PatchCatalogParameters { items?: File | fs.ReadStream | Duplex variations?: File | fs.ReadStream | Duplex; itemGroups?: File | fs.ReadStream | Duplex; + format?: 'csv' | 'jsonl'; } export interface ReplaceCatalogUsingTarArchiveParameters { @@ -192,6 +194,7 @@ export interface ReplaceCatalogUsingTarArchiveParameters { notificationEmail?: string; force?: boolean; tarArchive?: File | fs.ReadStream | Duplex; + format?: 'csv' | 'jsonl'; } export interface UpdateCatalogUsingTarArchiveParameters From 660901232a670f0727f0c54a76232f0833309617 Mon Sep 17 00:00:00 2001 From: Enes Kutay SEZEN Date: Mon, 6 Apr 2026 17:26:10 +0300 Subject: [PATCH 4/9] Update implementation --- src/modules/catalog.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/modules/catalog.js b/src/modules/catalog.js index 8f17ec15..6c290f44 100644 --- a/src/modules/catalog.js +++ b/src/modules/catalog.js @@ -106,32 +106,32 @@ async function createQueryParamsAndFormData(parameters) { queryParams.on_missing = onMissing; } - if (format) { - if (!['csv', 'jsonl'].includes(format.toLowerCase())) { - throw new Error('format must be one of csv, jsonl'); - } + const normalizedFormat = format.toLowerCase(); - queryParams.format = format; + if (!['csv', 'jsonl'].includes(normalizedFormat)) { + throw new Error('format must be csv or jsonl'); } + queryParams.format = normalizedFormat; + // Pull items from parameters if (items) { formData.append('items', items, { - filename: `items.${format}`, + filename: `items.${normalizedFormat}`, }); } // Pull variations from parameters if (variations) { formData.append('variations', variations, { - filename: `variations.${format}`, + filename: `variations.${normalizedFormat}`, }); } // Pull item groups from parameters if (itemGroups) { formData.append('item_groups', itemGroups, { - filename: `item_groups.${format}`, + filename: `item_groups.${normalizedFormat}`, }); } } From 123c906a3cb991d89e4003cfdf6854e7d17ebe0d Mon Sep 17 00:00:00 2001 From: Enes Kutay SEZEN Date: Mon, 6 Apr 2026 17:26:14 +0300 Subject: [PATCH 5/9] Add new test --- spec/src/modules/catalog/catalog-files.js | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/spec/src/modules/catalog/catalog-files.js b/spec/src/modules/catalog/catalog-files.js index 53f07ce0..b6b138ea 100644 --- a/spec/src/modules/catalog/catalog-files.js +++ b/spec/src/modules/catalog/catalog-files.js @@ -258,6 +258,27 @@ describe('ConstructorIO - Catalog', () => { }); }); + it('Should replace a catalog of items with default format parameter (csv) when format is not specified', (done) => { + const { catalog } = new ConstructorIO({ + ...validOptions, + fetch: fetchSpy, + }); + + const data = { + items: itemsBuffer, + section: 'Products', + }; + + catalog.replaceCatalog(data).then((res) => { + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + + expect(requestedUrlParams).to.have.property('format').to.equal('csv'); + expect(res).to.have.property('task_id'); + expect(res).to.have.property('task_status_path'); + done(); + }); + }); + it('Should replace a catalog of items with format parameter set to csv', (done) => { const { catalog } = new ConstructorIO({ ...validOptions, From 33fce75a06b35890dc27329b5aada3c06fb781c5 Mon Sep 17 00:00:00 2001 From: Enes Kutay SEZEN Date: Mon, 6 Apr 2026 17:29:49 +0300 Subject: [PATCH 6/9] Update tests --- spec/src/modules/catalog/catalog-files.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/spec/src/modules/catalog/catalog-files.js b/spec/src/modules/catalog/catalog-files.js index b6b138ea..812944d8 100644 --- a/spec/src/modules/catalog/catalog-files.js +++ b/spec/src/modules/catalog/catalog-files.js @@ -335,7 +335,7 @@ describe('ConstructorIO - Catalog', () => { format: 'xml', }; - return expect(catalog.replaceCatalog(data)).to.eventually.be.rejectedWith('format must be one of csv, jsonl'); + return expect(catalog.replaceCatalog(data)).to.eventually.be.rejectedWith('format must be csv or jsonl'); }); if (!skipNetworkTimeoutTests) { @@ -510,7 +510,7 @@ describe('ConstructorIO - Catalog', () => { format: 'xml', }; - return expect(catalog.replaceCatalogUsingTarArchive(data)).to.eventually.be.rejectedWith('format must be one of csv, jsonl'); + return expect(catalog.replaceCatalogUsingTarArchive(data)).to.eventually.be.rejectedWith('format must be csv or jsonl'); }); if (!skipNetworkTimeoutTests) { @@ -764,7 +764,7 @@ describe('ConstructorIO - Catalog', () => { format: 'xml', }; - return expect(catalog.updateCatalog(data)).to.eventually.be.rejectedWith('format must be one of csv, jsonl'); + return expect(catalog.updateCatalog(data)).to.eventually.be.rejectedWith('format must be csv or jsonl'); }); if (!skipNetworkTimeoutTests) { @@ -940,7 +940,7 @@ describe('ConstructorIO - Catalog', () => { format: 'xml', }; - return expect(catalog.updateCatalogUsingTarArchive(data)).to.eventually.be.rejectedWith('format must be one of csv, jsonl'); + return expect(catalog.updateCatalogUsingTarArchive(data)).to.eventually.be.rejectedWith('format must be csv or jsonl'); }); if (!skipNetworkTimeoutTests) { @@ -1228,7 +1228,7 @@ describe('ConstructorIO - Catalog', () => { format: 'xml', }; - return expect(catalog.patchCatalog(data)).to.eventually.be.rejectedWith('format must be one of csv, jsonl'); + return expect(catalog.patchCatalog(data)).to.eventually.be.rejectedWith('format must be csv or jsonl'); }); if (!skipNetworkTimeoutTests) { @@ -1386,7 +1386,7 @@ describe('ConstructorIO - Catalog', () => { format: 'xml', }; - return expect(catalog.patchCatalogUsingTarArchive(data)).to.eventually.be.rejectedWith('format must be one of csv, jsonl'); + return expect(catalog.patchCatalogUsingTarArchive(data)).to.eventually.be.rejectedWith('format must be csv or jsonl'); }); if (!skipNetworkTimeoutTests) { From 2f98b547ed7eda07e1faea4d1e9dbae29950a2c8 Mon Sep 17 00:00:00 2001 From: Enes Kutay SEZEN Date: Mon, 6 Apr 2026 17:39:40 +0300 Subject: [PATCH 7/9] Add tests --- spec/src/modules/catalog/catalog-files.js | 105 ++++++++++++++++++++++ 1 file changed, 105 insertions(+) diff --git a/spec/src/modules/catalog/catalog-files.js b/spec/src/modules/catalog/catalog-files.js index 812944d8..f9b881e4 100644 --- a/spec/src/modules/catalog/catalog-files.js +++ b/spec/src/modules/catalog/catalog-files.js @@ -454,6 +454,27 @@ describe('ConstructorIO - Catalog', () => { }); }); + it('Should replace a catalog using tar archive with default format parameter (csv) when format is not specified', (done) => { + const { catalog } = new ConstructorIO({ + ...validOptions, + fetch: fetchSpy, + }); + + const data = { + tarArchive: tarArchiveBuffer, + section: 'Products', + }; + + catalog.replaceCatalogUsingTarArchive(data).then((res) => { + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + + expect(requestedUrlParams).to.have.property('format').to.equal('csv'); + expect(res).to.have.property('task_id'); + expect(res).to.have.property('task_status_path'); + done(); + }); + }); + it('Should replace a catalog using tar archive with format parameter set to csv', (done) => { const { catalog } = new ConstructorIO({ ...validOptions, @@ -708,6 +729,27 @@ describe('ConstructorIO - Catalog', () => { }); }); + it('Should update a catalog of items with default format parameter (csv) when format is not specified', (done) => { + const { catalog } = new ConstructorIO({ + ...validOptions, + fetch: fetchSpy, + }); + + const data = { + items: itemsBuffer, + section: 'Products', + }; + + catalog.updateCatalog(data).then((res) => { + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + + expect(requestedUrlParams).to.have.property('format').to.equal('csv'); + expect(res).to.have.property('task_id'); + expect(res).to.have.property('task_status_path'); + done(); + }); + }); + it('Should update a catalog of items with format parameter set to csv', (done) => { const { catalog } = new ConstructorIO({ ...validOptions, @@ -884,6 +926,27 @@ describe('ConstructorIO - Catalog', () => { }); }); + it('Should update a catalog using tar archive with default format parameter (csv) when format is not specified', (done) => { + const { catalog } = new ConstructorIO({ + ...validOptions, + fetch: fetchSpy, + }); + + const data = { + tarArchive: tarArchiveBuffer, + section: 'Products', + }; + + catalog.updateCatalogUsingTarArchive(data).then((res) => { + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + + expect(requestedUrlParams).to.have.property('format').to.equal('csv'); + expect(res).to.have.property('task_id'); + expect(res).to.have.property('task_status_path'); + done(); + }); + }); + it('Should update a catalog using tar archive with format parameter set to csv', (done) => { const { catalog } = new ConstructorIO({ ...validOptions, @@ -1172,6 +1235,27 @@ describe('ConstructorIO - Catalog', () => { return expect(catalog.patchCatalog(data)).to.eventually.be.rejectedWith('onMissing must be one of FAIL, IGNORE, or CREATE'); }); + it('Should patch a catalog of items with default format parameter (csv) when format is not specified', (done) => { + const { catalog } = new ConstructorIO({ + ...validOptions, + fetch: fetchSpy, + }); + + const data = { + items: itemsBuffer, + section: 'Products', + }; + + catalog.patchCatalog(data).then((res) => { + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + + expect(requestedUrlParams).to.have.property('format').to.equal('csv'); + expect(res).to.have.property('task_id'); + expect(res).to.have.property('task_status_path'); + done(); + }); + }); + it('Should patch a catalog of items with format parameter set to csv', (done) => { const { catalog } = new ConstructorIO({ ...validOptions, @@ -1330,6 +1414,27 @@ describe('ConstructorIO - Catalog', () => { return expect(catalog.patchCatalogUsingTarArchive(data)).to.eventually.be.rejectedWith('onMissing must be one of FAIL, IGNORE, or CREATE'); }); + it('Should patch a catalog using tar archive with default format parameter (csv) when format is not specified', (done) => { + const { catalog } = new ConstructorIO({ + ...validOptions, + fetch: fetchSpy, + }); + + const data = { + tarArchive: tarArchiveBuffer, + section: 'Products', + }; + + catalog.patchCatalogUsingTarArchive(data).then((res) => { + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + + expect(requestedUrlParams).to.have.property('format').to.equal('csv'); + expect(res).to.have.property('task_id'); + expect(res).to.have.property('task_status_path'); + done(); + }); + }); + it('Should patch a catalog using tar archive with format parameter set to csv', (done) => { const { catalog } = new ConstructorIO({ ...validOptions, From ac77cc701f91a7d2c4f8569c6dd364219017a23a Mon Sep 17 00:00:00 2001 From: Enes Kutay SEZEN Date: Mon, 6 Apr 2026 17:40:11 +0300 Subject: [PATCH 8/9] Address comments --- src/modules/catalog.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/modules/catalog.js b/src/modules/catalog.js index 6c290f44..b6e8c611 100644 --- a/src/modules/catalog.js +++ b/src/modules/catalog.js @@ -106,12 +106,12 @@ async function createQueryParamsAndFormData(parameters) { queryParams.on_missing = onMissing; } - const normalizedFormat = format.toLowerCase(); - - if (!['csv', 'jsonl'].includes(normalizedFormat)) { + if (typeof format !== 'string' || !['csv', 'jsonl'].includes(format.toLowerCase())) { throw new Error('format must be csv or jsonl'); } + const normalizedFormat = format.toLowerCase(); + queryParams.format = normalizedFormat; // Pull items from parameters From 37d7104bb9274e5472005780872739caffcee628 Mon Sep 17 00:00:00 2001 From: Enes Kutay SEZEN Date: Mon, 6 Apr 2026 17:49:08 +0300 Subject: [PATCH 9/9] Update src/modules/catalog.js Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/modules/catalog.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/catalog.js b/src/modules/catalog.js index b6e8c611..083f73f8 100644 --- a/src/modules/catalog.js +++ b/src/modules/catalog.js @@ -2564,7 +2564,7 @@ class Catalog { * @param {file} [parameters.items] - The file with all new items (csv or jsonl, depending on the format parameter) * @param {file} [parameters.variations] - The file with all new variations (csv or jsonl, depending on the format parameter) * @param {file} [parameters.itemGroups] - The file with all new itemGroups (csv or jsonl, depending on the format parameter) - * @param {string} [parameters.format] - File format of the uploaded items and variations files. Can be either csv or jsonl. + * @param {string} [parameters.format] - File format of all uploaded catalog files (items, variations, and itemGroups). Can be either csv or jsonl. * @param {object} [networkParameters] - Parameters relevant to the network request * @param {number} [networkParameters.timeout] - Request timeout (in milliseconds) * @returns {Promise}