Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/yummy-paths-swim.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'owox': minor
---

# Microsoft Ads: AccountID changes to AccountIDs

The Microsoft Ads connector configuration field has been renamed from `AccountID` to `AccountIDs` to better reflect its capability. You can now specify multiple Account IDs (comma-separated) in a single field, allowing you to load data for several accounts using one connector instead of creating separate connectors for each account.

Existing Microsoft Ads connectors will be automatically migrated with no action required on your part.
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { MigrationInterface, QueryRunner } from 'typeorm';

export class RenameMicrosoftAdsAccountIdToAccountIds1767093646000 implements MigrationInterface {
public readonly name = 'RenameMicrosoftAdsAccountIdToAccountIds1767093646000';

public async up(queryRunner: QueryRunner): Promise<void> {
const rows = await queryRunner.query(`
SELECT id, definition
FROM data_mart
WHERE definitionType = 'CONNECTOR'
AND (
definition LIKE '%"source":%"name":"MicrosoftAds"%'
OR definition LIKE '%"source":%"name": "MicrosoftAds"%'
)
AND (
definition LIKE '%"AccountID"%'
)
`);

for (const row of rows) {
const def = typeof row.definition === 'string' ? JSON.parse(row.definition) : row.definition;
const configuration = def?.connector?.source?.configuration;

if (!Array.isArray(configuration)) continue;

let hasChanges = false;
for (const configItem of configuration) {
if (configItem && typeof configItem === 'object' && 'AccountID' in configItem) {
configItem.AccountIDs = configItem.AccountID;
delete configItem.AccountID;
hasChanges = true;
}
}

if (hasChanges) {
await queryRunner.query(`UPDATE data_mart SET definition = ? WHERE id = ?`, [
JSON.stringify(def),
row.id,
]);
}
}
}

public async down(queryRunner: QueryRunner): Promise<void> {
const rows = await queryRunner.query(`
SELECT id, definition
FROM data_mart
WHERE definitionType = 'CONNECTOR'
AND (
definition LIKE '%"source":%"name":"MicrosoftAds"%'
OR definition LIKE '%"source":%"name": "MicrosoftAds"%'
)
`);

for (const row of rows) {
const def = typeof row.definition === 'string' ? JSON.parse(row.definition) : row.definition;
const configuration = def?.connector?.source?.configuration;

if (!Array.isArray(configuration)) continue;

let hasChanges = false;
for (const configItem of configuration) {
if (configItem && typeof configItem === 'object' && 'AccountIDs' in configItem) {
configItem.AccountID = configItem.AccountIDs;
delete configItem.AccountIDs;
hasChanges = true;
}
}

if (hasChanges) {
await queryRunner.query(`UPDATE data_mart SET definition = ? WHERE id = ?`, [
JSON.stringify(def),
row.id,
]);
}
}
}
}
21 changes: 16 additions & 5 deletions packages/connectors/src/Core/Utils/FormatUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@ var FormatUtils = {
* @param {string} [options.stripCharacters] - Characters to remove from ID
* @return {Array<number>} Array of numeric IDs
*/
parseIds: function(idsString, {prefix, stripCharacters = ''}) {
parseIds: function (idsString, { prefix, stripCharacters = '' }) {
return String(idsString)
.split(/[,;]\s*/)
.map(id => this.formatId(id.trim(), {prefix, stripCharacters}));
.map(id => this.formatId(id.trim(), { prefix, stripCharacters }));
},

/**
Expand All @@ -31,11 +31,11 @@ var FormatUtils = {
* @param {string} [options.stripCharacters] - Characters to remove from ID
* @return {number} Numeric ID
*/
formatId: function(id, {prefix, stripCharacters = ''}) {
formatId: function (id, { prefix, stripCharacters = '' }) {
if (stripCharacters) {
id = String(id).split(stripCharacters).join('');
}

if (typeof id === 'string' && id.startsWith(prefix)) {
return parseInt(id.replace(prefix, ''));
}
Expand All @@ -47,11 +47,22 @@ var FormatUtils = {
* @param {string} fieldsString - Fields string in format "nodeName fieldName, nodeName fieldName"
* @return {Object} Object with node names as keys and arrays of field names as values
*/
parseFields: function(fieldsString) {
parseFields: function (fieldsString) {
return fieldsString.split(", ").reduce((acc, pair) => {
let [key, value] = pair.split(" ");
(acc[key] = acc[key] || []).push(value.trim());
return acc;
}, {});
},

/**
* Parse account IDs from a comma/semicolon separated string
* @param {string} accountIdsString - Comma/semicolon separated list of account IDs
* @return {Array<string>} Array of trimmed account IDs (non-empty strings)
*/
parseAccountIds: function (accountIdsString) {
return String(accountIdsString)
.split(/[,;]\s*/)
.filter(id => id.trim().length > 0);
}
};
54 changes: 30 additions & 24 deletions packages/connectors/src/Sources/MicrosoftAds/Connector.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,20 @@ var MicrosoftAdsConnector = class MicrosoftAdsConnector extends AbstractConnecto
* Processes all nodes defined in the fields configuration
*/
async startImportProcess() {
const fields = MicrosoftAdsHelper.parseFields(this.config.Fields.value);

for (const nodeName in fields) {
await this.processNode({
nodeName,
accountId: this.config.AccountID.value,
fields: fields[nodeName] || []
});
const accountIds = FormatUtils.parseAccountIds(this.config.AccountIDs.value);
const fields = MicrosoftAdsHelper.parseFields(this.config.Fields.value);

for (const rawAccountId of accountIds) {
const accountId = rawAccountId.trim();
this.config.logMessage(`Starting import process for Account ID: ${accountId}`);

for (const nodeName in fields) {
await this.processNode({
nodeName,
accountId,
fields: fields[nodeName] || []
});
}
}
}

Expand Down Expand Up @@ -61,7 +67,7 @@ var MicrosoftAdsConnector = class MicrosoftAdsConnector extends AbstractConnecto
*/
async processTimeSeriesNode({ nodeName, accountId, fields }) {
const [startDate, daysToFetch] = this.getStartDateAndDaysToFetch();

if (daysToFetch <= 0) {
console.log('No days to fetch for time series data');
return;
Expand All @@ -71,17 +77,17 @@ var MicrosoftAdsConnector = class MicrosoftAdsConnector extends AbstractConnecto
for (let dayOffset = 0; dayOffset < daysToFetch; dayOffset++) {
const currentDate = new Date(startDate);
currentDate.setDate(currentDate.getDate() + dayOffset);

const formattedDate = DateUtils.formatDate(currentDate);

this.config.logMessage(`Processing ${nodeName} for ${accountId} on ${formattedDate} (day ${dayOffset + 1} of ${daysToFetch})`);

const data = await this.source.fetchData({
nodeName,
accountId,
start_time: formattedDate,
end_time: formattedDate,
fields
const data = await this.source.fetchData({
nodeName,
accountId,
start_time: formattedDate,
end_time: formattedDate,
fields
});

this.config.logMessage(data.length ? `${data.length} rows of ${nodeName} were fetched for ${accountId} on ${formattedDate}` : `No records have been fetched`);
Expand All @@ -99,7 +105,7 @@ var MicrosoftAdsConnector = class MicrosoftAdsConnector extends AbstractConnecto
}
}
}

/**
* Process a catalog node
* @param {Object} options - Processing options
Expand All @@ -109,9 +115,9 @@ var MicrosoftAdsConnector = class MicrosoftAdsConnector extends AbstractConnecto
* @param {Object} options.storage - Storage instance
*/
async processCatalogNode({ nodeName, accountId, fields }) {
const data = await this.source.fetchData({
nodeName,
accountId,
const data = await this.source.fetchData({
nodeName,
accountId,
fields,
onBatchReady: async (batchData) => {
this.config.logMessage(`Saving batch of ${batchData.length} records to storage`);
Expand All @@ -120,7 +126,7 @@ var MicrosoftAdsConnector = class MicrosoftAdsConnector extends AbstractConnecto
await storage.saveData(preparedData);
}
});

this.config.logMessage(data.length ? `${data.length} rows of ${nodeName} were fetched for ${accountId}` : `No records have been fetched`);

if (data.length || this.config.CreateEmptyTables?.value) {
Expand Down Expand Up @@ -148,7 +154,7 @@ var MicrosoftAdsConnector = class MicrosoftAdsConnector extends AbstractConnecto
const uniqueFields = this.source.fieldsSchema[nodeName].uniqueKeys;

this.storages[nodeName] = new globalThis[this.storageName](
this.config.mergeParameters({
this.config.mergeParameters({
DestinationSheetName: { value: this.source.fieldsSchema[nodeName].destinationName },
DestinationTableName: { value: this.getDestinationName(nodeName, this.config, this.source.fieldsSchema[nodeName].destinationName) },
}),
Expand All @@ -159,7 +165,7 @@ var MicrosoftAdsConnector = class MicrosoftAdsConnector extends AbstractConnecto

await this.storages[nodeName].init();
}

return this.storages[nodeName];
}
};
24 changes: 12 additions & 12 deletions packages/connectors/src/Sources/MicrosoftAds/Source.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,11 @@ var MicrosoftAdsSource = class MicrosoftAdsSource extends AbstractSource {
description: "Your Microsoft Ads API Refresh Token",
attributes: [CONFIG_ATTRIBUTES.SECRET]
},
AccountID: {
AccountIDs: {
isRequired: true,
requiredType: "string",
label: "Account ID",
description: "Your Microsoft Ads Account ID"
label: "Account ID(s)",
description: "Your Microsoft Ads Account IDs (comma separated)"
},
CustomerID: {
isRequired: true,
Expand All @@ -56,7 +56,7 @@ var MicrosoftAdsSource = class MicrosoftAdsSource extends AbstractSource {
},
EndDate: {
requiredType: "date",
label: "End Date",
label: "End Date",
description: "End date for data import",
attributes: [CONFIG_ATTRIBUTES.MANUAL_BACKFILL, CONFIG_ATTRIBUTES.HIDE_IN_CONFIG_FORM]
},
Expand Down Expand Up @@ -187,9 +187,9 @@ var MicrosoftAdsSource = class MicrosoftAdsSource extends AbstractSource {
*/
async _fetchCampaignData({ accountId, fields, onBatchReady }) {
await this.getAccessToken();

this.config.logMessage(`Fetching Campaigns, AssetGroups and AdGroups for account ${accountId}...`);

const entityTypes = ['Campaigns', 'AssetGroups', 'AdGroups'];
const allRecords = [];
let campaignRecords = [];
Expand Down Expand Up @@ -234,21 +234,21 @@ var MicrosoftAdsSource = class MicrosoftAdsSource extends AbstractSource {
campaignRecords = records;
}
}

// Save main data immediately
const filteredMainData = MicrosoftAdsHelper.filterByFields(allRecords, fields);
if (filteredMainData.length > 0) {
await onBatchReady(filteredMainData);
await onBatchReady(filteredMainData);
}

// Handle Keywords with batching to avoid 100MB limit
this.config.logMessage(`Fetching Keywords for account ${accountId} (processing by campaigns to avoid size limits)...`);

// Extract campaign IDs from campaigns
const campaignIds = MicrosoftAdsHelper.extractCampaignIds(campaignRecords);
this.config.logMessage(`Found ${campaignIds.length} campaigns, fetching Keywords in batches`);
this.config.logMessage(`Campaign IDs: ${campaignIds.slice(0, 10).join(', ')}${campaignIds.length > 10 ? '...' : ''}`);

let totalFetched = 0;
await this._fetchEntityByCampaigns({
accountId,
Expand Down Expand Up @@ -320,7 +320,7 @@ var MicrosoftAdsSource = class MicrosoftAdsSource extends AbstractSource {

for (let i = 0; i < campaignIds.length; i += batchSize) {
const campaignBatch = campaignIds.slice(i, i + batchSize);
this.config.logMessage(`Fetching ${entityType} for campaigns batch ${Math.floor(i/batchSize) + 1}/${Math.ceil(campaignIds.length/batchSize)} (${campaignBatch.length} campaigns)`);
this.config.logMessage(`Fetching ${entityType} for campaigns batch ${Math.floor(i / batchSize) + 1}/${Math.ceil(campaignIds.length / batchSize)} (${campaignBatch.length} campaigns)`);

try {
const batchRecords = await this._downloadEntityBatch({ accountId, entityType, campaignBatch });
Expand Down
Loading