Skip to content

Commit b00cc91

Browse files
Add support for payload generation for other APIs
1 parent af73d9b commit b00cc91

File tree

3 files changed

+361
-163
lines changed

3 files changed

+361
-163
lines changed

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

Lines changed: 152 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,27 @@ import {
1111
generateSamplePayloadFromMapping,
1212
generateDestinationSettings,
1313
generateAudienceSettings,
14-
addAudienceSettingsToPayload
14+
addAudienceSettingsToPayload,
15+
generateSampleFromSchema
1516
} from '../lib/payload-generator/payload'
17+
import {
18+
API_ENDPOINTS,
19+
ApiEndpoint,
20+
getApiEndpointByName,
21+
getFormattedPath
22+
} from '../lib/payload-generator/api-definitions'
1623

1724
export default class GenerateTestPayload extends Command {
1825
private spinner: ora.Ora = ora()
1926

20-
static description = `Generates sample test payload curl commands for a cloud mode destination.`
27+
static description = `Generates sample test payload curl commands for different APIs in a cloud mode destination.`
2128

2229
static examples = [
2330
`$ ./bin/run generate-test-payload`,
2431
`$ ./bin/run generate-test-payload --destination=slack`,
25-
`$ ./bin/run generate-test-payload --destination=slack --action=postToChannel`
32+
`$ ./bin/run generate-test-payload --destination=slack --action=postToChannel`,
33+
`$ ./bin/run generate-test-payload --destination=slack --api="Execute Action"`,
34+
`$ ./bin/run generate-test-payload --destination=slack --api="Create Audience"`
2635
]
2736

2837
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -36,6 +45,9 @@ export default class GenerateTestPayload extends Command {
3645
char: 'a',
3746
description: 'specific action to generate test payload for'
3847
}),
48+
api: flags.boolean({
49+
description: 'prompt for API Selection'
50+
}),
3951
directory: flags.string({
4052
char: 'p',
4153
description: 'destination actions directory path',
@@ -53,6 +65,29 @@ export default class GenerateTestPayload extends Command {
5365
const { flags } = this.parse(GenerateTestPayload)
5466
let destinationName = flags.destination
5567
const isBrowser = !!flags.browser
68+
let selectedApiEndpoint: ApiEndpoint | undefined = getApiEndpointByName('Execute Action')
69+
70+
// Get the API endpoint based on the flag if provided
71+
if (flags.api) {
72+
// Prompt for API selection instead of exiting
73+
const { selectedApi } = await autoPrompt<{ selectedApi: string }>(flags, {
74+
type: 'select',
75+
name: 'selectedApi',
76+
message: 'Please select a valid API endpoint:',
77+
choices: API_ENDPOINTS.map((api) => ({
78+
title: api.name === 'Execute Action' ? 'Execute Action (default)' : api.name,
79+
value: api.name
80+
}))
81+
})
82+
selectedApiEndpoint = getApiEndpointByName(selectedApi)
83+
}
84+
85+
if (!selectedApiEndpoint) {
86+
this.warn('No valid API endpoint selected. Exiting.')
87+
this.exit(1)
88+
}
89+
90+
this.spinner.info(`Generating test payload for API: ${selectedApiEndpoint.name}`)
5691

5792
if (!destinationName) {
5893
const integrationsGlob = `${flags.directory}/*`
@@ -97,49 +132,57 @@ export default class GenerateTestPayload extends Command {
97132
this.error(`Failed to load destination: ${destinationName}`)
98133
}
99134

100-
const actions = Object.entries(destination.actions)
135+
this.spinner.succeed(`Successfully loaded destination: ${destinationName}`)
101136

102-
if (actions.length === 0) {
103-
this.warn('No actions found for this destination.')
104-
this.exit()
105-
}
137+
// If Execute Action API is selected, proceed with action selection
138+
if (selectedApiEndpoint.name === 'Execute Action') {
139+
const actions = Object.entries(destination.actions)
106140

107-
let actionToGenerate = flags.action
141+
if (actions.length === 0) {
142+
this.warn('No actions found for this destination.')
143+
this.exit()
144+
}
108145

109-
if (!actionToGenerate && actions.length > 1) {
110-
const { selectedAction } = await autoPrompt<{ selectedAction: string }>(flags, {
111-
type: 'select',
112-
name: 'selectedAction',
113-
message: 'Which action to generate test payload for?',
114-
choices: [
115-
{ title: 'All actions', value: 'all' },
116-
...actions.map(([slug, action]) => ({
117-
title: action.title || slug,
118-
value: slug
119-
}))
120-
]
121-
})
146+
let actionToGenerate = flags.action
147+
148+
if (!actionToGenerate && actions.length > 1) {
149+
const { selectedAction } = await autoPrompt<{ selectedAction: string }>(flags, {
150+
type: 'select',
151+
name: 'selectedAction',
152+
message: 'Which action to generate test payload for?',
153+
choices: [
154+
{ title: 'All actions', value: 'all' },
155+
...actions.map(([slug, action]) => ({
156+
title: action.title || slug,
157+
value: slug
158+
}))
159+
]
160+
})
122161

123-
actionToGenerate = selectedAction
124-
} else if (!actionToGenerate) {
125-
actionToGenerate = 'all'
126-
}
162+
actionToGenerate = selectedAction
163+
} else if (!actionToGenerate) {
164+
actionToGenerate = 'all'
165+
}
127166

128-
this.log(chalk.bold('\nTest Payload curl commands:'))
167+
this.log(chalk.bold('\nTest Payload curl commands:'))
168+
169+
if (actionToGenerate === 'all') {
170+
for (const [slug, action] of actions) {
171+
await this.generatePayloadForAction(destination, slug, action)
172+
}
173+
} else {
174+
const action = actions.find(([slug]) => slug === actionToGenerate)
175+
if (!action) {
176+
this.warn(`Action "${actionToGenerate}" not found. Exiting.`)
177+
this.exit(1)
178+
return
179+
}
129180

130-
if (actionToGenerate === 'all') {
131-
for (const [slug, action] of actions) {
132-
await this.generatePayloadForAction(destination, slug, action)
181+
await this.generatePayloadForAction(destination, action[0], action[1])
133182
}
134183
} else {
135-
const action = actions.find(([slug]) => slug === actionToGenerate)
136-
if (!action) {
137-
this.warn(`Action "${actionToGenerate}" not found. Exiting.`)
138-
this.exit(1)
139-
return
140-
}
141-
142-
await this.generatePayloadForAction(destination, action[0], action[1])
184+
// Generate payload for selected API endpoint
185+
await this.generatePayloadForApi(destination, selectedApiEndpoint)
143186
}
144187

145188
this.log(chalk.green(`\nDone generating test payloads! 🎉`))
@@ -221,6 +264,77 @@ export default class GenerateTestPayload extends Command {
221264
}
222265
}
223266

267+
/**
268+
* Generate payload for a specific API endpoint
269+
*/
270+
async generatePayloadForApi(destination: DestinationDefinition, apiEndpoint: ApiEndpoint) {
271+
this.spinner.start(`Generating test payload for API: ${apiEndpoint.name}`)
272+
273+
try {
274+
// Generate destination settings and auth
275+
const { settings, auth } = generateDestinationSettings(destination)
276+
277+
// Get audience settings if needed
278+
const audienceSettings = generateAudienceSettings(destination)
279+
const audienceSettingsValues =
280+
Object.keys(audienceSettings).length > 0 ? generateSampleFromSchema(audienceSettings || {}) : {}
281+
282+
// Start with the template payload from the API definition
283+
const request = { ...apiEndpoint.requestTemplate }
284+
285+
// Fill in settings and auth if the payload has those fields
286+
if ('settings' in request && !Object.keys(request.settings).length) {
287+
request.settings = settings
288+
}
289+
290+
if ('auth' in request && !Object.keys(request.auth || {}).length && auth) {
291+
request.auth = auth
292+
}
293+
294+
// Fill in audience settings if applicable
295+
if ('audienceSettings' in request && !Object.keys(request.audienceSettings || {}).length) {
296+
request.audienceSettings = audienceSettingsValues
297+
}
298+
299+
if ('payload' in request) {
300+
const baseEvent = generateSamplePayloadFromMapping({}, {})
301+
const isDeleteHandler = apiEndpoint.name === 'Invoke Delete Handler'
302+
request.payload = {
303+
...baseEvent,
304+
type: isDeleteHandler ? 'delete' : 'track',
305+
traits: {}
306+
}
307+
}
308+
309+
// Handle path parameters if needed
310+
const pathParams: Record<string, string> = {}
311+
if (apiEndpoint.pathParams) {
312+
// For now we'll use placeholders, but in the future we could prompt for these values
313+
for (const param of apiEndpoint.pathParams) {
314+
pathParams[param.key] = param.placeholder
315+
}
316+
}
317+
318+
// Format the path with any parameters
319+
const formattedPath = getFormattedPath(apiEndpoint, pathParams)
320+
321+
this.spinner.succeed(`Generated test payload for API: ${apiEndpoint.name}`)
322+
323+
// Print the curl command to the terminal
324+
this.log(chalk.cyan(`\n# Test payload for ${chalk.bold(destination.name)} - ${chalk.bold(apiEndpoint.name)}`))
325+
326+
if (apiEndpoint.method === 'GET') {
327+
this.log(chalk.yellow(`curl -X GET http://localhost:3000${formattedPath}`))
328+
} else {
329+
this.log(chalk.yellow(`curl -X ${apiEndpoint.method} http://localhost:3000${formattedPath} \\`))
330+
this.log(chalk.yellow(` -H "Content-Type: application/json" \\`))
331+
this.log(chalk.yellow(` -d '${JSON.stringify(request).replace(/'/g, "\\'")}'`))
332+
}
333+
} catch (error) {
334+
this.spinner.fail(`Failed to generate payload for API ${apiEndpoint.name}: ${(error as Error).message}`)
335+
}
336+
}
337+
224338
async catch(error: unknown) {
225339
if (this.spinner?.isSpinning) {
226340
this.spinner.fail()

0 commit comments

Comments
 (0)