Skip to content
Open
Show file tree
Hide file tree
Changes from 10 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
17 changes: 16 additions & 1 deletion @types/lib/metadataTypes/Asset.d.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion @types/lib/metadataTypes/Asset.d.ts.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

126 changes: 122 additions & 4 deletions lib/metadataTypes/Asset.js
Original file line number Diff line number Diff line change
Expand Up @@ -848,21 +848,103 @@ class Asset extends MetadataType {
if (Util.OPTIONS.refresh) {
if (createdUpdated.updated) {
// only run this if assets were updated. for created assets we do not expect
await this._refreshTriggeredSend(metadata);
await this.refresh(Object.keys(metadata));
} else {
Util.logger.warn(
'You set the --refresh flag but no updated assets found. Skipping refresh of triggeredSendDefinitions.'
);
}
}
}
/**
* helper for {@link Asset}. finds active emails that reference the updated block
*
* @private
* @param {string[]} keys metadata keys
* @returns {Promise.<object[]>} - array of assets
Comment thread
yuliialikhyt marked this conversation as resolved.
Outdated
*/
static async _findEmailsUsingBlock(keys) {
const uri = '/asset/v1/content/assets/query';

const emailCondition = {
property: 'assetType.name',
simpleOperator: 'in',
value: this.definition.extendedSubTypes.message,
};

// Build metadata keys condition
// @ts-ignore
const metadataCondition = keys.reduce((acc, key, index) => {
const condition = {
property: 'content',
simpleOperator: 'mustContain',
value: key,
};

if (index === 0) {
return condition;
}

return {
leftOperand: acc,
logicalOperator: 'OR',
rightOperand: condition,
};
}, null);
const query = {
leftOperand: emailCondition,
logicalOperator: 'AND',
rightOperand: metadataCondition,
};
/** @type {AssetRequestParams} */
const payload = {
page: {
page: 1,
pageSize: 50,
},
// @ts-ignore
query: query,
};
let items = [];
let moreResults;
let lastPage = 0;
try {
do {
payload.page.page = lastPage + 1;
const response = await this.client.rest.post(uri, payload);
if (response?.items?.length) {
// sometimes the api will return a payload without items
// --> ensure we only add proper items-arrays here
items = items.concat(response.items);
}
// check if any more records
if (response?.message?.includes('all shards failed')) {
payload.query = {
property: 'id',
simpleOperator: 'greaterThan',
value: items.at(-1).id,
};
lastPage = 0;
moreResults = true;
Comment on lines +931 to +937

Copilot AI Mar 24, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In _findEmailsUsingBlock(), the pagination fallback for "all shards failed" sets value: items.at(-1).id. If the first response triggers this condition and returns no items, items.at(-1) is undefined and this will throw, aborting the refresh. Add a guard (e.g., only switch to the greaterThan strategy once at least one item has been collected, otherwise log/return).

Suggested change
payload.query = {
property: 'id',
simpleOperator: 'greaterThan',
value: items.at(-1).id,
};
lastPage = 0;
moreResults = true;
if (!items.length) {
// cannot apply greaterThan pagination without at least one item
Util.logger.warn(
'Asset._findEmailsUsingBlock: received "all shards failed" response without any items; stopping pagination.'
);
moreResults = false;
} else {
payload.query = {
property: 'id',
simpleOperator: 'greaterThan',
value: items.at(-1).id,
};
lastPage = 0;
moreResults = true;
}

Copilot uses AI. Check for mistakes.
} else if (response.page * response.pageSize < response.count) {
moreResults = true;
lastPage = Number(response.page);
} else {
moreResults = false;
}
} while (moreResults);
} catch (ex) {
Util.logger.error(`An error occured while attempting to query assets. ${ex.message}`);
Comment thread
yuliialikhyt marked this conversation as resolved.
Outdated
}
return Object.fromEntries(items.map((item) => [item.customerKey, item]));
}

