Skip to content

Commit

Permalink
adding new s3 csv destination
Browse files Browse the repository at this point in the history
  • Loading branch information
joe-ayoub-segment committed Jul 18, 2024
1 parent 53d8440 commit a6caca3
Show file tree
Hide file tree
Showing 10 changed files with 735 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// import nock from 'nock'
// // import { createTestEvent, createTestIntegration } from '@segment/actions-core'
// import { createTestIntegration } from '@segment/actions-core'
// import Definition from '../index'

//const testDestination = createTestIntegration(Definition)

describe('Aws S3', () => {
describe('testAuthentication', () => {
it('should validate authentication inputs', async () => {
expect(true).toEqual(true)
})
})
})

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

103 changes: 103 additions & 0 deletions packages/destination-actions/src/destinations/aws-s3/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { AudienceDestinationDefinition, IntegrationError } from '@segment/actions-core'
import type { AudienceSettings, Settings } from './generated-types'

import syncAudienceToCSV from './syncAudienceToCSV'

type PersonasSettings = {
computation_id: string
}

const destination: AudienceDestinationDefinition<Settings, AudienceSettings> = {
name: 'AWS S3 CSV',
slug: 'actions-s3-csv',
mode: 'cloud',
description: 'Sync Segment event and Audience data to AWS S3.',
audienceFields: {
s3_aws_folder_name: {
label: 'AWS Subfolder Name',
description:
'Name of the S3 Subfolder where the files will be uploaded to. "/" must exist at the end of the folder name.',
type: 'string',
required: false
},
filename: {
label: 'Filename prefix',
description: `Prefix to append to the name of the uploaded file. A timestamp and lower cased audience name will be appended to the filename to ensure uniqueness.`,
type: 'string',
required: false
},
delimiter: {
label: 'Delimeter',
description: `Character used to separate tokens in the resulting file.`,
type: 'string',
required: true,
choices: [
{ label: 'comma', value: ',' },
{ label: 'pipe', value: '|' },
{ label: 'tab', value: 'tab' },
{ label: 'semicolon', value: ';' },
{ label: 'colon', value: ':' }
],
default: ','
}
},
authentication: {
scheme: 'custom',
fields: {
iam_role_arn: {
label: 'IAM Role ARN',
description:
'IAM role ARN with write permissions to the S3 bucket. Format: arn:aws:iam::account-id:role/role-name',
type: 'string',
required: true
},
s3_aws_bucket_name: {
label: 'AWS Bucket Name',
description: 'Name of the S3 bucket where the files will be uploaded to.',
type: 'string',
required: true
},
s3_aws_region: {
label: 'AWS Region Code (S3 only)',
description:
'Region Code where the S3 bucket is hosted. See [AWS S3 Documentation](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-regions-availability-zones.html#concepts-regions)',
type: 'string',
required: true
},
iam_external_id: {
label: 'IAM External ID',
description: 'The External ID to your IAM role. Generate a secure string and treat it like a password.',
type: 'password',
required: true
}
}
},
audienceConfig: {
mode: {
type: 'synced',
full_audience_sync: true
},
async createAudience(_, createAudienceInput) {
const audienceSettings = createAudienceInput.audienceSettings
// @ts-ignore type is not defined, and we will define it later
const personas = audienceSettings.personas as PersonasSettings
if (!personas) {
throw new IntegrationError('Missing computation parameters: Id and Key', 'MISSING_REQUIRED_FIELD', 400)
}

return { externalId: personas.computation_id }
},
async getAudience(_, getAudienceInput) {
const audience_id = getAudienceInput.externalId
if (!audience_id) {
throw new IntegrationError('Missing audience_id value', 'MISSING_REQUIRED_FIELD', 400)
}
return { externalId: audience_id }
}
},
actions: {
syncAudienceToCSV
}
}

export default destination
99 changes: 99 additions & 0 deletions packages/destination-actions/src/destinations/aws-s3/operations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { ExecuteInput } from '@segment/actions-core'
import type { Payload } from './syncAudienceToCSV/generated-types'
import type { AudienceSettings } from './generated-types'

// Type definitions
export type RawData = {
context?: {
personas?: {
computation_key?: string
computation_class?: string
computation_id?: string
}
}
}

export type ExecuteInputRaw<Settings, Payload, RawData, AudienceSettings = unknown> = ExecuteInput<
Settings,
Payload,
AudienceSettings
> & { rawData?: RawData }

function generateFile(payloads: Payload[], audienceSettings: AudienceSettings): string {
const headers: string[] = []
const columnsField = payloads[0].columns
const additionalColumns = payloads[0].additional_identifiers_and_traits_columns ?? []

Object.entries(columnsField).forEach(([_, value]) => {
if (value !== undefined) {
headers.push(value)
}
})

additionalColumns.forEach((additionalColumn) => {
headers.push(additionalColumn.value)
})

const headerString = `${headers.join(audienceSettings.delimiter === 'tab' ? '\t' : audienceSettings.delimiter)}\n`

const rows: string[] = [headerString]

payloads.forEach((payload, index, arr) => {
const action = payload.propertiesOrTraits[payload.audienceName]

const row: string[] = []
if (headers.includes('audience_name')) {
row.push(enquoteIdentifier(String(payload.audienceName ?? '')))
}
if (headers.includes('audience_id')) {
row.push(enquoteIdentifier(String(payload.audienceId ?? '')))
}
if (headers.includes('audience_action')) {
row.push(enquoteIdentifier(String(action ?? '')))
}
if (headers.includes('email')) {
row.push(enquoteIdentifier(String(payload.email ?? '')))
}
if (headers.includes('user_id')) {
row.push(enquoteIdentifier(String(payload.userId ?? '')))
}
if (headers.includes('anonymous_id')) {
row.push(enquoteIdentifier(String(payload.anonymousId ?? '')))
}
if (headers.includes('timestamp')) {
row.push(enquoteIdentifier(String(payload.timestamp ?? '')))
}
if (headers.includes('message_id')) {
row.push(enquoteIdentifier(String(payload.messageId ?? '')))
}
if (headers.includes('space_id')) {
row.push(enquoteIdentifier(String(payload.spaceId ?? '')))
}
if (headers.includes('integrations_object')) {
row.push(enquoteIdentifier(String(JSON.stringify(payload.integrationsObject) ?? '')))
}
if (headers.includes('properties_or_traits')) {
row.push(enquoteIdentifier(String(JSON.stringify(payload.propertiesOrTraits) ?? '')))
}

additionalColumns.forEach((additionalColumn) => {
//row.push(enquoteIdentifier(String(JSON.stringify(payload.propertiesOrTraits[additionalColumn.key]) ?? '')))
row.push(enquoteIdentifier(String(JSON.stringify(additionalColumn.key) ?? '')))
})

const isLastRow = arr.length === index + 1
const rowString = `${row.join(audienceSettings.delimiter === 'tab' ? '\t' : audienceSettings.delimiter)}${
isLastRow ? '' : '\n'
}`

rows.push(rowString)
})

return rows.join('')
}

function enquoteIdentifier(str: string) {
return `"${String(str).replace(/"/g, '""')}"`
}

export { generateFile, enquoteIdentifier }
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const ACTION_SLUG = 'actions-s3-csv'
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
describe('Aws S3', () => {
describe('uploadCsv', () => {
it('upload CSV', async () => {
expect(true).toBe(true)
})
})
})

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

Loading

0 comments on commit a6caca3

Please sign in to comment.