From ea73a8773b7da054336ae04095e49169d435585e Mon Sep 17 00:00:00 2001 From: Mouad BANI Date: Fri, 3 Apr 2026 13:41:57 +0100 Subject: [PATCH 01/15] feat: meetings implementation Signed-off-by: Mouad BANI --- .../src/consumer/transformerConsumer.ts | 28 ++-- .../src/core/transformerBase.ts | 15 ++- .../src/integrations/index.ts | 11 ++ .../meeting-attendance/buildSourceQuery.ts | 81 ++++++++++++ .../meeting-attendance/transformer.ts | 121 ++++++++++++++++++ .../src/integrations/types.ts | 1 + 6 files changed, 240 insertions(+), 17 deletions(-) create mode 100644 services/apps/snowflake_connectors/src/integrations/meetings/meeting-attendance/buildSourceQuery.ts create mode 100644 services/apps/snowflake_connectors/src/integrations/meetings/meeting-attendance/transformer.ts diff --git a/services/apps/snowflake_connectors/src/consumer/transformerConsumer.ts b/services/apps/snowflake_connectors/src/consumer/transformerConsumer.ts index 0161048e2f..93289b8008 100644 --- a/services/apps/snowflake_connectors/src/consumer/transformerConsumer.ts +++ b/services/apps/snowflake_connectors/src/consumer/transformerConsumer.ts @@ -89,24 +89,26 @@ export class TransformerConsumer { let resolveSkippedCount = 0 for await (const row of this.s3Service.streamParquetRows(job.s3Path)) { - const result = source.transformer.safeTransformRow(row) - if (!result) { + const results = source.transformer.safeTransformRow(row) + if (!results) { transformSkippedCount++ continue } - const resolved = await this.integrationResolver.resolve(platform, result.segment) - if (!resolved) { - resolveSkippedCount++ - continue + for (const result of results) { + const resolved = await this.integrationResolver.resolve(platform, result.segment) + if (!resolved) { + resolveSkippedCount++ + continue + } + + await this.emitter.createAndProcessActivityResult( + resolved.segmentId, + resolved.integrationId, + result.activity, + ) + transformedCount++ } - - await this.emitter.createAndProcessActivityResult( - resolved.segmentId, - resolved.integrationId, - result.activity, - ) - transformedCount++ } const skippedCount = transformSkippedCount + resolveSkippedCount diff --git a/services/apps/snowflake_connectors/src/core/transformerBase.ts b/services/apps/snowflake_connectors/src/core/transformerBase.ts index cbbc5e957e..011bfaad00 100644 --- a/services/apps/snowflake_connectors/src/core/transformerBase.ts +++ b/services/apps/snowflake_connectors/src/core/transformerBase.ts @@ -24,10 +24,12 @@ export abstract class TransformerBase { abstract readonly platform: PlatformType /** - * Transform a single raw row from the S3 export into an activity + * Transform a single raw row from the S3 export into one or more activities * along with routing metadata. Returns null if the row should be skipped. */ - abstract transformRow(row: Record): TransformedActivity | null + abstract transformRow( + row: Record, + ): TransformedActivity | TransformedActivity[] | null private static readonly INDIVIDUAL_NO_ACCOUNT_RE = /^individual\s*(?:[-–?]|with)\s*no\s+account$/i @@ -104,10 +106,15 @@ export abstract class TransformerBase { /** * Safe wrapper around transformRow that catches errors and returns null. + * Always normalizes the result to an array for consistent consumption. */ - safeTransformRow(row: Record): TransformedActivity | null { + safeTransformRow(row: Record): TransformedActivity[] | null { try { - return this.transformRow(row) + const result = this.transformRow(row) + if (result === null) { + return null + } + return Array.isArray(result) ? result : [result] } catch (err) { const message = err instanceof Error ? err.message : String(err) const stack = err instanceof Error ? err.stack : undefined diff --git a/services/apps/snowflake_connectors/src/integrations/index.ts b/services/apps/snowflake_connectors/src/integrations/index.ts index 7adc90bec0..6a6415d8f4 100644 --- a/services/apps/snowflake_connectors/src/integrations/index.ts +++ b/services/apps/snowflake_connectors/src/integrations/index.ts @@ -8,6 +8,8 @@ import { PlatformType } from '@crowd/types' import { buildSourceQuery as cventBuildSourceQuery } from './cvent/event-registrations/buildSourceQuery' import { CventTransformer } from './cvent/event-registrations/transformer' +import { buildSourceQuery as meetingAttendanceBuildQuery } from './meetings/meeting-attendance/buildSourceQuery' +import { MeetingAttendanceTransformer } from './meetings/meeting-attendance/transformer' import { buildSourceQuery as tncCertificatesBuildQuery } from './tnc/certificates/buildSourceQuery' import { TncCertificatesTransformer } from './tnc/certificates/transformer' import { buildSourceQuery as tncCoursesBuildQuery } from './tnc/courses/buildSourceQuery' @@ -20,6 +22,15 @@ export type { BuildSourceQuery, DataSource, PlatformDefinition } from './types' export { DataSourceName } from './types' const supported: Partial> = { + [PlatformType.MEETINGS]: { + sources: [ + { + name: DataSourceName.MEETINGS_MEETING_ATTENDANCE, + buildSourceQuery: meetingAttendanceBuildQuery, + transformer: new MeetingAttendanceTransformer(), + }, + ], + }, [PlatformType.CVENT]: { sources: [ { diff --git a/services/apps/snowflake_connectors/src/integrations/meetings/meeting-attendance/buildSourceQuery.ts b/services/apps/snowflake_connectors/src/integrations/meetings/meeting-attendance/buildSourceQuery.ts new file mode 100644 index 0000000000..faf762e07d --- /dev/null +++ b/services/apps/snowflake_connectors/src/integrations/meetings/meeting-attendance/buildSourceQuery.ts @@ -0,0 +1,81 @@ +import { IS_PROD_ENV } from '@crowd/common' + +const CDP_MATCHED_SEGMENTS = ` + cdp_matched_segments AS ( + SELECT DISTINCT + s.SOURCE_ID AS sourceId, + s.slug + FROM ANALYTICS.BRONZE_KAFKA_CROWD_DEV.SEGMENTS s + WHERE s.PARENT_SLUG IS NOT NULL + AND s.GRANDPARENTS_SLUG IS NOT NULL + AND s.SOURCE_ID IS NOT NULL + )` + +export const buildSourceQuery = (sinceTimestamp?: string): string => { + let select = ` + SELECT + t.PRIMARY_KEY, + t.MEETING_ID, + t.MEETING_NAME, + t.PROJECT_ID, + t.PROJECT_NAME, + t.PROJECT_SLUG, + t.ACCOUNT_ID, + t.ACCOUNT_NAME, + t.MEETING_DATE, + t.MEETING_TIME, + t.INVITEE_FULL_NAME, + t.INVITEE_LF_SSO, + t.INVITEE_LF_USER_ID, + t.INVITEE_EMAIL, + t.INVITEE_ATTENDED, + t.WAS_INVITED, + t.RAW_COMMITTEE_TYPE + FROM ANALYTICS.SILVER_FACT.MEETING_ATTENDANCE t + INNER JOIN cdp_matched_segments cms + ON cms.slug = t.PROJECT_SLUG + AND cms.sourceId = t.PROJECT_ID + WHERE (t.WAS_INVITED = TRUE OR t.INVITEE_ATTENDED = TRUE)` + + if (!IS_PROD_ENV) { + select += ` AND t.PROJECT_SLUG = 'cncf'` + } + + const dedup = ` + QUALIFY ROW_NUMBER() OVER (PARTITION BY t.PRIMARY_KEY ORDER BY t.MEETING_DATE DESC) = 1` + + if (!sinceTimestamp) { + return ` + WITH ${CDP_MATCHED_SEGMENTS} + ${select} + ${dedup}`.trim() + } + + return ` + WITH ${CDP_MATCHED_SEGMENTS}, + new_cdp_segments AS ( + SELECT DISTINCT + s.SOURCE_ID AS sourceId, + s.slug + FROM ANALYTICS.BRONZE_KAFKA_CROWD_DEV.SEGMENTS s + WHERE s.CREATED_TS >= '${sinceTimestamp}' + AND s.PARENT_SLUG IS NOT NULL + AND s.GRANDPARENTS_SLUG IS NOT NULL + AND s.SOURCE_ID IS NOT NULL + ) + + -- Updated records in existing segments + ${select} + AND t.MEETING_DATE > '${sinceTimestamp}'::DATE + ${dedup} + + UNION + + -- All records in newly created segments + ${select} + AND EXISTS ( + SELECT 1 FROM new_cdp_segments ncs + WHERE ncs.slug = cms.slug AND ncs.sourceId = cms.sourceId + ) + ${dedup}`.trim() +} diff --git a/services/apps/snowflake_connectors/src/integrations/meetings/meeting-attendance/transformer.ts b/services/apps/snowflake_connectors/src/integrations/meetings/meeting-attendance/transformer.ts new file mode 100644 index 0000000000..39e86299cb --- /dev/null +++ b/services/apps/snowflake_connectors/src/integrations/meetings/meeting-attendance/transformer.ts @@ -0,0 +1,121 @@ +import { MEETINGS_GRID, MeetingsActivityType } from '@crowd/integrations' +import { getServiceChildLogger } from '@crowd/logging' +import { IActivityData, OrganizationSource, PlatformType } from '@crowd/types' + +import { TransformedActivity, TransformerBase } from '../../../core/transformerBase' + +const log = getServiceChildLogger('meetingAttendanceTransformer') + +export class MeetingAttendanceTransformer extends TransformerBase { + readonly platform = PlatformType.MEETINGS + + transformRow(row: Record): TransformedActivity | TransformedActivity[] | null { + const email = (row.INVITEE_EMAIL as string | null)?.trim() || null + if (!email) { + log.debug({ primaryKey: row.PRIMARY_KEY }, 'Skipping row: missing email') + return null + } + + const lfUsername = (row.INVITEE_LF_SSO as string | null)?.trim() || null + const sourceId = (row.INVITEE_LF_USER_ID as string | null)?.trim() || undefined + const displayName = (row.INVITEE_FULL_NAME as string | null)?.trim() || email + + const identities = this.buildMemberIdentities({ + email, + sourceId, + platformUsername: null, + lfUsername, + }) + + const segmentSlug = (row.PROJECT_SLUG as string | null)?.trim() || null + const segmentSourceId = (row.PROJECT_ID as string | null)?.trim() || null + if (!segmentSlug || !segmentSourceId) { + return null + } + + const meetingDate = (row.MEETING_DATE as string | null) || null + const meetingTime = (row.MEETING_TIME as string | null) || null + const timestamp = + meetingDate && meetingTime ? `${meetingDate}T${meetingTime}` : meetingDate || null + + const primaryKey = (row.PRIMARY_KEY as string)?.trim() + + const attributes = { + meetingID: row.MEETING_ID, + scheduledTime: timestamp, + topic: (row.MEETING_NAME as string | null) || null, + projectID: (row.PROJECT_ID as string | null) || null, + projectName: (row.PROJECT_NAME as string | null) || null, + organizationId: (row.ACCOUNT_ID as string | null) || null, + organizationName: (row.ACCOUNT_NAME as string | null) || null, + meetingType: (row.RAW_COMMITTEE_TYPE as string | null) || null, + } + + const member = { + displayName, + identities, + organizations: this.buildOrganizations(row), + } + + const segment = { slug: segmentSlug, sourceId: segmentSourceId } + + const activities: TransformedActivity[] = [] + + if (row.WAS_INVITED === true) { + activities.push({ + activity: { + type: MeetingsActivityType.INVITED_MEETING, + platform: PlatformType.MEETINGS, + timestamp, + score: MEETINGS_GRID[MeetingsActivityType.INVITED_MEETING].score, + sourceId: `${primaryKey}_invited`, + member: { ...member }, + attributes, + } as IActivityData, + segment, + }) + } + + if (row.INVITEE_ATTENDED === true) { + activities.push({ + activity: { + type: MeetingsActivityType.ATTENDED_MEETING, + platform: PlatformType.MEETINGS, + timestamp, + score: MEETINGS_GRID[MeetingsActivityType.ATTENDED_MEETING].score, + sourceId: `${primaryKey}_attended`, + member: { ...member }, + attributes, + } as IActivityData, + segment, + }) + } + + if (activities.length === 0) { + return null + } + + return activities.length === 1 ? activities[0] : activities + } + + private buildOrganizations( + row: Record, + ): IActivityData['member']['organizations'] { + const accountName = (row.ACCOUNT_NAME as string | null)?.trim() || null + if (!accountName) { + return undefined + } + + if (this.isIndividualNoAccount(accountName)) { + return undefined + } + + return [ + { + displayName: accountName, + source: OrganizationSource.MEETINGS, + identities: [], + }, + ] + } +} diff --git a/services/apps/snowflake_connectors/src/integrations/types.ts b/services/apps/snowflake_connectors/src/integrations/types.ts index 36377a4f4f..f0f68b4aab 100644 --- a/services/apps/snowflake_connectors/src/integrations/types.ts +++ b/services/apps/snowflake_connectors/src/integrations/types.ts @@ -8,6 +8,7 @@ export enum DataSourceName { TNC_ENROLLMENTS = 'enrollments', TNC_CERTIFICATES = 'certificates', TNC_COURSES = 'courses', + MEETINGS_MEETING_ATTENDANCE = 'meeting-attendance', } export interface DataSource { From f2bf2c8c5e2c3439e843505f09724c922b71a31d Mon Sep 17 00:00:00 2001 From: Mouad BANI Date: Fri, 3 Apr 2026 13:42:21 +0100 Subject: [PATCH 02/15] feat: meetings implementation Signed-off-by: Mouad BANI --- .../U1775219382__addMeetingsActivityTypes.sql | 0 .../V1775219382__addMeetingsActivityTypes.sql | 3 +++ .../src/organizations/attributesConfig.ts | 1 + services/libs/integrations/src/integrations/index.ts | 2 ++ .../integrations/src/integrations/meetings/types.ts | 11 +++++++++++ services/libs/types/src/enums/organizations.ts | 2 ++ services/libs/types/src/enums/platforms.ts | 1 + 7 files changed, 20 insertions(+) create mode 100644 backend/src/database/migrations/U1775219382__addMeetingsActivityTypes.sql create mode 100644 backend/src/database/migrations/V1775219382__addMeetingsActivityTypes.sql create mode 100644 services/libs/integrations/src/integrations/meetings/types.ts diff --git a/backend/src/database/migrations/U1775219382__addMeetingsActivityTypes.sql b/backend/src/database/migrations/U1775219382__addMeetingsActivityTypes.sql new file mode 100644 index 0000000000..e69de29bb2 diff --git a/backend/src/database/migrations/V1775219382__addMeetingsActivityTypes.sql b/backend/src/database/migrations/V1775219382__addMeetingsActivityTypes.sql new file mode 100644 index 0000000000..5c4ec8ffc3 --- /dev/null +++ b/backend/src/database/migrations/V1775219382__addMeetingsActivityTypes.sql @@ -0,0 +1,3 @@ +INSERT INTO "activityTypes" ("activityType", platform, "isCodeContribution", "isCollaboration", description, "label") VALUES +('invited-meeting', 'meetings', false, false, 'User is invited to a meeting', 'Invited to a meeting'), +('attended-meeting', 'meetings', false, false, 'User attends a meeting', 'Attended a meeting'); diff --git a/services/libs/data-access-layer/src/organizations/attributesConfig.ts b/services/libs/data-access-layer/src/organizations/attributesConfig.ts index c8d0414c3c..06cbf45bf9 100644 --- a/services/libs/data-access-layer/src/organizations/attributesConfig.ts +++ b/services/libs/data-access-layer/src/organizations/attributesConfig.ts @@ -233,6 +233,7 @@ export const ORG_DB_ATTRIBUTE_SOURCE_PRIORITY = [ OrganizationAttributeSource.ENRICHMENT_PEOPLEDATALABS, OrganizationAttributeSource.CVENT, OrganizationAttributeSource.TNC, + OrganizationAttributeSource.MEETINGS, // legacy - keeping this for backward compatibility OrganizationAttributeSource.ENRICHMENT, OrganizationAttributeSource.GITHUB, diff --git a/services/libs/integrations/src/integrations/index.ts b/services/libs/integrations/src/integrations/index.ts index 3db691cd2b..5e43213340 100644 --- a/services/libs/integrations/src/integrations/index.ts +++ b/services/libs/integrations/src/integrations/index.ts @@ -52,4 +52,6 @@ export * from './cvent/types' export * from './tnc/types' +export * from './meetings/types' + export * from './activityDisplayService' diff --git a/services/libs/integrations/src/integrations/meetings/types.ts b/services/libs/integrations/src/integrations/meetings/types.ts new file mode 100644 index 0000000000..553e326114 --- /dev/null +++ b/services/libs/integrations/src/integrations/meetings/types.ts @@ -0,0 +1,11 @@ +import { IActivityScoringGrid } from '@crowd/types' + +export enum MeetingsActivityType { + INVITED_MEETING = 'invited-meeting', + ATTENDED_MEETING = 'attended-meeting', +} + +export const MEETINGS_GRID: Record = { + [MeetingsActivityType.INVITED_MEETING]: { score: 1 }, + [MeetingsActivityType.ATTENDED_MEETING]: { score: 1 }, +} diff --git a/services/libs/types/src/enums/organizations.ts b/services/libs/types/src/enums/organizations.ts index 3dc62f28dc..0d20a79063 100644 --- a/services/libs/types/src/enums/organizations.ts +++ b/services/libs/types/src/enums/organizations.ts @@ -14,6 +14,7 @@ export enum OrganizationSource { UI = 'ui', CVENT = 'cvent', TNC = 'tnc', + MEETINGS = 'meetings', } export enum OrganizationMergeSuggestionType { @@ -42,6 +43,7 @@ export enum OrganizationAttributeSource { ENRICHMENT_PEOPLEDATALABS = 'enrichment-peopledatalabs', CVENT = 'cvent', TNC = 'tnc', + MEETINGS = 'meetings', // legacy - keeping this for backward compatibility ENRICHMENT = 'enrichment', GITHUB = 'github', diff --git a/services/libs/types/src/enums/platforms.ts b/services/libs/types/src/enums/platforms.ts index 5090f17ad5..a4835879a8 100644 --- a/services/libs/types/src/enums/platforms.ts +++ b/services/libs/types/src/enums/platforms.ts @@ -26,6 +26,7 @@ export enum PlatformType { LFID = 'lfid', CVENT = 'cvent', TNC = 'tnc', + MEETINGS = 'meetings', GITLAB = 'gitlab', FACEBOOK = 'facebook', OTHER = 'other', From 6039d0625111b875b74f9445b2bd2c30cfa3180f Mon Sep 17 00:00:00 2001 From: Mouad BANI Date: Fri, 3 Apr 2026 15:27:23 +0100 Subject: [PATCH 03/15] fix: missing org data Signed-off-by: Mouad BANI --- .../meeting-attendance/buildSourceQuery.ts | 28 +++++++-- .../meeting-attendance/transformer.ts | 61 ++++++++++++++++++- 2 files changed, 82 insertions(+), 7 deletions(-) diff --git a/services/apps/snowflake_connectors/src/integrations/meetings/meeting-attendance/buildSourceQuery.ts b/services/apps/snowflake_connectors/src/integrations/meetings/meeting-attendance/buildSourceQuery.ts index faf762e07d..837f83b23f 100644 --- a/services/apps/snowflake_connectors/src/integrations/meetings/meeting-attendance/buildSourceQuery.ts +++ b/services/apps/snowflake_connectors/src/integrations/meetings/meeting-attendance/buildSourceQuery.ts @@ -11,6 +11,17 @@ const CDP_MATCHED_SEGMENTS = ` AND s.SOURCE_ID IS NOT NULL )` +const ORG_ACCOUNTS = ` + org_accounts AS ( + SELECT account_id, account_name, website, domain_aliases, LOGO_URL, INDUSTRY, N_EMPLOYEES + FROM analytics.bronze_fivetran_salesforce.accounts + WHERE website IS NOT NULL + UNION ALL + SELECT account_id, account_name, website, domain_aliases, NULL AS LOGO_URL, NULL AS INDUSTRY, NULL AS N_EMPLOYEES + FROM analytics.bronze_fivetran_salesforce_b2b.accounts + WHERE website IS NOT NULL + )` + export const buildSourceQuery = (sinceTimestamp?: string): string => { let select = ` SELECT @@ -30,11 +41,18 @@ export const buildSourceQuery = (sinceTimestamp?: string): string => { t.INVITEE_EMAIL, t.INVITEE_ATTENDED, t.WAS_INVITED, - t.RAW_COMMITTEE_TYPE + t.RAW_COMMITTEE_TYPE, + org.website AS ORG_WEBSITE, + org.domain_aliases AS ORG_DOMAIN_ALIASES, + org.logo_url AS LOGO_URL, + org.industry AS ORGANIZATION_INDUSTRY, + CAST(org.n_employees AS VARCHAR) AS ORGANIZATION_SIZE FROM ANALYTICS.SILVER_FACT.MEETING_ATTENDANCE t INNER JOIN cdp_matched_segments cms ON cms.slug = t.PROJECT_SLUG AND cms.sourceId = t.PROJECT_ID + LEFT JOIN org_accounts org + ON t.ACCOUNT_ID = org.account_id WHERE (t.WAS_INVITED = TRUE OR t.INVITEE_ATTENDED = TRUE)` if (!IS_PROD_ENV) { @@ -42,17 +60,19 @@ export const buildSourceQuery = (sinceTimestamp?: string): string => { } const dedup = ` - QUALIFY ROW_NUMBER() OVER (PARTITION BY t.PRIMARY_KEY ORDER BY t.MEETING_DATE DESC) = 1` + QUALIFY ROW_NUMBER() OVER (PARTITION BY t.PRIMARY_KEY ORDER BY org.website DESC) = 1` if (!sinceTimestamp) { return ` - WITH ${CDP_MATCHED_SEGMENTS} + WITH ${ORG_ACCOUNTS}, + ${CDP_MATCHED_SEGMENTS} ${select} ${dedup}`.trim() } return ` - WITH ${CDP_MATCHED_SEGMENTS}, + WITH ${ORG_ACCOUNTS}, + ${CDP_MATCHED_SEGMENTS}, new_cdp_segments AS ( SELECT DISTINCT s.SOURCE_ID AS sourceId, diff --git a/services/apps/snowflake_connectors/src/integrations/meetings/meeting-attendance/transformer.ts b/services/apps/snowflake_connectors/src/integrations/meetings/meeting-attendance/transformer.ts index 39e86299cb..4adad18ced 100644 --- a/services/apps/snowflake_connectors/src/integrations/meetings/meeting-attendance/transformer.ts +++ b/services/apps/snowflake_connectors/src/integrations/meetings/meeting-attendance/transformer.ts @@ -1,6 +1,12 @@ import { MEETINGS_GRID, MeetingsActivityType } from '@crowd/integrations' import { getServiceChildLogger } from '@crowd/logging' -import { IActivityData, OrganizationSource, PlatformType } from '@crowd/types' +import { + IActivityData, + IOrganizationIdentity, + OrganizationIdentityType, + OrganizationSource, + PlatformType, +} from '@crowd/types' import { TransformedActivity, TransformerBase } from '../../../core/transformerBase' @@ -106,15 +112,64 @@ export class MeetingAttendanceTransformer extends TransformerBase { return undefined } + const website = (row.ORG_WEBSITE as string | null)?.trim() || null + const domainAliases = (row.ORG_DOMAIN_ALIASES as string | null)?.trim() || null + if (this.isIndividualNoAccount(accountName)) { - return undefined + return [ + { + displayName: accountName, + source: OrganizationSource.MEETINGS, + identities: website + ? [ + { + platform: this.platform, + value: website, + type: OrganizationIdentityType.PRIMARY_DOMAIN, + verified: true, + }, + ] + : [], + }, + ] + } + + const identities: IOrganizationIdentity[] = [] + + if (website) { + identities.push({ + platform: this.platform, + value: website, + type: OrganizationIdentityType.PRIMARY_DOMAIN, + verified: true, + }) + } + + if (domainAliases) { + for (const alias of domainAliases.split(',')) { + const trimmed = alias.trim() + if (trimmed) { + identities.push({ + platform: this.platform, + value: trimmed, + type: OrganizationIdentityType.ALTERNATIVE_DOMAIN, + verified: true, + }) + } + } } return [ { displayName: accountName, source: OrganizationSource.MEETINGS, - identities: [], + identities, + logo: (row.LOGO_URL as string | null)?.trim() || undefined, + size: + typeof row.ORGANIZATION_SIZE === 'string' + ? row.ORGANIZATION_SIZE.trim() || undefined + : undefined, + industry: (row.ORGANIZATION_INDUSTRY as string | null)?.trim() || undefined, }, ] } From b3c3880b8e65af700d2e1f5b0dddc25ae72ef1d9 Mon Sep 17 00:00:00 2001 From: Mouad BANI Date: Fri, 3 Apr 2026 15:27:56 +0100 Subject: [PATCH 04/15] chore: add heads up for missing organization Signed-off-by: Mouad BANI --- .claude/skills/scaffold-snowflake-connector/SKILL.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.claude/skills/scaffold-snowflake-connector/SKILL.md b/.claude/skills/scaffold-snowflake-connector/SKILL.md index ab027fa2ee..023d7ae468 100644 --- a/.claude/skills/scaffold-snowflake-connector/SKILL.md +++ b/.claude/skills/scaffold-snowflake-connector/SKILL.md @@ -342,7 +342,10 @@ After all identity fields are confirmed, summarize how `buildMemberIdentities()` ### 3b. Organization Mapping -If Pre-Analysis determined there is no org data (no org-related columns found in any table), confirm: "I don't see any organization columns in the schema. Does this source have org/company data?" — if yes, proceed; if no, skip to 3c. +If Pre-Analysis determined there is no org data (no org-related columns found in any table): before asking the user, first read existing transformers in `services/apps/snowflake_connectors/src/integrations/` to check whether any of them join an org table using a key that also exists in the user's tables. If a match is found, prompt the user: +> "I don't see org columns in the tables you provided, but [EXISTING_PLATFORM] sources org data from `{ORG_TABLE}` via `{join_key}` — which also appears in your table. Did you mean to include this? (Recommended)" + +If no existing pattern is joinable, ask: "I don't see any org columns. Does this source have org/company data?" — if yes, ask for the table; if no, skip to 3c. If Pre-Analysis identified org columns: From fc137a650f423d84b49bd8d8b2c88f126ed19998 Mon Sep 17 00:00:00 2001 From: Mouad BANI Date: Fri, 3 Apr 2026 16:27:31 +0100 Subject: [PATCH 05/15] fix: activity timestamp Signed-off-by: Mouad BANI --- .../scaffold-snowflake-connector/SKILL.md | 5 +++++ .../meeting-attendance/transformer.ts | 22 +++++++++++++++---- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/.claude/skills/scaffold-snowflake-connector/SKILL.md b/.claude/skills/scaffold-snowflake-connector/SKILL.md index 023d7ae468..74ff0b8cad 100644 --- a/.claude/skills/scaffold-snowflake-connector/SKILL.md +++ b/.claude/skills/scaffold-snowflake-connector/SKILL.md @@ -196,6 +196,8 @@ After Step 1 and/or Step 2, build a column registry per table: **Store this as the canonical column reference. Every column name used in generated code must appear in this registry. Never assume or invent a column name.** +**Flag non-VARCHAR column types** (e.g., `DATE`, `TIME`, `TIMESTAMP_TZ`, `BOOLEAN`, `NUMBER`) — these arrive as native JS types from the Parquet reader, not strings (see touch point 9 rules). + For each JOIN table, check whether any existing transformer in `services/apps/snowflake_connectors/src/integrations/` queries from the same table. If yes, inherit its column mappings; if no, treat every column as unknown and derive it from sample data in the Pre-Analysis step below. ### Step 3 — Sample data @@ -568,6 +570,7 @@ File: `services/apps/snowflake_connectors/src/integrations/{platform}/{source}/b **Rules (enforced — do not deviate):** - Use explicit column names only. Do not use `table.*` or `table.* EXCLUDE (...)` in new implementations — existing sources (TNC, CVENT) use these patterns but new sources should list columns explicitly to avoid parquet encoding/decoding issues - If any TIMESTAMP_TZ columns exist in the schema, exclude and re-cast them as TIMESTAMP_NTZ (see CVENT pattern) +- Do not concatenate or transform date/time columns in SQL — keep them as separate columns and let the transformer handle type coercion (see touch point 9 rules) - Follow the CTE structure: 1. `org_accounts` CTE (if org data present) 2. `CDP_MATCHED_SEGMENTS` CTE (always) @@ -588,6 +591,8 @@ Show the full generated file and ask for confirmation before writing. File: `services/apps/snowflake_connectors/src/integrations/{platform}/{source}/transformer.ts` **Rules (enforced — do not deviate):** + +- **Parquet type coercion — never blindly cast `row.COLUMN as string`.** Snowflake types may arrive as native JS types after Parquet decoding (e.g., `DATE` → `Date` object, `TIME` → `number` in ms, `BOOLEAN` → `boolean`). Always check the Snowflake column type from the schema registry and handle the actual JS type the Parquet reader delivers — do not assume every column is a string. - All string comparisons must be case-insensitive: use `.toLowerCase()` on both sides of comparison only; preserve the original value in the output - No broad `else` statements — every branch must have an explicit condition - All column names referenced in code must exactly match the schema registry — never assumed diff --git a/services/apps/snowflake_connectors/src/integrations/meetings/meeting-attendance/transformer.ts b/services/apps/snowflake_connectors/src/integrations/meetings/meeting-attendance/transformer.ts index 4adad18ced..431fe9318a 100644 --- a/services/apps/snowflake_connectors/src/integrations/meetings/meeting-attendance/transformer.ts +++ b/services/apps/snowflake_connectors/src/integrations/meetings/meeting-attendance/transformer.ts @@ -12,6 +12,23 @@ import { TransformedActivity, TransformerBase } from '../../../core/transformerB const log = getServiceChildLogger('meetingAttendanceTransformer') +function toISOTimestamp(rawDate: unknown, rawTime: unknown): string | null { + const date = + rawDate instanceof Date + ? new Date(rawDate) + : typeof rawDate === 'string' + ? new Date(rawDate) + : null + if (!date || isNaN(date.getTime())) return null + if (typeof rawTime === 'number') { + date.setTime(date.getTime() + rawTime) + } else if (typeof rawTime === 'string' && rawTime.trim()) { + const combined = new Date(`${date.toISOString().slice(0, 10)}T${rawTime.trim()}Z`) + if (!isNaN(combined.getTime())) return combined.toISOString() + } + return date.toISOString() +} + export class MeetingAttendanceTransformer extends TransformerBase { readonly platform = PlatformType.MEETINGS @@ -39,10 +56,7 @@ export class MeetingAttendanceTransformer extends TransformerBase { return null } - const meetingDate = (row.MEETING_DATE as string | null) || null - const meetingTime = (row.MEETING_TIME as string | null) || null - const timestamp = - meetingDate && meetingTime ? `${meetingDate}T${meetingTime}` : meetingDate || null + const timestamp = toISOTimestamp(row.MEETING_DATE, row.MEETING_TIME) const primaryKey = (row.PRIMARY_KEY as string)?.trim() From d7f4e8c5ca2a6ef3f40a78fa583199caeb75dda1 Mon Sep 17 00:00:00 2001 From: Mouad BANI Date: Fri, 3 Apr 2026 16:41:23 +0100 Subject: [PATCH 06/15] fix: code review fixed Signed-off-by: Mouad BANI --- .../src/core/transformerBase.ts | 3 +- .../meeting-attendance/buildSourceQuery.ts | 2 +- .../meeting-attendance/transformer.ts | 68 +++++++++---------- 3 files changed, 34 insertions(+), 39 deletions(-) diff --git a/services/apps/snowflake_connectors/src/core/transformerBase.ts b/services/apps/snowflake_connectors/src/core/transformerBase.ts index 011bfaad00..f4c6cd8b9c 100644 --- a/services/apps/snowflake_connectors/src/core/transformerBase.ts +++ b/services/apps/snowflake_connectors/src/core/transformerBase.ts @@ -114,7 +114,8 @@ export abstract class TransformerBase { if (result === null) { return null } - return Array.isArray(result) ? result : [result] + const arr = Array.isArray(result) ? result : [result] + return arr.length > 0 ? arr : null } catch (err) { const message = err instanceof Error ? err.message : String(err) const stack = err instanceof Error ? err.stack : undefined diff --git a/services/apps/snowflake_connectors/src/integrations/meetings/meeting-attendance/buildSourceQuery.ts b/services/apps/snowflake_connectors/src/integrations/meetings/meeting-attendance/buildSourceQuery.ts index 837f83b23f..d8ef51417b 100644 --- a/services/apps/snowflake_connectors/src/integrations/meetings/meeting-attendance/buildSourceQuery.ts +++ b/services/apps/snowflake_connectors/src/integrations/meetings/meeting-attendance/buildSourceQuery.ts @@ -86,7 +86,7 @@ export const buildSourceQuery = (sinceTimestamp?: string): string => { -- Updated records in existing segments ${select} - AND t.MEETING_DATE > '${sinceTimestamp}'::DATE + AND t.MEETING_DATE >= '${sinceTimestamp}'::DATE ${dedup} UNION diff --git a/services/apps/snowflake_connectors/src/integrations/meetings/meeting-attendance/transformer.ts b/services/apps/snowflake_connectors/src/integrations/meetings/meeting-attendance/transformer.ts index 431fe9318a..1e99939fec 100644 --- a/services/apps/snowflake_connectors/src/integrations/meetings/meeting-attendance/transformer.ts +++ b/services/apps/snowflake_connectors/src/integrations/meetings/meeting-attendance/transformer.ts @@ -71,44 +71,36 @@ export class MeetingAttendanceTransformer extends TransformerBase { meetingType: (row.RAW_COMMITTEE_TYPE as string | null) || null, } - const member = { - displayName, - identities, - organizations: this.buildOrganizations(row), - } - - const segment = { slug: segmentSlug, sourceId: segmentSourceId } + const organizations = this.buildOrganizations(row) + + const buildActivity = ( + type: MeetingsActivityType, + sourceIdSuffix: string, + ): TransformedActivity => ({ + activity: { + type, + platform: PlatformType.MEETINGS, + timestamp, + score: MEETINGS_GRID[type].score, + sourceId: `${primaryKey}_${sourceIdSuffix}`, + member: { + displayName, + identities: [...identities], + organizations: organizations ? [...organizations] : undefined, + }, + attributes: { ...attributes }, + } as IActivityData, + segment: { slug: segmentSlug, sourceId: segmentSourceId }, + }) const activities: TransformedActivity[] = [] if (row.WAS_INVITED === true) { - activities.push({ - activity: { - type: MeetingsActivityType.INVITED_MEETING, - platform: PlatformType.MEETINGS, - timestamp, - score: MEETINGS_GRID[MeetingsActivityType.INVITED_MEETING].score, - sourceId: `${primaryKey}_invited`, - member: { ...member }, - attributes, - } as IActivityData, - segment, - }) + activities.push(buildActivity(MeetingsActivityType.INVITED_MEETING, 'invited')) } if (row.INVITEE_ATTENDED === true) { - activities.push({ - activity: { - type: MeetingsActivityType.ATTENDED_MEETING, - platform: PlatformType.MEETINGS, - timestamp, - score: MEETINGS_GRID[MeetingsActivityType.ATTENDED_MEETING].score, - sourceId: `${primaryKey}_attended`, - member: { ...member }, - attributes, - } as IActivityData, - segment, - }) + activities.push(buildActivity(MeetingsActivityType.ATTENDED_MEETING, 'attended')) } if (activities.length === 0) { @@ -121,18 +113,20 @@ export class MeetingAttendanceTransformer extends TransformerBase { private buildOrganizations( row: Record, ): IActivityData['member']['organizations'] { + const website = (row.ORG_WEBSITE as string | null)?.trim() || null + const domainAliases = (row.ORG_DOMAIN_ALIASES as string | null)?.trim() || null const accountName = (row.ACCOUNT_NAME as string | null)?.trim() || null - if (!accountName) { + + if (!accountName && !website && !domainAliases) { return undefined } - const website = (row.ORG_WEBSITE as string | null)?.trim() || null - const domainAliases = (row.ORG_DOMAIN_ALIASES as string | null)?.trim() || null + const displayName = accountName || website - if (this.isIndividualNoAccount(accountName)) { + if (this.isIndividualNoAccount(displayName)) { return [ { - displayName: accountName, + displayName, source: OrganizationSource.MEETINGS, identities: website ? [ @@ -175,7 +169,7 @@ export class MeetingAttendanceTransformer extends TransformerBase { return [ { - displayName: accountName, + displayName, source: OrganizationSource.MEETINGS, identities, logo: (row.LOGO_URL as string | null)?.trim() || undefined, From dce716b0ffa732dbd8062e07da83b060365e03d1 Mon Sep 17 00:00:00 2001 From: Mouad BANI Date: Mon, 6 Apr 2026 12:11:01 +0100 Subject: [PATCH 07/15] chore: standardize attributes key casing Signed-off-by: Mouad BANI --- .../integrations/meetings/meeting-attendance/transformer.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/apps/snowflake_connectors/src/integrations/meetings/meeting-attendance/transformer.ts b/services/apps/snowflake_connectors/src/integrations/meetings/meeting-attendance/transformer.ts index 1e99939fec..f027347237 100644 --- a/services/apps/snowflake_connectors/src/integrations/meetings/meeting-attendance/transformer.ts +++ b/services/apps/snowflake_connectors/src/integrations/meetings/meeting-attendance/transformer.ts @@ -61,10 +61,10 @@ export class MeetingAttendanceTransformer extends TransformerBase { const primaryKey = (row.PRIMARY_KEY as string)?.trim() const attributes = { - meetingID: row.MEETING_ID, + meetingId: row.MEETING_ID, scheduledTime: timestamp, topic: (row.MEETING_NAME as string | null) || null, - projectID: (row.PROJECT_ID as string | null) || null, + projectId: (row.PROJECT_ID as string | null) || null, projectName: (row.PROJECT_NAME as string | null) || null, organizationId: (row.ACCOUNT_ID as string | null) || null, organizationName: (row.ACCOUNT_NAME as string | null) || null, From 188c6c53dd021135bb7fa9e8236c1460647d80a6 Mon Sep 17 00:00:00 2001 From: Mouad BANI Date: Mon, 6 Apr 2026 15:12:20 +0100 Subject: [PATCH 08/15] fix: cast meetingid to varchar Signed-off-by: Mouad BANI --- .../meetings/meeting-attendance/buildSourceQuery.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/apps/snowflake_connectors/src/integrations/meetings/meeting-attendance/buildSourceQuery.ts b/services/apps/snowflake_connectors/src/integrations/meetings/meeting-attendance/buildSourceQuery.ts index d8ef51417b..3a01bb029c 100644 --- a/services/apps/snowflake_connectors/src/integrations/meetings/meeting-attendance/buildSourceQuery.ts +++ b/services/apps/snowflake_connectors/src/integrations/meetings/meeting-attendance/buildSourceQuery.ts @@ -26,7 +26,7 @@ export const buildSourceQuery = (sinceTimestamp?: string): string => { let select = ` SELECT t.PRIMARY_KEY, - t.MEETING_ID, + CAST(t.MEETING_ID AS VARCHAR) AS MEETING_ID, t.MEETING_NAME, t.PROJECT_ID, t.PROJECT_NAME, From befa516b629de7c01e7598332342e13a91c9a34c Mon Sep 17 00:00:00 2001 From: Mouad BANI Date: Thu, 9 Apr 2026 15:50:15 +0100 Subject: [PATCH 09/15] fix: populate null LF_SSO from other records having same LF_USER_ID Signed-off-by: Mouad BANI --- .../meeting-attendance/buildSourceQuery.ts | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/services/apps/snowflake_connectors/src/integrations/meetings/meeting-attendance/buildSourceQuery.ts b/services/apps/snowflake_connectors/src/integrations/meetings/meeting-attendance/buildSourceQuery.ts index 3a01bb029c..1572496266 100644 --- a/services/apps/snowflake_connectors/src/integrations/meetings/meeting-attendance/buildSourceQuery.ts +++ b/services/apps/snowflake_connectors/src/integrations/meetings/meeting-attendance/buildSourceQuery.ts @@ -22,6 +22,15 @@ const ORG_ACCOUNTS = ` WHERE website IS NOT NULL )` +const LF_SSO_LOOKUP = ` + lf_sso_lookup AS ( + SELECT INVITEE_LF_USER_ID AS LF_USER_ID, MAX(INVITEE_LF_SSO) AS LF_SSO + FROM ANALYTICS.SILVER_FACT.MEETING_ATTENDANCE + WHERE INVITEE_LF_SSO IS NOT NULL + AND INVITEE_LF_USER_ID IS NOT NULL + GROUP BY INVITEE_LF_USER_ID + )` + export const buildSourceQuery = (sinceTimestamp?: string): string => { let select = ` SELECT @@ -36,7 +45,7 @@ export const buildSourceQuery = (sinceTimestamp?: string): string => { t.MEETING_DATE, t.MEETING_TIME, t.INVITEE_FULL_NAME, - t.INVITEE_LF_SSO, + COALESCE(t.INVITEE_LF_SSO, sso.LF_SSO) AS INVITEE_LF_SSO, t.INVITEE_LF_USER_ID, t.INVITEE_EMAIL, t.INVITEE_ATTENDED, @@ -51,6 +60,8 @@ export const buildSourceQuery = (sinceTimestamp?: string): string => { INNER JOIN cdp_matched_segments cms ON cms.slug = t.PROJECT_SLUG AND cms.sourceId = t.PROJECT_ID + LEFT JOIN lf_sso_lookup sso + ON t.INVITEE_LF_USER_ID = sso.LF_USER_ID LEFT JOIN org_accounts org ON t.ACCOUNT_ID = org.account_id WHERE (t.WAS_INVITED = TRUE OR t.INVITEE_ATTENDED = TRUE)` @@ -65,7 +76,8 @@ export const buildSourceQuery = (sinceTimestamp?: string): string => { if (!sinceTimestamp) { return ` WITH ${ORG_ACCOUNTS}, - ${CDP_MATCHED_SEGMENTS} + ${CDP_MATCHED_SEGMENTS}, + ${LF_SSO_LOOKUP} ${select} ${dedup}`.trim() } @@ -73,6 +85,7 @@ export const buildSourceQuery = (sinceTimestamp?: string): string => { return ` WITH ${ORG_ACCOUNTS}, ${CDP_MATCHED_SEGMENTS}, + ${LF_SSO_LOOKUP}, new_cdp_segments AS ( SELECT DISTINCT s.SOURCE_ID AS sourceId, From efbec032be640c121ac808f413cf8305ce273101 Mon Sep 17 00:00:00 2001 From: Mouad BANI Date: Thu, 9 Apr 2026 16:07:22 +0100 Subject: [PATCH 10/15] fix: user updated_ts as timestamp for incremental updates Signed-off-by: Mouad BANI --- .../meetings/meeting-attendance/buildSourceQuery.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/services/apps/snowflake_connectors/src/integrations/meetings/meeting-attendance/buildSourceQuery.ts b/services/apps/snowflake_connectors/src/integrations/meetings/meeting-attendance/buildSourceQuery.ts index 1572496266..ea06e36333 100644 --- a/services/apps/snowflake_connectors/src/integrations/meetings/meeting-attendance/buildSourceQuery.ts +++ b/services/apps/snowflake_connectors/src/integrations/meetings/meeting-attendance/buildSourceQuery.ts @@ -51,6 +51,7 @@ export const buildSourceQuery = (sinceTimestamp?: string): string => { t.INVITEE_ATTENDED, t.WAS_INVITED, t.RAW_COMMITTEE_TYPE, + t.UPDATED_TS, org.website AS ORG_WEBSITE, org.domain_aliases AS ORG_DOMAIN_ALIASES, org.logo_url AS LOGO_URL, @@ -99,7 +100,7 @@ export const buildSourceQuery = (sinceTimestamp?: string): string => { -- Updated records in existing segments ${select} - AND t.MEETING_DATE >= '${sinceTimestamp}'::DATE + AND t.UPDATED_TS > '${sinceTimestamp}' ${dedup} UNION From c122c30387857b61fbcffd2ccf8b7c2a87c03848 Mon Sep 17 00:00:00 2001 From: Mouad BANI Date: Thu, 9 Apr 2026 16:18:13 +0100 Subject: [PATCH 11/15] chore: skip rows without email Signed-off-by: Mouad BANI --- .../meetings/meeting-attendance/buildSourceQuery.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/services/apps/snowflake_connectors/src/integrations/meetings/meeting-attendance/buildSourceQuery.ts b/services/apps/snowflake_connectors/src/integrations/meetings/meeting-attendance/buildSourceQuery.ts index ea06e36333..8d5555b912 100644 --- a/services/apps/snowflake_connectors/src/integrations/meetings/meeting-attendance/buildSourceQuery.ts +++ b/services/apps/snowflake_connectors/src/integrations/meetings/meeting-attendance/buildSourceQuery.ts @@ -65,7 +65,8 @@ export const buildSourceQuery = (sinceTimestamp?: string): string => { ON t.INVITEE_LF_USER_ID = sso.LF_USER_ID LEFT JOIN org_accounts org ON t.ACCOUNT_ID = org.account_id - WHERE (t.WAS_INVITED = TRUE OR t.INVITEE_ATTENDED = TRUE)` + WHERE (t.WAS_INVITED = TRUE OR t.INVITEE_ATTENDED = TRUE) + AND NULLIF(TRIM(t.INVITEE_EMAIL), '') IS NOT NULL` if (!IS_PROD_ENV) { select += ` AND t.PROJECT_SLUG = 'cncf'` From 1ac5d3a44cb70c5592de0435f28db037dced32ca Mon Sep 17 00:00:00 2001 From: Mouad BANI Date: Thu, 9 Apr 2026 18:22:50 +0100 Subject: [PATCH 12/15] fix: handle null userId Signed-off-by: Mouad BANI --- .../meeting-attendance/buildSourceQuery.ts | 2 +- .../meetings/meeting-attendance/transformer.ts | 17 +++++++++-------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/services/apps/snowflake_connectors/src/integrations/meetings/meeting-attendance/buildSourceQuery.ts b/services/apps/snowflake_connectors/src/integrations/meetings/meeting-attendance/buildSourceQuery.ts index 8d5555b912..43a606ba0e 100644 --- a/services/apps/snowflake_connectors/src/integrations/meetings/meeting-attendance/buildSourceQuery.ts +++ b/services/apps/snowflake_connectors/src/integrations/meetings/meeting-attendance/buildSourceQuery.ts @@ -34,7 +34,7 @@ const LF_SSO_LOOKUP = ` export const buildSourceQuery = (sinceTimestamp?: string): string => { let select = ` SELECT - t.PRIMARY_KEY, + MD5(COALESCE(CAST(t.PRIMARY_KEY AS VARCHAR), '') || '|' || COALESCE(CAST(t.COMMITTEE_ID AS VARCHAR), '')) AS GENERATED_SOURCE_ID, CAST(t.MEETING_ID AS VARCHAR) AS MEETING_ID, t.MEETING_NAME, t.PROJECT_ID, diff --git a/services/apps/snowflake_connectors/src/integrations/meetings/meeting-attendance/transformer.ts b/services/apps/snowflake_connectors/src/integrations/meetings/meeting-attendance/transformer.ts index f027347237..84e6fedf9d 100644 --- a/services/apps/snowflake_connectors/src/integrations/meetings/meeting-attendance/transformer.ts +++ b/services/apps/snowflake_connectors/src/integrations/meetings/meeting-attendance/transformer.ts @@ -40,12 +40,14 @@ export class MeetingAttendanceTransformer extends TransformerBase { } const lfUsername = (row.INVITEE_LF_SSO as string | null)?.trim() || null - const sourceId = (row.INVITEE_LF_USER_ID as string | null)?.trim() || undefined + const memberSourceId = (row.INVITEE_LF_USER_ID as string | null)?.trim() || undefined + const generatedSourceId = (row.GENERATED_SOURCE_ID as string | null)?.trim() || undefined + const userId = memberSourceId ?? generatedSourceId const displayName = (row.INVITEE_FULL_NAME as string | null)?.trim() || email const identities = this.buildMemberIdentities({ email, - sourceId, + sourceId: userId, platformUsername: null, lfUsername, }) @@ -58,10 +60,10 @@ export class MeetingAttendanceTransformer extends TransformerBase { const timestamp = toISOTimestamp(row.MEETING_DATE, row.MEETING_TIME) - const primaryKey = (row.PRIMARY_KEY as string)?.trim() + const meetingId = (row.MEETING_ID as string)?.trim() const attributes = { - meetingId: row.MEETING_ID, + meetingId, scheduledTime: timestamp, topic: (row.MEETING_NAME as string | null) || null, projectId: (row.PROJECT_ID as string | null) || null, @@ -75,14 +77,13 @@ export class MeetingAttendanceTransformer extends TransformerBase { const buildActivity = ( type: MeetingsActivityType, - sourceIdSuffix: string, ): TransformedActivity => ({ activity: { type, platform: PlatformType.MEETINGS, timestamp, score: MEETINGS_GRID[type].score, - sourceId: `${primaryKey}_${sourceIdSuffix}`, + sourceId: `${meetingId}-${userId}`, member: { displayName, identities: [...identities], @@ -96,11 +97,11 @@ export class MeetingAttendanceTransformer extends TransformerBase { const activities: TransformedActivity[] = [] if (row.WAS_INVITED === true) { - activities.push(buildActivity(MeetingsActivityType.INVITED_MEETING, 'invited')) + activities.push(buildActivity(MeetingsActivityType.INVITED_MEETING)) } if (row.INVITEE_ATTENDED === true) { - activities.push(buildActivity(MeetingsActivityType.ATTENDED_MEETING, 'attended')) + activities.push(buildActivity(MeetingsActivityType.ATTENDED_MEETING)) } if (activities.length === 0) { From 9d855d667b7ab8da28d0707f5694b13858dd421a Mon Sep 17 00:00:00 2001 From: Mouad BANI Date: Fri, 10 Apr 2026 11:32:13 +0100 Subject: [PATCH 13/15] fix: merge conflicts typo Signed-off-by: Mouad BANI --- services/apps/snowflake_connectors/src/integrations/index.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/services/apps/snowflake_connectors/src/integrations/index.ts b/services/apps/snowflake_connectors/src/integrations/index.ts index 98e2730a4f..acd9be1a6c 100644 --- a/services/apps/snowflake_connectors/src/integrations/index.ts +++ b/services/apps/snowflake_connectors/src/integrations/index.ts @@ -30,6 +30,9 @@ const supported: Partial> = { name: DataSourceName.MEETINGS_MEETING_ATTENDANCE, buildSourceQuery: meetingAttendanceBuildQuery, transformer: new MeetingAttendanceTransformer(), + }, + ], + }, [PlatformType.COMMITTEES]: { sources: [ { From 5ba1afd0657b669584f83920c37a30e954ac957a Mon Sep 17 00:00:00 2001 From: Mouad BANI Date: Fri, 10 Apr 2026 11:55:30 +0100 Subject: [PATCH 14/15] fix: ensure org identities not null Signed-off-by: Mouad BANI --- .../committees/committees/transformer.ts | 18 +++++++-------- .../meeting-attendance/transformer.ts | 22 +++++++++---------- 2 files changed, 18 insertions(+), 22 deletions(-) diff --git a/services/apps/snowflake_connectors/src/integrations/committees/committees/transformer.ts b/services/apps/snowflake_connectors/src/integrations/committees/committees/transformer.ts index 8b4e833098..694dfe0778 100644 --- a/services/apps/snowflake_connectors/src/integrations/committees/committees/transformer.ts +++ b/services/apps/snowflake_connectors/src/integrations/committees/committees/transformer.ts @@ -111,16 +111,14 @@ export class CommitteesCommitteesTransformer extends TransformerBase { { displayName, source: OrganizationSource.COMMITTEES, - identities: website - ? [ - { - platform: PlatformType.COMMITTEES, - value: website, - type: OrganizationIdentityType.PRIMARY_DOMAIN, - verified: true, - }, - ] - : [], + identities: [ + { + platform: PlatformType.COMMITTEES, + value: website, + type: OrganizationIdentityType.PRIMARY_DOMAIN, + verified: true, + }, + ], }, ] } diff --git a/services/apps/snowflake_connectors/src/integrations/meetings/meeting-attendance/transformer.ts b/services/apps/snowflake_connectors/src/integrations/meetings/meeting-attendance/transformer.ts index 84e6fedf9d..6e998ef1bb 100644 --- a/services/apps/snowflake_connectors/src/integrations/meetings/meeting-attendance/transformer.ts +++ b/services/apps/snowflake_connectors/src/integrations/meetings/meeting-attendance/transformer.ts @@ -116,12 +116,12 @@ export class MeetingAttendanceTransformer extends TransformerBase { ): IActivityData['member']['organizations'] { const website = (row.ORG_WEBSITE as string | null)?.trim() || null const domainAliases = (row.ORG_DOMAIN_ALIASES as string | null)?.trim() || null - const accountName = (row.ACCOUNT_NAME as string | null)?.trim() || null - if (!accountName && !website && !domainAliases) { + if (!website && !domainAliases) { return undefined } + const accountName = (row.ACCOUNT_NAME as string | null)?.trim() || null const displayName = accountName || website if (this.isIndividualNoAccount(displayName)) { @@ -129,16 +129,14 @@ export class MeetingAttendanceTransformer extends TransformerBase { { displayName, source: OrganizationSource.MEETINGS, - identities: website - ? [ - { - platform: this.platform, - value: website, - type: OrganizationIdentityType.PRIMARY_DOMAIN, - verified: true, - }, - ] - : [], + identities: [ + { + platform: this.platform, + value: website, + type: OrganizationIdentityType.PRIMARY_DOMAIN, + verified: true, + }, + ], }, ] } From f52614a5d00a4e0e3af5127a220c81dd54f4511b Mon Sep 17 00:00:00 2001 From: Mouad BANI Date: Fri, 10 Apr 2026 11:56:01 +0100 Subject: [PATCH 15/15] fix: lint Signed-off-by: Mouad BANI --- .../committees/committees/transformer.ts | 16 ++++++++-------- .../meetings/meeting-attendance/transformer.ts | 4 +--- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/services/apps/snowflake_connectors/src/integrations/committees/committees/transformer.ts b/services/apps/snowflake_connectors/src/integrations/committees/committees/transformer.ts index 694dfe0778..42da0504eb 100644 --- a/services/apps/snowflake_connectors/src/integrations/committees/committees/transformer.ts +++ b/services/apps/snowflake_connectors/src/integrations/committees/committees/transformer.ts @@ -111,14 +111,14 @@ export class CommitteesCommitteesTransformer extends TransformerBase { { displayName, source: OrganizationSource.COMMITTEES, - identities: [ - { - platform: PlatformType.COMMITTEES, - value: website, - type: OrganizationIdentityType.PRIMARY_DOMAIN, - verified: true, - }, - ], + identities: [ + { + platform: PlatformType.COMMITTEES, + value: website, + type: OrganizationIdentityType.PRIMARY_DOMAIN, + verified: true, + }, + ], }, ] } diff --git a/services/apps/snowflake_connectors/src/integrations/meetings/meeting-attendance/transformer.ts b/services/apps/snowflake_connectors/src/integrations/meetings/meeting-attendance/transformer.ts index 6e998ef1bb..4484506da5 100644 --- a/services/apps/snowflake_connectors/src/integrations/meetings/meeting-attendance/transformer.ts +++ b/services/apps/snowflake_connectors/src/integrations/meetings/meeting-attendance/transformer.ts @@ -75,9 +75,7 @@ export class MeetingAttendanceTransformer extends TransformerBase { const organizations = this.buildOrganizations(row) - const buildActivity = ( - type: MeetingsActivityType, - ): TransformedActivity => ({ + const buildActivity = (type: MeetingsActivityType): TransformedActivity => ({ activity: { type, platform: PlatformType.MEETINGS,