diff --git a/docs/agent/asa-management.md b/docs/agent/asa-management.md index 0024e31..8c43df8 100644 --- a/docs/agent/asa-management.md +++ b/docs/agent/asa-management.md @@ -32,6 +32,7 @@ Every `list` and `get` command in this file returns metadata only, no metrics. E | `asa campaigns update ` | 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 ` | 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 diff --git a/package.json b/package.json index ddb578b..4605eaa 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "adapty", "description": "Adapty command line interface", - "version": "0.6.0", + "version": "0.7.0", "author": "Adapty team ", "bin": { "adapty": "./bin/run.js" diff --git a/src/commands/asa/campaigns/bulk-list.ts b/src/commands/asa/campaigns/bulk-list.ts new file mode 100644 index 0000000..c58637b --- /dev/null +++ b/src/commands/asa/campaigns/bulk-list.ts @@ -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 { + 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 { + const {flags} = await this.parse(AsaCampaignsBulkList) + const client = await createAsaClient(this.config) + const result = await client.get('/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[], this.log.bind(this), { + count: result.total, + page: flags.page, + pages, + }) + + return result + } +} diff --git a/src/lib/asa-flags.ts b/src/lib/asa-flags.ts index 0015725..f0e527d 100644 --- a/src/lib/asa-flags.ts +++ b/src/lib/asa-flags.ts @@ -35,7 +35,7 @@ export const byDaysFlag = { }), } -async function parseDate(input: string): Promise { +export async function parseDate(input: string): Promise { if (!DATE_REGEX.test(input)) throw new Error('Dates must be written as YYYY-MM-DD.') return input } diff --git a/src/lib/asa-schemas.ts b/src/lib/asa-schemas.ts index 0b61768..6d2d3f7 100644 --- a/src/lib/asa-schemas.ts +++ b/src/lib/asa-schemas.ts @@ -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 diff --git a/test/commands/asa-bulk.test.ts b/test/commands/asa-bulk.test.ts index 1748b31..c710929 100644 --- a/test/commands/asa-bulk.test.ts +++ b/test/commands/asa-bulk.test.ts @@ -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) + }) +}) })