Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/agent/asa-management.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ Every `list` and `get` command in this file returns metadata only, no metrics. E
| `asa campaigns update <id>` | at least one of `--name`, `--status`, `--country`, `--daily-budget`, `--budget`, `--target-cpa`, `--bidding-strategy` | |
| `asa campaigns bulk-create` | exactly one of `--file` (JSON structure, `-` for stdin) / `--from-file` (Apple Ads template, `.xlsx` or keywords `.csv`); `--org-id` required with `--from-file`; optional `--preview`, `--no-wait`, `--poll-interval` (default `5`), `--timeout` (default `900`) | Creates a whole structure — campaigns → ad groups → keywords/negative keywords/ads — as one queued operation. `--org-id` is the exception to this file's UUID rule: it takes the **numeric** `org_id` from `asa orgs list` (Apple's `campaign_group_id`), not the `internal_id` UUID that `campaigns create --org` takes. `--from-file` converts the template server-side first (its own budget — see [Request budgets](#request-budgets)); with `--preview` the command prints the converted request and creates nothing. By default it polls until the operation finishes (`success`/`partial`/`failed`, per-object failures listed); `--no-wait` prints the `operation_id` and returns — follow up with `bulk-status`. |
| `asa campaigns bulk-status <operation-id>` | positional operation id, printed by `bulk-create` | Progress of one bulk operation: status, applied/failed counts, and the per-object log with each failure's reason. |
| `asa campaigns bulk-list` | optional `--status` (`pending`/`running`/`success`/`partial`/`failed`, repeatable), `--app` (UUID), `--created-from`/`--created-to` (YYYY-MM-DD) | This company's bulk operations, newest first — one row per operation with its verdict and timestamps, no per-object detail. Use it to find an `operation_id` you lost or to check what ran recently, then drill in with `bulk-status`. Cheap catalog read. |

## Ad groups

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "adapty",
"description": "Adapty command line interface",
"version": "0.6.0",
"version": "0.7.0",
"author": "Adapty team <support@adapty.io>",
"bin": {
"adapty": "./bin/run.js"
Expand Down
61 changes: 61 additions & 0 deletions src/commands/asa/campaigns/bulk-list.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import {Command, Flags} from '@oclif/core'

import type {AsaBulkOperationListDTO} from '../../../lib/asa-schemas.js'

import {createAsaClient} from '../../../lib/asa-client.js'
import {asaPaginationFlags, parseDate} from '../../../lib/asa-flags.js'
import {isValidUuid, paginationParams} from '../../../lib/flags.js'
import {printList} from '../../../lib/output.js'

async function parseAppId(input: string): Promise<string> {
if (!isValidUuid(input)) throw new Error('Invalid app ID format. Run `adapty asa apps list` to find your app ID.')
return input
}

export default class AsaCampaignsBulkList extends Command {
static description = "This company's bulk operations, newest first; inspect one with bulk-status"
static enableJsonFlag = true
static examples = [
'<%= config.bin %> asa campaigns bulk-list',
'<%= config.bin %> asa campaigns bulk-list --status partial --status failed',
'<%= config.bin %> asa campaigns bulk-list --created-from 2026-08-01 --created-to 2026-08-20',
]
static flags = {
...asaPaginationFlags,
app: Flags.string({description: 'Keep only operations of this app (UUID)', parse: parseAppId}),
'created-from': Flags.string({
description: 'Keep only operations created on or after this date (YYYY-MM-DD)',
parse: parseDate,
}),
'created-to': Flags.string({
description: 'Keep only operations created on or before this date (YYYY-MM-DD)',
parse: parseDate,
}),
status: Flags.string({
description: 'Keep only operations in this state; repeatable',
multiple: true,
options: ['failed', 'partial', 'pending', 'running', 'success'],
}),
}

async run(): Promise<AsaBulkOperationListDTO> {
const {flags} = await this.parse(AsaCampaignsBulkList)
const client = await createAsaClient(this.config)
const result = await client.get<AsaBulkOperationListDTO>('/bulk-operations', {
...paginationParams(flags),
app_id: flags.app,
created_from: flags['created-from'],
created_to: flags['created-to'],
status: flags.status,
})

const pages = Math.max(1, Math.ceil(result.total / result.limit))
printList(result.items as unknown as Record<string, unknown>[], this.log.bind(this), {
count: result.total,
page: flags.page,
pages,
})

return result
}
}
2 changes: 1 addition & 1 deletion src/lib/asa-flags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export const byDaysFlag = {
}),
}

async function parseDate(input: string): Promise<string> {
export async function parseDate(input: string): Promise<string> {
if (!DATE_REGEX.test(input)) throw new Error('Dates must be written as YYYY-MM-DD.')
return input
}
Expand Down
17 changes: 17 additions & 0 deletions src/lib/asa-schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,23 @@ export interface AsaBulkOperationStateDTO {
status: AsaBulkOperationStatus
}

export interface AsaBulkOperationListItemDTO {
app_id: string
created_at: string
error: null | string
finished_at: null | string
operation_id: string
started_at: null | string
status: AsaBulkOperationStatus
}

export interface AsaBulkOperationListDTO {
items: AsaBulkOperationListItemDTO[]
limit: number
offset: number
total: number
}

export interface AsaTemplateIssueDTO {
column: null | string
message: string
Expand Down
67 changes: 67 additions & 0 deletions test/commands/asa-bulk.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,4 +204,71 @@ describe('asa bulk', () => {
expect(fetchStub.callCount).to.equal(0)
})
})

describe('campaigns bulk-list', () => {
let fetchStub: sinon.SinonStub

beforeEach(() => {
process.env.ADAPTY_TOKEN = 'dev_live_test'
delete process.env.ADAPTY_ASA_API_URL
})

afterEach(() => {
restoreFetch(fetchStub)
delete process.env.ADAPTY_TOKEN
})

it('lists operations passing every filter as a query param', async () => {
fetchStub = mockFetch([
{
items: [
{
app_id: TEST_RESOURCE_ID,
created_at: '2026-08-13T10:00:00Z',
error: null,
finished_at: '2026-08-13T10:01:00Z',
operation_id: TEST_RESOURCE_ID,
started_at: '2026-08-13T10:00:01Z',
status: 'partial',
},
],
limit: 50,
offset: 50,
total: 51,
},
])
const {stdout} = await runCommand(
`asa campaigns bulk-list --status partial --status failed --app ${TEST_RESOURCE_ID} ` +
'--created-from 2026-08-01 --created-to 2026-08-13 --page 2 --page-size 50',
)

assertFetch({
base: ASA_API_BASE,
callIndex: 0,
method: 'GET',
path: '/bulk-operations/',
query: {
app_id: TEST_RESOURCE_ID,
created_from: '2026-08-01',
created_to: '2026-08-13',
'page[number]': '2',
'page[size]': '50',
},
stub: fetchStub,
})
const url = fetchStub.getCall(0).args[0] as string
expect(new URLSearchParams(url.split('?')[1]).getAll('status')).to.deep.equal(['partial', 'failed'])
expect(stdout).to.contain(`Operation ID: ${TEST_RESOURCE_ID}`)
expect(stdout).to.contain('Status: partial')
expect(stdout).to.contain('Page 2 of 2 (51 total)')
})

it('rejects a malformed app id before calling the API', async () => {
fetchStub = mockFetch([])
const {error} = await runCommand('asa campaigns bulk-list --app not-a-uuid')

expect(error?.message).to.contain('Invalid app ID')
expect(fetchStub.callCount).to.equal(0)
})
})
})
Loading