From 3f76e5540df6f38a87991a777f19728f0215202e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Uro=C5=A1=20Marolt?= Date: Fri, 3 Oct 2025 08:03:25 +0200 Subject: [PATCH 01/26] fix: use platform instead of default for incorrectly formatted attributes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Uroš Marolt --- .../data_sink_worker/src/service/member.service.ts | 14 ++++++-------- .../src/service/memberAttribute.service.ts | 2 +- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/services/apps/data_sink_worker/src/service/member.service.ts b/services/apps/data_sink_worker/src/service/member.service.ts index 73970672f3..385674ab33 100644 --- a/services/apps/data_sink_worker/src/service/member.service.ts +++ b/services/apps/data_sink_worker/src/service/member.service.ts @@ -65,7 +65,7 @@ export default class MemberService extends LoggerBase { segmentIds: string[], integrationId: string, data: IMemberCreateData, - source: string, + platform: PlatformType, releaseMemberLock?: () => Promise, ): Promise { return logExecutionTimeV2( @@ -89,8 +89,7 @@ export default class MemberService extends LoggerBase { let attributes: Record = {} if (data.attributes) { attributes = await logExecutionTimeV2( - () => - memberAttributeService.validateAttributes(data.attributes, source as PlatformType), + () => memberAttributeService.validateAttributes(platform, data.attributes), this.log, 'memberService -> create -> validateAttributes', ) @@ -190,7 +189,7 @@ export default class MemberService extends LoggerBase { if (data.organizations) { for (const org of data.organizations) { const id = await logExecutionTimeV2( - () => orgService.findOrCreate(source, integrationId, org), + () => orgService.findOrCreate(platform, integrationId, org), this.log, 'memberService -> create -> findOrCreateOrg', ) @@ -265,7 +264,7 @@ export default class MemberService extends LoggerBase { data: IMemberUpdateData, original: IDbMember, originalIdentities: IMemberIdentity[], - source: string, + platform: PlatformType, releaseMemberLock?: () => Promise, ): Promise { await logExecutionTimeV2( @@ -282,8 +281,7 @@ export default class MemberService extends LoggerBase { if (data.attributes) { this.log.trace({ memberId: id }, 'Validating member attributes!') data.attributes = await logExecutionTimeV2( - () => - memberAttributeService.validateAttributes(data.attributes, source as PlatformType), + () => memberAttributeService.validateAttributes(platform, data.attributes), this.log, 'memberService -> update -> validateAttributes', ) @@ -403,7 +401,7 @@ export default class MemberService extends LoggerBase { this.log.trace({ memberId: id }, 'Finding or creating organization!') const orgId = await logExecutionTimeV2( - () => orgService.findOrCreate(source, integrationId, org), + () => orgService.findOrCreate(platform, integrationId, org), this.log, 'memberService -> update -> findOrCreateOrg', ) diff --git a/services/apps/data_sink_worker/src/service/memberAttribute.service.ts b/services/apps/data_sink_worker/src/service/memberAttribute.service.ts index c1ac384d0e..c6e9168978 100644 --- a/services/apps/data_sink_worker/src/service/memberAttribute.service.ts +++ b/services/apps/data_sink_worker/src/service/memberAttribute.service.ts @@ -26,8 +26,8 @@ export default class MemberAttributeService extends LoggerBase { } public async validateAttributes( - attributes: Record, platform: PlatformType, + attributes: Record, ): Promise> { const settings = await getMemberAttributeSettings(dbStoreQx(this.store), this.redis) const memberAttributeSettings = settings.reduce((acc, attribute) => { From df455de545878b52c676d6d7530fb6ee70ed02ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Uro=C5=A1=20Marolt?= Date: Fri, 3 Oct 2025 08:59:05 +0200 Subject: [PATCH 02/26] chore: script to fix the corrupted attributes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Uroš Marolt --- .../src/bin/fix-member-attributes.ts | 225 ++++++++++++++++++ 1 file changed, 225 insertions(+) create mode 100644 services/apps/data_sink_worker/src/bin/fix-member-attributes.ts diff --git a/services/apps/data_sink_worker/src/bin/fix-member-attributes.ts b/services/apps/data_sink_worker/src/bin/fix-member-attributes.ts new file mode 100644 index 0000000000..6fb5da0a79 --- /dev/null +++ b/services/apps/data_sink_worker/src/bin/fix-member-attributes.ts @@ -0,0 +1,225 @@ +import isEqual from 'lodash.isequal' + +import { connQx, updateMember } from '@crowd/data-access-layer' +import { + DbConnOrTx, + DbStore, + WRITE_DB_CONFIG, + getDbConnection, +} from '@crowd/data-access-layer/src/database' +import { getServiceLogger } from '@crowd/logging' +import { REDIS_CONFIG, getRedisClient } from '@crowd/redis' + +import MemberAttributeService from '../service/memberAttribute.service' + +/* eslint-disable @typescript-eslint/no-explicit-any */ + +const log = getServiceLogger() + +async function getMemberIds( + db: DbConnOrTx, + lastId?: string, +): Promise<{ id: string; attributes: any; manuallyChangedFields: any }[]> { + try { + log.debug({ lastId }, 'Querying for members with attribute issues') + + const results = await db.any( + `with relevant_members as (with member_with_attributes as (select id, + "createdAt", + jsonb_object_keys(attributes) as attr_key, + attributes -> jsonb_object_keys(attributes) as attr_value + from members + where "deletedAt" is null + and attributes is not null + and attributes != 'null'::jsonb + and attributes != '{}'::jsonb) + select distinct id + from member_with_attributes + where jsonb_typeof(attr_value) = 'object' + and coalesce(attr_value ->> 'default', '') = '' + ${lastId ? `and id < '${lastId}'` : ''} + order by id desc + limit 5000) + select m.id, m.attributes, m."manuallyChangedFields" + from members m + inner join + relevant_members rm on rm.id = m.id;`, + { lastId }, + ) + + log.debug( + { + resultCount: results.length, + lastId, + firstId: results.length > 0 ? results[0].id : null, + lastResultId: results.length > 0 ? results[results.length - 1].id : null, + }, + 'Query completed', + ) + + return results + } catch (error) { + log.error( + { + error: error.message, + lastId, + stack: error.stack, + }, + 'Failed to query member IDs', + ) + throw error + } +} + +setImmediate(async () => { + let dbClient: DbConnOrTx | undefined + let redisClient: any | undefined + + try { + log.info('Starting member attributes fix script') + + // Initialize connections + log.info('Connecting to database...') + dbClient = await getDbConnection(WRITE_DB_CONFIG()) + log.info('Database connection established') + + log.info('Connecting to Redis...') + redisClient = await getRedisClient(REDIS_CONFIG()) + log.info('Redis connection established') + + const pgQx = connQx(dbClient) + const mas = new MemberAttributeService(redisClient, new DbStore(log, dbClient), log) + + let totalProcessed = 0 + let totalUpdated = 0 + let batchNumber = 1 + + log.info('Starting to process members with attribute issues') + let membersToFix = await getMemberIds(dbClient) + log.info({ count: membersToFix.length }, 'Found members to process in first batch') + + while (membersToFix.length > 0) { + log.info({ batchNumber, batchSize: membersToFix.length }, 'Processing batch') + let batchUpdated = 0 + + for (const data of membersToFix) { + try { + if (data.attributes) { + log.debug({ memberId: data.id }, 'Processing member attributes') + + const oldAttributes = data.attributes + data.attributes = await mas.setAttributesDefaultValues(data.attributes) + + let attributes: Record | undefined + const temp = { ...data.attributes } + const manuallyChangedFields: string[] = data.manuallyChangedFields || [] + + if (manuallyChangedFields.length > 0) { + log.debug( + { + memberId: data.id, + manuallyChangedFieldsCount: manuallyChangedFields.length, + }, + 'Member has manually changed fields', + ) + + const prefix = 'attributes.' + const manuallyChangedAttributes = [ + ...new Set( + manuallyChangedFields + .filter((f) => f.startsWith(prefix)) + .map((f) => f.slice(prefix.length)), + ), + ] + + log.debug( + { + memberId: data.id, + manuallyChangedAttributes, + }, + 'Preserving manually changed attributes', + ) + + // Preserve manually changed attributes + for (const key of manuallyChangedAttributes) { + if (oldAttributes?.[key] !== undefined) { + temp[key] = oldAttributes[key] // Fixed: removed .attributes + } + } + } + + if (!isEqual(temp, oldAttributes)) { + attributes = temp + log.debug({ memberId: data.id }, 'Attributes changed, will update') + } else { + log.debug({ memberId: data.id }, 'No changes needed for attributes') + } + + if (attributes) { + log.debug({ memberId: data.id }, 'Updating member attributes') + await updateMember(pgQx, data.id, { attributes } as any) + batchUpdated++ + totalUpdated++ + log.debug({ memberId: data.id }, 'Member attributes updated successfully') + } + } else { + log.debug({ memberId: data.id }, 'Member has no attributes to process') + } + + totalProcessed++ + } catch (error) { + log.error( + { + error: error.message, + memberId: data.id, + stack: error.stack, + }, + 'Failed to process member', + ) + // Continue processing other members + } + } + + log.info( + { + batchNumber, + batchProcessed: membersToFix.length, + batchUpdated, + totalProcessed, + totalUpdated, + }, + 'Completed batch processing', + ) + + // Get next batch + const lastId = membersToFix[membersToFix.length - 1].id + log.debug({ lastId }, 'Fetching next batch starting from last ID') + + membersToFix = await getMemberIds(dbClient, lastId) + log.info({ count: membersToFix.length }, 'Found members for next batch') + + batchNumber++ + } + + log.info( + { + totalProcessed, + totalUpdated, + totalBatches: batchNumber - 1, + }, + 'Member attributes fix completed successfully', + ) + } catch (error) { + log.error( + { + error: error.message, + stack: error.stack, + }, + 'Fatal error in member attributes fix script', + ) + process.exit(1) + } finally { + log.info('Script execution completed') + process.exit(0) + } +}) From 62eb163b47d8f75bed46451d6488f6b2e320a7da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Uro=C5=A1=20Marolt?= Date: Fri, 3 Oct 2025 20:34:36 +0200 Subject: [PATCH 03/26] chore: fixed small issues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Uroš Marolt --- .../src/bin/fix-member-attributes.ts | 127 +++++++++++------- 1 file changed, 78 insertions(+), 49 deletions(-) diff --git a/services/apps/data_sink_worker/src/bin/fix-member-attributes.ts b/services/apps/data_sink_worker/src/bin/fix-member-attributes.ts index 6fb5da0a79..4359245983 100644 --- a/services/apps/data_sink_worker/src/bin/fix-member-attributes.ts +++ b/services/apps/data_sink_worker/src/bin/fix-member-attributes.ts @@ -107,60 +107,89 @@ setImmediate(async () => { if (data.attributes) { log.debug({ memberId: data.id }, 'Processing member attributes') - const oldAttributes = data.attributes - data.attributes = await mas.setAttributesDefaultValues(data.attributes) - - let attributes: Record | undefined - const temp = { ...data.attributes } - const manuallyChangedFields: string[] = data.manuallyChangedFields || [] - - if (manuallyChangedFields.length > 0) { - log.debug( - { - memberId: data.id, - manuallyChangedFieldsCount: manuallyChangedFields.length, - }, - 'Member has manually changed fields', - ) - - const prefix = 'attributes.' - const manuallyChangedAttributes = [ - ...new Set( - manuallyChangedFields - .filter((f) => f.startsWith(prefix)) - .map((f) => f.slice(prefix.length)), - ), - ] - - log.debug( - { - memberId: data.id, - manuallyChangedAttributes, - }, - 'Preserving manually changed attributes', - ) - - // Preserve manually changed attributes - for (const key of manuallyChangedAttributes) { - if (oldAttributes?.[key] !== undefined) { - temp[key] = oldAttributes[key] // Fixed: removed .attributes + // check if any has default empty but other are full + let process = false + for (const attName of Object.keys(data.attributes)) { + const defValue = data.attributes[attName].default + + if (defValue === undefined || defValue === null || defValue === '') { + for (const platform of Object.keys(data.attributes[attName]).filter( + (p) => p !== 'default', + )) { + const value = data.attributes[attName][platform] + + if (value !== undefined && value !== null && value !== '') { + log.debug( + { memberId: data.id, attName, platform, value }, + 'Found value for attribute', + ) + process = true + break + } + } + + if (process) { + break } } } - if (!isEqual(temp, oldAttributes)) { - attributes = temp - log.debug({ memberId: data.id }, 'Attributes changed, will update') - } else { - log.debug({ memberId: data.id }, 'No changes needed for attributes') - } + if (process) { + const oldAttributes = data.attributes + data.attributes = await mas.setAttributesDefaultValues(data.attributes) + + let attributes: Record | undefined + const temp = { ...data.attributes } + const manuallyChangedFields: string[] = data.manuallyChangedFields || [] + + if (manuallyChangedFields.length > 0) { + log.debug( + { + memberId: data.id, + manuallyChangedFieldsCount: manuallyChangedFields.length, + }, + 'Member has manually changed fields', + ) + + const prefix = 'attributes.' + const manuallyChangedAttributes = [ + ...new Set( + manuallyChangedFields + .filter((f) => f.startsWith(prefix)) + .map((f) => f.slice(prefix.length)), + ), + ] + + log.debug( + { + memberId: data.id, + manuallyChangedAttributes, + }, + 'Preserving manually changed attributes', + ) + + // Preserve manually changed attributes + for (const key of manuallyChangedAttributes) { + if (oldAttributes?.[key] !== undefined) { + temp[key] = oldAttributes[key] // Fixed: removed .attributes + } + } + } + + if (!isEqual(temp, oldAttributes)) { + attributes = temp + log.debug({ memberId: data.id }, 'Attributes changed, will update') + } else { + log.debug({ memberId: data.id }, 'No changes needed for attributes') + } - if (attributes) { - log.debug({ memberId: data.id }, 'Updating member attributes') - await updateMember(pgQx, data.id, { attributes } as any) - batchUpdated++ - totalUpdated++ - log.debug({ memberId: data.id }, 'Member attributes updated successfully') + if (attributes) { + log.debug({ memberId: data.id }, 'Updating member attributes') + await updateMember(pgQx, data.id, { attributes } as any) + batchUpdated++ + totalUpdated++ + log.debug({ memberId: data.id }, 'Member attributes updated successfully') + } } } else { log.debug({ memberId: data.id }, 'Member has no attributes to process') From d343400b4e4ecef6e32917ccceb0dd36117dc93d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Uro=C5=A1=20Marolt?= Date: Fri, 3 Oct 2025 20:38:06 +0200 Subject: [PATCH 04/26] chore: fixed small issues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Uroš Marolt --- .../data_sink_worker/src/bin/fix-member-attributes.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/services/apps/data_sink_worker/src/bin/fix-member-attributes.ts b/services/apps/data_sink_worker/src/bin/fix-member-attributes.ts index 4359245983..5471530ba3 100644 --- a/services/apps/data_sink_worker/src/bin/fix-member-attributes.ts +++ b/services/apps/data_sink_worker/src/bin/fix-member-attributes.ts @@ -184,8 +184,12 @@ setImmediate(async () => { } if (attributes) { - log.debug({ memberId: data.id }, 'Updating member attributes') - await updateMember(pgQx, data.id, { attributes } as any) + log.debug( + { memberId: data.id, oldAttributes, attributes }, + 'Updating member attributes', + ) + + // await updateMember(pgQx, data.id, { attributes } as any) batchUpdated++ totalUpdated++ log.debug({ memberId: data.id }, 'Member attributes updated successfully') From 174826b585bf4470c8903e14d3d67e84362823db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Uro=C5=A1=20Marolt?= Date: Fri, 3 Oct 2025 20:40:25 +0200 Subject: [PATCH 05/26] chore: fixed small issues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Uroš Marolt --- .../apps/data_sink_worker/src/bin/fix-member-attributes.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/services/apps/data_sink_worker/src/bin/fix-member-attributes.ts b/services/apps/data_sink_worker/src/bin/fix-member-attributes.ts index 5471530ba3..91f292e8ef 100644 --- a/services/apps/data_sink_worker/src/bin/fix-member-attributes.ts +++ b/services/apps/data_sink_worker/src/bin/fix-member-attributes.ts @@ -194,6 +194,11 @@ setImmediate(async () => { totalUpdated++ log.debug({ memberId: data.id }, 'Member attributes updated successfully') } + } else { + log.debug( + { memberId: data.id, attributes: data.attributes }, + 'No changes needed for attributes', + ) } } else { log.debug({ memberId: data.id }, 'Member has no attributes to process') From 10cd7f2fe753bbfe286a60a657f9544b20d8af81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Uro=C5=A1=20Marolt?= Date: Fri, 3 Oct 2025 20:45:10 +0200 Subject: [PATCH 06/26] chore: fixed small issues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Uroš Marolt --- .../apps/data_sink_worker/src/bin/fix-member-attributes.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/apps/data_sink_worker/src/bin/fix-member-attributes.ts b/services/apps/data_sink_worker/src/bin/fix-member-attributes.ts index 91f292e8ef..29a1ad1c21 100644 --- a/services/apps/data_sink_worker/src/bin/fix-member-attributes.ts +++ b/services/apps/data_sink_worker/src/bin/fix-member-attributes.ts @@ -112,13 +112,13 @@ setImmediate(async () => { for (const attName of Object.keys(data.attributes)) { const defValue = data.attributes[attName].default - if (defValue === undefined || defValue === null || defValue === '') { + if (defValue === undefined || defValue === null || String(defValue) === '') { for (const platform of Object.keys(data.attributes[attName]).filter( (p) => p !== 'default', )) { const value = data.attributes[attName][platform] - if (value !== undefined && value !== null && value !== '') { + if (value !== undefined && value !== null && String(value) !== '') { log.debug( { memberId: data.id, attName, platform, value }, 'Found value for attribute', From 01b07a5230e373847c14531645d7048d88313fa3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Uro=C5=A1=20Marolt?= Date: Fri, 3 Oct 2025 20:51:27 +0200 Subject: [PATCH 07/26] chore: fixed small issues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Uroš Marolt --- .../src/bin/fix-member-attributes.ts | 46 +++++++++++-------- 1 file changed, 26 insertions(+), 20 deletions(-) diff --git a/services/apps/data_sink_worker/src/bin/fix-member-attributes.ts b/services/apps/data_sink_worker/src/bin/fix-member-attributes.ts index 29a1ad1c21..cda43c4fb4 100644 --- a/services/apps/data_sink_worker/src/bin/fix-member-attributes.ts +++ b/services/apps/data_sink_worker/src/bin/fix-member-attributes.ts @@ -24,26 +24,32 @@ async function getMemberIds( log.debug({ lastId }, 'Querying for members with attribute issues') const results = await db.any( - `with relevant_members as (with member_with_attributes as (select id, - "createdAt", - jsonb_object_keys(attributes) as attr_key, - attributes -> jsonb_object_keys(attributes) as attr_value - from members - where "deletedAt" is null - and attributes is not null - and attributes != 'null'::jsonb - and attributes != '{}'::jsonb) - select distinct id - from member_with_attributes - where jsonb_typeof(attr_value) = 'object' - and coalesce(attr_value ->> 'default', '') = '' - ${lastId ? `and id < '${lastId}'` : ''} - order by id desc - limit 5000) - select m.id, m.attributes, m."manuallyChangedFields" - from members m - inner join - relevant_members rm on rm.id = m.id;`, + ` + with relevant_members as (with member_with_attributes as (select id, + "createdAt", + jsonb_object_keys(attributes) as attr_key, + attributes -> jsonb_object_keys(attributes) as attr_value + from members + where "deletedAt" is null + and attributes is not null + and attributes != 'null'::jsonb + and attributes != '{}'::jsonb) + select distinct id + from member_with_attributes + where jsonb_typeof(attr_value) = 'object' + and coalesce(attr_value ->> 'default', '') = '' + and exists (select 1 + from jsonb_each_text(attr_value) as kv + where kv.key != 'default' + and coalesce(kv.value, '') != '') + ${lastId ? `and id < '${lastId}'` : ''} + order by id desc + limit 5000) +select m.id, m.attributes +from members m + inner join + relevant_members rm on rm.id = m.id; + `, { lastId }, ) From cd2e52746784e6d95e3368411426f8bae668ab5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Uro=C5=A1=20Marolt?= Date: Fri, 3 Oct 2025 20:59:02 +0200 Subject: [PATCH 08/26] chore: fixed small issues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Uroš Marolt --- .../data_sink_worker/src/bin/fix-member-attributes.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/services/apps/data_sink_worker/src/bin/fix-member-attributes.ts b/services/apps/data_sink_worker/src/bin/fix-member-attributes.ts index cda43c4fb4..a21fa1d692 100644 --- a/services/apps/data_sink_worker/src/bin/fix-member-attributes.ts +++ b/services/apps/data_sink_worker/src/bin/fix-member-attributes.ts @@ -119,6 +119,14 @@ setImmediate(async () => { const defValue = data.attributes[attName].default if (defValue === undefined || defValue === null || String(defValue) === '') { + log.debug( + { + memberId: data.id, + attName, + defValue: defValue ? String(defValue) : 'undefined', + }, + 'Attribute has default empty', + ) for (const platform of Object.keys(data.attributes[attName]).filter( (p) => p !== 'default', )) { From 31bfc8a33063471b4fa11f74b3dfaa75d054b103 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Uro=C5=A1=20Marolt?= Date: Fri, 3 Oct 2025 21:00:35 +0200 Subject: [PATCH 09/26] chore: fixed small issues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Uroš Marolt --- services/apps/data_sink_worker/src/bin/fix-member-attributes.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/services/apps/data_sink_worker/src/bin/fix-member-attributes.ts b/services/apps/data_sink_worker/src/bin/fix-member-attributes.ts index a21fa1d692..ed41bd39ad 100644 --- a/services/apps/data_sink_worker/src/bin/fix-member-attributes.ts +++ b/services/apps/data_sink_worker/src/bin/fix-member-attributes.ts @@ -122,6 +122,7 @@ setImmediate(async () => { log.debug( { memberId: data.id, + attributes: data.attributes, attName, defValue: defValue ? String(defValue) : 'undefined', }, From 2ad563f1cfe67fcca5c00ddcb4682df379989ec5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Uro=C5=A1=20Marolt?= Date: Fri, 3 Oct 2025 21:02:12 +0200 Subject: [PATCH 10/26] chore: fixed small issues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Uroš Marolt --- .../apps/data_sink_worker/src/bin/fix-member-attributes.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/services/apps/data_sink_worker/src/bin/fix-member-attributes.ts b/services/apps/data_sink_worker/src/bin/fix-member-attributes.ts index ed41bd39ad..60140e733e 100644 --- a/services/apps/data_sink_worker/src/bin/fix-member-attributes.ts +++ b/services/apps/data_sink_worker/src/bin/fix-member-attributes.ts @@ -195,7 +195,10 @@ setImmediate(async () => { attributes = temp log.debug({ memberId: data.id }, 'Attributes changed, will update') } else { - log.debug({ memberId: data.id }, 'No changes needed for attributes') + log.debug( + { memberId: data.id, newAttributes: temp }, + 'No changes needed for attributes', + ) } if (attributes) { From fd4f319bfcedb5d424b1df845a5a2591f60f230f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Uro=C5=A1=20Marolt?= Date: Fri, 3 Oct 2025 21:09:15 +0200 Subject: [PATCH 11/26] chore: fixed small issues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Uroš Marolt --- services/libs/common/src/member.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/services/libs/common/src/member.ts b/services/libs/common/src/member.ts index e9253847a4..f0dc1a14bb 100644 --- a/services/libs/common/src/member.ts +++ b/services/libs/common/src/member.ts @@ -21,8 +21,14 @@ export async function setAttributesDefaultValues( throw err } } + + const nonEmptyPlatform = Object.keys(attributes[attributeName]).filter((p) => { + const value = attributes[attributeName][p] + return value !== undefined && value !== null && String(value).trim().length > 0 + }) + const highestPriorityPlatform = getHighestPriorityPlatformForAttributes( - Object.keys(attributes[attributeName]), + nonEmptyPlatform, priorities, ) From b7d09d8520e181a17820c394109156da81076b91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Uro=C5=A1=20Marolt?= Date: Fri, 3 Oct 2025 21:14:05 +0200 Subject: [PATCH 12/26] chore: fixed small issues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Uroš Marolt --- services/apps/data_sink_worker/src/bin/fix-member-attributes.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/apps/data_sink_worker/src/bin/fix-member-attributes.ts b/services/apps/data_sink_worker/src/bin/fix-member-attributes.ts index 60140e733e..23247e4237 100644 --- a/services/apps/data_sink_worker/src/bin/fix-member-attributes.ts +++ b/services/apps/data_sink_worker/src/bin/fix-member-attributes.ts @@ -122,7 +122,7 @@ setImmediate(async () => { log.debug( { memberId: data.id, - attributes: data.attributes, + attribute: data.attributes[attName], attName, defValue: defValue ? String(defValue) : 'undefined', }, From eb16c58530c4322d05ab9779d9acfc9c8abcf946 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Uro=C5=A1=20Marolt?= Date: Fri, 3 Oct 2025 21:15:50 +0200 Subject: [PATCH 13/26] chore: fixed small issues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Uroš Marolt --- services/apps/data_sink_worker/src/bin/fix-member-attributes.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/apps/data_sink_worker/src/bin/fix-member-attributes.ts b/services/apps/data_sink_worker/src/bin/fix-member-attributes.ts index 23247e4237..ec108e12cc 100644 --- a/services/apps/data_sink_worker/src/bin/fix-member-attributes.ts +++ b/services/apps/data_sink_worker/src/bin/fix-member-attributes.ts @@ -196,7 +196,7 @@ setImmediate(async () => { log.debug({ memberId: data.id }, 'Attributes changed, will update') } else { log.debug( - { memberId: data.id, newAttributes: temp }, + { memberId: data.id, newAttributes: temp, oldAttributes }, 'No changes needed for attributes', ) } From c080a4d9c1ce0e3eaadd4eec7d9e5be0009c090c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Uro=C5=A1=20Marolt?= Date: Fri, 3 Oct 2025 21:27:39 +0200 Subject: [PATCH 14/26] chore: fixed small issues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Uroš Marolt --- .../apps/data_sink_worker/src/bin/fix-member-attributes.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/services/apps/data_sink_worker/src/bin/fix-member-attributes.ts b/services/apps/data_sink_worker/src/bin/fix-member-attributes.ts index ec108e12cc..7b3b2b8188 100644 --- a/services/apps/data_sink_worker/src/bin/fix-member-attributes.ts +++ b/services/apps/data_sink_worker/src/bin/fix-member-attributes.ts @@ -111,7 +111,10 @@ setImmediate(async () => { for (const data of membersToFix) { try { if (data.attributes) { - log.debug({ memberId: data.id }, 'Processing member attributes') + log.debug( + { memberId: data.id, oldAttributes: data.attributes }, + 'Processing member attributes', + ) // check if any has default empty but other are full let process = false From 6033af508c8e470f28e16ae1c988f1e4941bd837 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Uro=C5=A1=20Marolt?= Date: Fri, 3 Oct 2025 21:28:51 +0200 Subject: [PATCH 15/26] chore: fixed small issues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Uroš Marolt --- .../data_sink_worker/src/bin/fix-member-attributes.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/services/apps/data_sink_worker/src/bin/fix-member-attributes.ts b/services/apps/data_sink_worker/src/bin/fix-member-attributes.ts index 7b3b2b8188..3dceb73571 100644 --- a/services/apps/data_sink_worker/src/bin/fix-member-attributes.ts +++ b/services/apps/data_sink_worker/src/bin/fix-member-attributes.ts @@ -14,6 +14,8 @@ import MemberAttributeService from '../service/memberAttribute.service' /* eslint-disable @typescript-eslint/no-explicit-any */ +const BATCH_SIZE = process.env.TEST_RUN ? 1 : 5000 + const log = getServiceLogger() async function getMemberIds( @@ -44,7 +46,7 @@ async function getMemberIds( and coalesce(kv.value, '') != '') ${lastId ? `and id < '${lastId}'` : ''} order by id desc - limit 5000) + limit ${BATCH_SIZE}) select m.id, m.attributes from members m inner join @@ -254,6 +256,10 @@ setImmediate(async () => { const lastId = membersToFix[membersToFix.length - 1].id log.debug({ lastId }, 'Fetching next batch starting from last ID') + if (process.env.TEST_RUN) { + break + } + membersToFix = await getMemberIds(dbClient, lastId) log.info({ count: membersToFix.length }, 'Found members for next batch') From d99c73006b276e4d1f59369e059dd8c2a2d97be8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Uro=C5=A1=20Marolt?= Date: Fri, 3 Oct 2025 21:33:01 +0200 Subject: [PATCH 16/26] chore: fixed small issues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Uroš Marolt --- services/apps/data_sink_worker/src/bin/fix-member-attributes.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/apps/data_sink_worker/src/bin/fix-member-attributes.ts b/services/apps/data_sink_worker/src/bin/fix-member-attributes.ts index 3dceb73571..06e8c72fa2 100644 --- a/services/apps/data_sink_worker/src/bin/fix-member-attributes.ts +++ b/services/apps/data_sink_worker/src/bin/fix-member-attributes.ts @@ -155,7 +155,7 @@ setImmediate(async () => { } if (process) { - const oldAttributes = data.attributes + const oldAttributes = JSON.parse(JSON.stringify(data.attributes)) // Deep copy data.attributes = await mas.setAttributesDefaultValues(data.attributes) let attributes: Record | undefined From 6e6f8c7406b6ddd0ba9a83ddda36d8e81b75ac28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Uro=C5=A1=20Marolt?= Date: Fri, 3 Oct 2025 21:38:41 +0200 Subject: [PATCH 17/26] chore: fixed small issues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Uroš Marolt --- .../apps/data_sink_worker/src/bin/fix-member-attributes.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/services/apps/data_sink_worker/src/bin/fix-member-attributes.ts b/services/apps/data_sink_worker/src/bin/fix-member-attributes.ts index 06e8c72fa2..78335f3856 100644 --- a/services/apps/data_sink_worker/src/bin/fix-member-attributes.ts +++ b/services/apps/data_sink_worker/src/bin/fix-member-attributes.ts @@ -1,4 +1,5 @@ import isEqual from 'lodash.isequal' +import mergeWith from 'lodash.mergewith' import { connQx, updateMember } from '@crowd/data-access-layer' import { @@ -159,7 +160,7 @@ setImmediate(async () => { data.attributes = await mas.setAttributesDefaultValues(data.attributes) let attributes: Record | undefined - const temp = { ...data.attributes } + const temp = mergeWith({}, oldAttributes, data.attributes) const manuallyChangedFields: string[] = data.manuallyChangedFields || [] if (manuallyChangedFields.length > 0) { @@ -212,7 +213,7 @@ setImmediate(async () => { 'Updating member attributes', ) - // await updateMember(pgQx, data.id, { attributes } as any) + await updateMember(pgQx, data.id, { attributes } as any) batchUpdated++ totalUpdated++ log.debug({ memberId: data.id }, 'Member attributes updated successfully') From 8068e9b623ab997b4325dee89c8e068addac540a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Uro=C5=A1=20Marolt?= Date: Fri, 3 Oct 2025 21:40:17 +0200 Subject: [PATCH 18/26] chore: fixed small issues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Uroš Marolt --- .../src/bin/fix-member-attributes.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/services/apps/data_sink_worker/src/bin/fix-member-attributes.ts b/services/apps/data_sink_worker/src/bin/fix-member-attributes.ts index 78335f3856..a6f971779e 100644 --- a/services/apps/data_sink_worker/src/bin/fix-member-attributes.ts +++ b/services/apps/data_sink_worker/src/bin/fix-member-attributes.ts @@ -120,7 +120,7 @@ setImmediate(async () => { ) // check if any has default empty but other are full - let process = false + let toProcess = false for (const attName of Object.keys(data.attributes)) { const defValue = data.attributes[attName].default @@ -144,18 +144,18 @@ setImmediate(async () => { { memberId: data.id, attName, platform, value }, 'Found value for attribute', ) - process = true + toProcess = true break } } - if (process) { + if (toProcess) { break } } } - if (process) { + if (toProcess) { const oldAttributes = JSON.parse(JSON.stringify(data.attributes)) // Deep copy data.attributes = await mas.setAttributesDefaultValues(data.attributes) @@ -213,7 +213,10 @@ setImmediate(async () => { 'Updating member attributes', ) - await updateMember(pgQx, data.id, { attributes } as any) + if (!process.env.TEST_RUN) { + await updateMember(pgQx, data.id, { attributes } as any) + } + batchUpdated++ totalUpdated++ log.debug({ memberId: data.id }, 'Member attributes updated successfully') From 7989f151bd8a4f02e09d17c77bd39abc39fed01b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Uro=C5=A1=20Marolt?= Date: Sat, 4 Oct 2025 15:24:32 +0200 Subject: [PATCH 19/26] chore: fixed small issues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Uroš Marolt --- .../data_sink_worker/src/bin/fix-member-attributes.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/services/apps/data_sink_worker/src/bin/fix-member-attributes.ts b/services/apps/data_sink_worker/src/bin/fix-member-attributes.ts index a6f971779e..eeb697fc35 100644 --- a/services/apps/data_sink_worker/src/bin/fix-member-attributes.ts +++ b/services/apps/data_sink_worker/src/bin/fix-member-attributes.ts @@ -164,7 +164,7 @@ setImmediate(async () => { const manuallyChangedFields: string[] = data.manuallyChangedFields || [] if (manuallyChangedFields.length > 0) { - log.debug( + log.warn( { memberId: data.id, manuallyChangedFieldsCount: manuallyChangedFields.length, @@ -181,7 +181,7 @@ setImmediate(async () => { ), ] - log.debug( + log.warn( { memberId: data.id, manuallyChangedAttributes, @@ -199,7 +199,7 @@ setImmediate(async () => { if (!isEqual(temp, oldAttributes)) { attributes = temp - log.debug({ memberId: data.id }, 'Attributes changed, will update') + log.info({ memberId: data.id }, 'Attributes changed, will update') } else { log.debug( { memberId: data.id, newAttributes: temp, oldAttributes }, @@ -208,7 +208,7 @@ setImmediate(async () => { } if (attributes) { - log.debug( + log.info( { memberId: data.id, oldAttributes, attributes }, 'Updating member attributes', ) From 8c802ba11751b9c52a8bba67a1ee5b89685e89f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Uro=C5=A1=20Marolt?= Date: Sat, 4 Oct 2025 15:29:56 +0200 Subject: [PATCH 20/26] chore: fixed small issues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Uroš Marolt --- .../apps/data_sink_worker/src/bin/fix-member-attributes.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/services/apps/data_sink_worker/src/bin/fix-member-attributes.ts b/services/apps/data_sink_worker/src/bin/fix-member-attributes.ts index eeb697fc35..c253c640c0 100644 --- a/services/apps/data_sink_worker/src/bin/fix-member-attributes.ts +++ b/services/apps/data_sink_worker/src/bin/fix-member-attributes.ts @@ -208,10 +208,7 @@ setImmediate(async () => { } if (attributes) { - log.info( - { memberId: data.id, oldAttributes, attributes }, - 'Updating member attributes', - ) + log.info({ memberId: data.id }, 'Updating member attributes') if (!process.env.TEST_RUN) { await updateMember(pgQx, data.id, { attributes } as any) From f1b502c3546e768259f90e6c087f88f630c11c7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Uro=C5=A1=20Marolt?= Date: Sat, 4 Oct 2025 19:26:19 +0200 Subject: [PATCH 21/26] fix: lint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Uroš Marolt --- services/apps/data_sink_worker/src/service/activity.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/apps/data_sink_worker/src/service/activity.service.ts b/services/apps/data_sink_worker/src/service/activity.service.ts index ed36ec614f..f1d34f4ab1 100644 --- a/services/apps/data_sink_worker/src/service/activity.service.ts +++ b/services/apps/data_sink_worker/src/service/activity.service.ts @@ -1144,7 +1144,7 @@ export default class ActivityService extends LoggerBase { organizations: value.member.organizations, reach: value.member.reach, }, - value.platform, + value.platform as PlatformType, ) .then((memberId) => { // map ids for members From bd5d1cabbe5d3f6955724858778163da951998e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Uro=C5=A1=20Marolt?= Date: Mon, 23 Mar 2026 22:02:16 +0100 Subject: [PATCH 22/26] fix: include manuallyChangedFields MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Uroš Marolt --- services/apps/data_sink_worker/src/bin/fix-member-attributes.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/apps/data_sink_worker/src/bin/fix-member-attributes.ts b/services/apps/data_sink_worker/src/bin/fix-member-attributes.ts index c253c640c0..ad847e8b88 100644 --- a/services/apps/data_sink_worker/src/bin/fix-member-attributes.ts +++ b/services/apps/data_sink_worker/src/bin/fix-member-attributes.ts @@ -48,7 +48,7 @@ async function getMemberIds( ${lastId ? `and id < '${lastId}'` : ''} order by id desc limit ${BATCH_SIZE}) -select m.id, m.attributes +select m.id, m.attributes, m."manuallyChangedFields" from members m inner join relevant_members rm on rm.id = m.id; From b444f81c295bfee606eb049de6ec0502b1ef58e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Uro=C5=A1=20Marolt?= Date: Mon, 23 Mar 2026 22:05:04 +0100 Subject: [PATCH 23/26] fix: comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Uroš Marolt --- .../apps/data_sink_worker/src/service/activity.service.ts | 4 ++-- services/libs/common/src/member.ts | 7 +------ 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/services/apps/data_sink_worker/src/service/activity.service.ts b/services/apps/data_sink_worker/src/service/activity.service.ts index f1d34f4ab1..6dbd8ebde7 100644 --- a/services/apps/data_sink_worker/src/service/activity.service.ts +++ b/services/apps/data_sink_worker/src/service/activity.service.ts @@ -1068,7 +1068,7 @@ export default class ActivityService extends LoggerBase { segmentIds: Set resultIds: Set integrationId: string - platform: string + platform: PlatformType username: string timestamp: string } @@ -1144,7 +1144,7 @@ export default class ActivityService extends LoggerBase { organizations: value.member.organizations, reach: value.member.reach, }, - value.platform as PlatformType, + value.platform, ) .then((memberId) => { // map ids for members diff --git a/services/libs/common/src/member.ts b/services/libs/common/src/member.ts index f0dc1a14bb..718edf7178 100644 --- a/services/libs/common/src/member.ts +++ b/services/libs/common/src/member.ts @@ -14,12 +14,7 @@ export async function setAttributesDefaultValues( for (const attributeName of Object.keys(attributes)) { if (typeof attributes[attributeName] === 'string') { // we try to fix it - try { - attributes[attributeName] = JSON.parse(attributes[attributeName] as string) - } catch (err) { - this.log.error(err, { attributeName }, 'Could not parse a string attribute value!') - throw err - } + attributes[attributeName] = JSON.parse(attributes[attributeName] as string) } const nonEmptyPlatform = Object.keys(attributes[attributeName]).filter((p) => { From 6a1d5eca1e1c02c5e47ac2b4b34490e6fbd3a44e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Uro=C5=A1=20Marolt?= Date: Tue, 24 Mar 2026 14:53:08 +0100 Subject: [PATCH 24/26] fix: ignore default platform MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Uroš Marolt --- services/libs/common/src/member.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/services/libs/common/src/member.ts b/services/libs/common/src/member.ts index 718edf7178..322140839d 100644 --- a/services/libs/common/src/member.ts +++ b/services/libs/common/src/member.ts @@ -18,6 +18,7 @@ export async function setAttributesDefaultValues( } const nonEmptyPlatform = Object.keys(attributes[attributeName]).filter((p) => { + if (p === 'default') return false const value = attributes[attributeName][p] return value !== undefined && value !== null && String(value).trim().length > 0 }) From faeeceb27cb05921f6c692902a6b4a933288b6c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Uro=C5=A1=20Marolt?= Date: Tue, 24 Mar 2026 15:08:32 +0100 Subject: [PATCH 25/26] fix: bugfix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Uroš Marolt --- .../data_sink_worker/src/bin/fix-member-attributes.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/services/apps/data_sink_worker/src/bin/fix-member-attributes.ts b/services/apps/data_sink_worker/src/bin/fix-member-attributes.ts index ad847e8b88..302adbfc3f 100644 --- a/services/apps/data_sink_worker/src/bin/fix-member-attributes.ts +++ b/services/apps/data_sink_worker/src/bin/fix-member-attributes.ts @@ -51,7 +51,8 @@ async function getMemberIds( select m.id, m.attributes, m."manuallyChangedFields" from members m inner join - relevant_members rm on rm.id = m.id; + relevant_members rm on rm.id = m.id +order by m.id desc; `, { lastId }, ) @@ -83,6 +84,7 @@ from members m setImmediate(async () => { let dbClient: DbConnOrTx | undefined let redisClient: any | undefined + let exitCode = 0 try { log.info('Starting member attributes fix script') @@ -283,9 +285,9 @@ setImmediate(async () => { }, 'Fatal error in member attributes fix script', ) - process.exit(1) + exitCode = 1 } finally { log.info('Script execution completed') - process.exit(0) + process.exit(exitCode) } }) From 808425ff2c52ddeba8921d424cf772206e75051b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Uro=C5=A1=20Marolt?= Date: Tue, 24 Mar 2026 15:28:32 +0100 Subject: [PATCH 26/26] fix: bugfix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Uroš Marolt --- services/libs/common/src/member.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/services/libs/common/src/member.ts b/services/libs/common/src/member.ts index 322140839d..58d64a6b03 100644 --- a/services/libs/common/src/member.ts +++ b/services/libs/common/src/member.ts @@ -33,7 +33,17 @@ export async function setAttributesDefaultValues( ;(attributes[attributeName] as any).default = attributes[attributeName][highestPriorityPlatform] } else { - delete attributes[attributeName] + // Only delete if there is no existing non-empty default value. + // An attribute with only a `default` key and no platform-specific keys + // has no source platform to derive from, but its value should be preserved. + const existingDefault = (attributes[attributeName] as any).default + if ( + existingDefault === undefined || + existingDefault === null || + String(existingDefault).trim().length === 0 + ) { + delete attributes[attributeName] + } } }