/**
* helper for {@link Asset.postDeployTasks}. triggers a refresh of active triggerredSendDefinitions associated with the updated asset-message items. Gets executed if refresh option has been set.
*
* @private
* @param {MetadataTypeMap} metadata metadata mapped by their keyField
* @returns {Promise.<void>} -
* @returns {Promise.<string[]>} Returns list of keys that were refreshed
*/
static async _refreshTriggeredSend(metadata) {
// get legacyData.legacyId from assets to compare to TSD's metadata.Email.ID to
Expand Down Expand Up @@ -900,7 +982,7 @@ class Asset extends MetadataType {
// get keys of TSDs to refresh
const keyArr = await TriggeredSend.getKeysForValidTSDs(tsdObj);

await TriggeredSend.refresh(keyArr);
return await TriggeredSend.refresh(keyArr);
} catch {
Util.logger.warn('Failed to refresh triggeredSendDefinition');
}
Expand Down Expand Up @@ -2980,11 +3062,47 @@ class Asset extends MetadataType {
}
return cacheMatchedByName;
}
/**
* Finds emails in running journeys, filters out the ones that reference the block (if it's a block) and refreshes related TSDs
*
* @param {string[]} [keyArr] metadata keys
* @returns {Promise.<string[]>} Returns list of keys that were refreshed
*/
static async refresh(keyArr) {
const codeBlockKeys = [];
const toPublish = [];
Util.logger.info(' - Caching dependent Metadata: asset');
const assets = await this.retrieveForCache(undefined, null, undefined, false);
cache.mergeMetadata('asset', assets.metadata);
Util.logger.info(' - Caching dependent Metadata: shared-asset');
const sharedAssets = await this.retrieveForCache(undefined, null, undefined, true);
cache.mergeMetadata('asset', sharedAssets.metadata);
for (const key of keyArr) {
const item = cache.getByKey(this.definition.type, key);
if (!item) {
Comment on lines +3098 to +3112

Copilot AI Mar 24, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

refresh() declares keyArr as optional in the JSDoc/typings, but the implementation does for (const key of keyArr) which will throw if keyArr is undefined (e.g., if a caller invokes refresh without keys). Either make the parameter required everywhere (update docs/types) or add a default/guard (treat missing keys as an empty list or throw a clear error).

Copilot uses AI. Check for mistakes.
Util.logger.warn(` - Could not find asset in cache for key: ${key}, skipping`);
continue;
}
if (this.definition.extendedSubTypes.message.includes(item?.assetType?.name)) {
toPublish.push(item);
// refreshing code blocks referenced with ContentBlockBy
} else if (
this.definition.extendedSubTypes.block.includes(item?.assetType?.name) ||
this.definition.extendedSubTypes.other.includes(item?.assetType?.name)
) {
codeBlockKeys.push(key);
}
}
if (codeBlockKeys.length > 0) {
toPublish.push(...Object.values(await this._findEmailsUsingBlock(codeBlockKeys)));
}
Util.logger.info(` - Found ${toPublish.length} assets to publish`);
return toPublish.length ? await this._refreshTriggeredSend(toPublish) : [];

Copilot AI Mar 24, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

refresh() passes toPublish (an array) into _refreshTriggeredSend(), but _refreshTriggeredSend() expects a MetadataTypeMap keyed by customerKey and iterates Object.keys(metadata) to access items. With an array, keys become "0", "1", ... which is brittle and can break if callers ever pass a non-array iterable. Consider normalizing toPublish into an object keyed by customerKey (and ideally de-dupe) before calling _refreshTriggeredSend(), or update _refreshTriggeredSend() to explicitly accept an array and iterate items directly.

Suggested change
return toPublish.length ? await this._refreshTriggeredSend(toPublish) : [];
const toPublishMap =
toPublish.length > 0
? toPublish.reduce(
/**
* @param {MetadataTypeMap} acc
* @param {MetadataTypeItem} item
* @returns {MetadataTypeMap}
*/
(acc, item) => {
if (item && item.customerKey) {
acc[item.customerKey] = item;
}
return acc;
},
/** @type {MetadataTypeMap} */ ({})
)
: /** @type {MetadataTypeMap} */ ({});
return Object.keys(toPublishMap).length
? await this._refreshTriggeredSend(toPublishMap)
: [];

Copilot uses AI. Check for mistakes.
}
}

// Assign definition to static attributes
import MetadataTypeDefinitions from '../MetadataTypeDefinitions.js';

Asset.definition = MetadataTypeDefinitions.asset;

export default Asset;
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<table
cellpadding="0"
cellspacing="0"
width="100%"
role="presentation"
style="min-width: 100%; "
class="stylingblock-content-wrapper"
>
<tr>
<td class="stylingblock-content-wrapper camarker-inner">
<h1>Test Block</h1>
<div>This is a test block</div>
</td>
</tr>
</table>
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"id": 334322,
"customerKey": "testExisting_block_refresh",
"objectID": "baf8348d-61d5-451f-948a-28045e41e50c",
"assetType": {
"id": 220,
"name": "codesnippetblock",
"displayName": "Code Snippet Block"
},
"name": "testExisting_block_refresh",
"createdDate": "2025-10-24T12:19:21.2-06:00",
"createdBy": {
"id": 724496218,
"email": "test@test.com",
"name": "Yuliia Likhytska",
"userId": "724496218"
},
"modifiedDate": "2026-03-15T11:37:51.38-06:00",
"modifiedBy": {
"id": 724496218,
"email": "test@test.com",
"name": "Yuliia Likhytska",
"userId": "724496218"
},
"category": {
"id": 370568
},
"modelVersion": 2,
"r__folder_Path": "Content Builder"
}
2 changes: 1 addition & 1 deletion test/resourceFactory.js
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,7 @@ export const handleRESTRequest = async (config) => {
: null;

if (!testPathFilter && config.method === 'post' && config.data) {
const simpleOperators = { equal: '=', in: 'IN' };
const simpleOperators = { equal: '=', in: 'IN', mustContain: 'MUSTCONTAIN' };
const data = JSON.parse(config.data);
const myObj = data.query?.rightOperand || data.query;
if (myObj) {
Expand Down
Loading
Loading