Skip to content

Commit af73d9b

Browse files
Refactor
1 parent ba42964 commit af73d9b

File tree

4 files changed

+445
-293
lines changed

4 files changed

+445
-293
lines changed

packages/cli/src/commands/generate-test-payload.ts

Lines changed: 35 additions & 293 deletions
Original file line numberDiff line numberDiff line change
@@ -6,24 +6,16 @@ import * as path from 'path'
66
import { autoPrompt } from '../lib/prompt'
77
import { loadDestination } from '../lib/destinations'
88
import { DestinationDefinition } from '../lib/destinations'
9-
import { BrowserDestinationDefinition } from '@segment/destinations-manifest'
9+
import { InputField, BaseActionDefinition } from '@segment/actions-core'
1010
import {
11-
GlobalSetting,
12-
isDirective,
13-
DestinationDefinition as CloudModeDestinationDefinition,
14-
InputField,
15-
AudienceDestinationDefinition,
16-
BaseActionDefinition
17-
} from '@segment/actions-core'
18-
import Chance from 'chance'
19-
import { getRawKeys } from '@segment/actions-core/mapping-kit/value-keys'
20-
import { get, set } from 'lodash'
21-
import { ErrorCondition, GroupCondition, parseFql } from '@segment/destination-subscriptions'
22-
import { reconstructSegmentEvent } from '../lib/event-generator'
11+
generateSamplePayloadFromMapping,
12+
generateDestinationSettings,
13+
generateAudienceSettings,
14+
addAudienceSettingsToPayload
15+
} from '../lib/payload-generator/payload'
2316

2417
export default class GenerateTestPayload extends Command {
2518
private spinner: ora.Ora = ora()
26-
private chance: Chance.Chance = new Chance('Payload')
2719

2820
static description = `Generates sample test payload curl commands for a cloud mode destination.`
2921

@@ -157,28 +149,11 @@ export default class GenerateTestPayload extends Command {
157149
this.spinner.start(`Generating test payload for action: ${actionSlug}`)
158150

159151
try {
160-
let settings: unknown
161-
let auth: unknown
162-
if ((destination as BrowserDestinationDefinition).mode == 'device') {
163-
// Generate sample settings based on destination settings schema
164-
const destinationSettings = (destination as BrowserDestinationDefinition).settings
165-
settings = this.generateSampleFromSchema(destinationSettings || {})
166-
} else if ((destination as CloudModeDestinationDefinition).mode == 'cloud') {
167-
const destinationSettings = (destination as CloudModeDestinationDefinition).authentication?.fields
168-
settings = this.generateSampleFromSchema(destinationSettings || {})
169-
if ((destination as CloudModeDestinationDefinition).authentication?.scheme === 'oauth2') {
170-
auth = {
171-
oauth: {
172-
accessToken: 'YOUR_ACCESS_TOKEN',
173-
refreshToken: 'YOUR_REFRESH_TOKEN'
174-
}
175-
}
176-
}
177-
}
152+
// Generate destination settings and auth
153+
const { settings, auth } = generateDestinationSettings(destination)
178154

179-
const audienceSettings = {
180-
...(destination as AudienceDestinationDefinition)?.audienceFields
181-
}
155+
// Get audience settings
156+
const audienceSettings = generateAudienceSettings(destination)
182157

183158
// Generate sample mapping based on action fields
184159
const mapping = {} as Record<string, any>
@@ -197,30 +172,15 @@ export default class GenerateTestPayload extends Command {
197172
const defaultSubscription = (action as BaseActionDefinition).defaultSubscription
198173

199174
// Generate sample payload based on the fields.
200-
const payload = this.generateSamplePayloadFromMapping(mapping, fields, defaultSubscription)
175+
let payload = generateSamplePayloadFromMapping(mapping, fields, defaultSubscription)
201176

202-
// if audience settings exist, add them to the payload
177+
// Add audience settings to payload if they exist
203178
if (Object.keys(audienceSettings).length > 0) {
204-
set(payload, 'context.personas.audience_settings', this.generateSampleFromSchema(audienceSettings || {}))
179+
payload = addAudienceSettingsToPayload(payload, destination)
205180
}
206181

207182
// Generate final sample request
208-
const sampleRequest = {
209-
settings,
210-
mapping,
211-
payload,
212-
auth,
213-
features: {
214-
feature1: true,
215-
feature2: false
216-
},
217-
subscriptionMetadata: {
218-
actionConfigId: 'YOUR_ACTION_CONFIG_ID',
219-
destinationConfigId: 'YOUR_DESTINATION_CONFIG_ID',
220-
actionId: 'YOUR_ACTION_ID',
221-
sourceId: 'YOUR_SOURCE_ID'
222-
}
223-
}
183+
const sampleRequest = this.generateSampleRequest(settings, mapping, payload, auth)
224184

225185
this.spinner.succeed(`Generated test payload for action: ${actionSlug}`)
226186

@@ -234,249 +194,31 @@ export default class GenerateTestPayload extends Command {
234194
}
235195
}
236196

237-
generateSampleFromSchema(schema: Record<string, GlobalSetting>): Record<string, any> {
238-
const result: Record<string, any> = {}
239-
for (const [propName, setting] of Object.entries(schema)) {
240-
if (setting.default !== undefined) {
241-
result[propName] = setting.default
242-
} else {
243-
result[propName] = this.generatePlaceholderForSchema(setting)
244-
}
245-
}
246-
247-
return result
248-
}
249-
250-
generatePlaceholderForSchema(schema: GlobalSetting): any {
251-
const type = schema.type
252-
253-
switch (type) {
254-
case 'string':
255-
if (schema.choices) {
256-
return schema.choices[0]
257-
}
258-
return `<${schema.label || 'VALUE'}>`
259-
case 'number':
260-
return 0
261-
case 'boolean':
262-
return false
263-
case 'password':
264-
return `<${schema.label || 'PASSWORD'}>`
265-
default:
266-
return null
267-
}
268-
}
269-
270-
generateSamplePayloadFromMapping(
197+
/**
198+
* Generates a complete test request for a destination action.
199+
*/
200+
generateSampleRequest(
201+
settings: unknown,
271202
mapping: Record<string, any>,
272-
fields: Record<string, InputField>,
273-
defaultSubscription?: string
203+
payload: Record<string, any>,
204+
auth?: unknown
274205
): Record<string, any> {
275-
const chance = new Chance('payload')
276-
277-
const payload: Record<string, any> = {
278-
userId: chance.guid(),
279-
anonymousId: chance.guid(),
280-
event: 'Example Event',
281-
timestamp: new Date().toISOString(),
282-
context: {
283-
ip: chance.ip(),
284-
userAgent:
285-
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36',
286-
page: {
287-
path: `/${chance.word()}`,
288-
url: chance.url(),
289-
referrer: chance.url(),
290-
title: `${chance.capitalize(chance.word())} ${chance.capitalize(chance.word())}`
291-
},
292-
locale: chance.locale(),
293-
library: {
294-
name: 'analytics.js',
295-
version: `${chance.integer({ min: 1, max: 5 })}.${chance.integer({ min: 0, max: 20 })}.${chance.integer({
296-
min: 0,
297-
max: 99
298-
})}`
299-
}
206+
return {
207+
settings,
208+
mapping,
209+
payload,
210+
auth,
211+
features: {
212+
feature1: true,
213+
feature2: false
214+
},
215+
subscriptionMetadata: {
216+
actionConfigId: 'YOUR_ACTION_CONFIG_ID',
217+
destinationConfigId: 'YOUR_DESTINATION_CONFIG_ID',
218+
actionId: 'YOUR_ACTION_ID',
219+
sourceId: 'YOUR_SOURCE_ID'
300220
}
301221
}
302-
303-
// Add properties based on mapping with better values
304-
for (const [key, value] of Object.entries(mapping)) {
305-
if (isDirective(value)) {
306-
const [pathKey] = getRawKeys(value)
307-
const path = pathKey.replace('$.', '')
308-
const fieldDefinition = fields[key]
309-
const existingValue = get(payload, path)
310-
const newValue = this.setTestData(fieldDefinition, key)
311-
if (typeof existingValue === 'object' && existingValue !== null && !Array.isArray(existingValue)) {
312-
set(payload, path, { ...existingValue, ...newValue })
313-
} else {
314-
set(payload, path, newValue)
315-
}
316-
}
317-
}
318-
319-
if (defaultSubscription) {
320-
const parsed = parseFql(defaultSubscription)
321-
if ((parsed as ErrorCondition).error) {
322-
this.log(chalk.red(`Failed to parse FQL: ${(parsed as ErrorCondition).error}`))
323-
} else {
324-
const groupCondition = parsed as GroupCondition
325-
return reconstructSegmentEvent(groupCondition.children, payload)
326-
}
327-
}
328-
329-
return payload
330-
}
331-
332-
setTestData(fieldDefinition: Omit<InputField, 'Description'>, fieldName: string) {
333-
const chance = this.chance
334-
const { type, format, choices, multiple } = fieldDefinition
335-
336-
if (Array.isArray(choices)) {
337-
if (typeof choices[0] === 'object' && 'value' in choices[0]) {
338-
return choices[0].value
339-
}
340-
341-
return choices[0]
342-
}
343-
let val: any
344-
switch (type) {
345-
case 'boolean':
346-
val = chance.bool()
347-
break
348-
case 'datetime':
349-
val = '2021-02-01T00:00:00.000Z'
350-
break
351-
case 'integer':
352-
val = chance.integer()
353-
break
354-
case 'number':
355-
val = chance.floating({ fixed: 2 })
356-
break
357-
case 'text':
358-
val = chance.sentence()
359-
break
360-
case 'object':
361-
if (fieldDefinition.properties) {
362-
val = {}
363-
for (const [key, prop] of Object.entries(fieldDefinition.properties)) {
364-
val[key] = this.setTestData(prop as Omit<InputField, 'Description'>, key)
365-
}
366-
}
367-
break
368-
default:
369-
// covers string
370-
switch (format) {
371-
case 'date': {
372-
const d = chance.date()
373-
val = [d.getFullYear(), d.getMonth() + 1, d.getDate()].map((v) => String(v).padStart(2, '0')).join('-')
374-
break
375-
}
376-
case 'date-time':
377-
val = chance.date().toISOString()
378-
break
379-
case 'email':
380-
val = chance.email()
381-
break
382-
case 'hostname':
383-
val = chance.domain()
384-
break
385-
case 'ipv4':
386-
val = chance.ip()
387-
break
388-
case 'ipv6':
389-
val = chance.ipv6()
390-
break
391-
case 'time': {
392-
const d = chance.date()
393-
val = [d.getHours(), d.getMinutes(), d.getSeconds()].map((v) => String(v).padStart(2, '0')).join(':')
394-
break
395-
}
396-
case 'uri':
397-
val = chance.url()
398-
break
399-
case 'uuid':
400-
val = chance.guid()
401-
break
402-
default:
403-
val = this.generateValueByFieldName(fieldName)
404-
break
405-
}
406-
break
407-
}
408-
409-
if (multiple) {
410-
val = [val]
411-
}
412-
413-
return val
414-
}
415-
416-
generateValueByFieldName(fieldKey: string): any {
417-
const lowerFieldName = fieldKey.toLowerCase()
418-
419-
// Check for common field name patterns
420-
if (lowerFieldName.includes('email')) {
421-
return this.chance.email()
422-
} else if (lowerFieldName.includes('phone') || lowerFieldName.includes('mobile')) {
423-
return `+${this.chance.phone({ formatted: false })}`
424-
} else if (lowerFieldName.includes('name')) {
425-
if (lowerFieldName.includes('first')) {
426-
return this.chance.first()
427-
} else if (lowerFieldName.includes('last')) {
428-
return this.chance.last()
429-
} else if (lowerFieldName.includes('full')) {
430-
return this.chance.name()
431-
} else {
432-
return this.chance.name()
433-
}
434-
} else if (lowerFieldName.includes('url') || lowerFieldName.includes('link')) {
435-
return this.chance.url()
436-
} else if (lowerFieldName.includes('date')) {
437-
return this.chance.date().toISOString()
438-
} else if (lowerFieldName.includes('time')) {
439-
return this.chance.date().toISOString()
440-
} else if (
441-
lowerFieldName.includes('price') ||
442-
lowerFieldName.includes('amount') ||
443-
lowerFieldName.includes('total')
444-
) {
445-
return this.chance.floating({ min: 1, max: 1000, fixed: 2 })
446-
} else if (lowerFieldName.includes('currency')) {
447-
return this.chance.currency().code
448-
} else if (lowerFieldName.includes('country')) {
449-
return this.chance.country()
450-
} else if (lowerFieldName.includes('city')) {
451-
return this.chance.city()
452-
} else if (lowerFieldName.includes('state') || lowerFieldName.includes('province')) {
453-
return this.chance.state()
454-
} else if (lowerFieldName.includes('zip') || lowerFieldName.includes('postal')) {
455-
return this.chance.zip()
456-
} else if (lowerFieldName.includes('address')) {
457-
return this.chance.address()
458-
} else if (lowerFieldName.includes('company') || lowerFieldName.includes('organization')) {
459-
return this.chance.company()
460-
} else if (lowerFieldName.includes('description')) {
461-
return this.chance.paragraph()
462-
} else if (lowerFieldName.includes('id')) {
463-
return this.chance.guid()
464-
} else if (lowerFieldName.includes('quantity') || lowerFieldName.includes('count')) {
465-
return this.chance.integer({ min: 1, max: 10 })
466-
} else if (lowerFieldName.includes('age')) {
467-
return this.chance.age()
468-
} else if (lowerFieldName === 'gender') {
469-
return this.chance.gender()
470-
} else if (
471-
lowerFieldName.includes('boolean') ||
472-
lowerFieldName.includes('enabled') ||
473-
lowerFieldName.includes('active')
474-
) {
475-
return this.chance.bool()
476-
} else {
477-
// Default fallback
478-
return this.chance.word()
479-
}
480222
}
481223

482224
async catch(error: unknown) {
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { AudienceDestinationDefinition } from '@segment/actions-core'
2+
import { set } from 'lodash'
3+
import { generateSampleFromSchema } from './settings'
4+
5+
/**
6+
* Generates audience settings based on the destination definition.
7+
*/
8+
export function generateAudienceSettings(destination: any): Record<string, any> {
9+
return {
10+
...(destination as AudienceDestinationDefinition)?.audienceFields
11+
}
12+
}
13+
14+
/**
15+
* Adds audience settings to a payload if applicable.
16+
*/
17+
export function addAudienceSettingsToPayload(payload: Record<string, any>, destination: any): Record<string, any> {
18+
const audienceSettings = generateAudienceSettings(destination)
19+
20+
if (Object.keys(audienceSettings).length > 0) {
21+
const audienceSettingsValues = generateSampleFromSchema(audienceSettings || {})
22+
set(payload, 'context.personas.audience_settings', audienceSettingsValues)
23+
}
24+
25+
return payload
26+
}

0 commit comments

Comments
 (0)