@wavyx/pdcli 0.8.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +67 -0
- package/README.md +9 -0
- package/oclif.manifest.json +1960 -706
- package/package.json +4 -1
- package/src/base-command.js +22 -3
- package/src/commands/activity/list.js +48 -6
- package/src/commands/deal/history.js +73 -0
- package/src/commands/deal/list.js +41 -3
- package/src/commands/deal/product/add.js +69 -0
- package/src/commands/deal/product/list.js +56 -0
- package/src/commands/deal/product/remove.js +52 -0
- package/src/commands/deal/product/update.js +78 -0
- package/src/commands/funnel.js +7 -49
- package/src/commands/org/list.js +41 -3
- package/src/commands/person/list.js +41 -3
- package/src/commands/product/list.js +36 -3
- package/src/commands/user/find.js +50 -0
- package/src/commands/user/list.js +36 -0
- package/src/commands/webhook/create.js +8 -2
- package/src/lib/aliases.js +85 -5
- package/src/lib/changelog.js +85 -0
- package/src/lib/client.js +135 -117
package/src/commands/org/list.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Flags } from '@oclif/core'
|
|
2
2
|
import BaseCommand from '../../base-command.js'
|
|
3
3
|
import { collectPages } from '../../lib/pagination.js'
|
|
4
|
+
import { CliError } from '../../lib/errors.js'
|
|
4
5
|
|
|
5
6
|
const columns = {
|
|
6
7
|
id: { header: 'ID' },
|
|
@@ -20,21 +21,58 @@ export default class OrgListCommand extends BaseCommand {
|
|
|
20
21
|
static flags = {
|
|
21
22
|
...BaseCommand.baseFlags,
|
|
22
23
|
owner: Flags.integer({ description: 'Filter by owner (user) ID' }),
|
|
24
|
+
filter: Flags.integer({ description: 'Filter by saved filter ID' }),
|
|
25
|
+
ids: Flags.string({
|
|
26
|
+
description: 'Comma-separated IDs to fetch (max 100)',
|
|
27
|
+
// The API silently drops `ids` when filter_id is present — refuse
|
|
28
|
+
// the combination instead (matches deal bulk-update).
|
|
29
|
+
exclusive: ['filter'],
|
|
30
|
+
}),
|
|
31
|
+
'sort-by': Flags.string({
|
|
32
|
+
description: 'Sort field',
|
|
33
|
+
options: ['id', 'update_time', 'add_time'],
|
|
34
|
+
}),
|
|
35
|
+
'sort-direction': Flags.string({
|
|
36
|
+
description: 'Sort direction',
|
|
37
|
+
options: ['asc', 'desc'],
|
|
38
|
+
}),
|
|
39
|
+
'updated-since': Flags.string({
|
|
40
|
+
description:
|
|
41
|
+
'Only items updated at/after this RFC3339 time (no fractional seconds)',
|
|
42
|
+
}),
|
|
43
|
+
'updated-until': Flags.string({
|
|
44
|
+
description:
|
|
45
|
+
'Only items updated before this RFC3339 time (no fractional seconds)',
|
|
46
|
+
}),
|
|
23
47
|
}
|
|
24
48
|
|
|
25
49
|
async run() {
|
|
26
50
|
const { flags } = await this.parse(OrgListCommand)
|
|
27
|
-
const limit = flags.limit ??
|
|
51
|
+
const limit = flags.limit ?? 500
|
|
52
|
+
|
|
53
|
+
const idList = flags.ids
|
|
54
|
+
?.split(',')
|
|
55
|
+
.map((v) => v.trim())
|
|
56
|
+
.filter(Boolean)
|
|
57
|
+
if (idList && idList.length > 100) {
|
|
58
|
+
throw new CliError('--ids accepts at most 100 IDs', { exitCode: 64 })
|
|
59
|
+
}
|
|
28
60
|
|
|
29
61
|
const query = {
|
|
30
62
|
owner_id: flags.owner,
|
|
31
|
-
|
|
63
|
+
filter_id: flags.filter,
|
|
64
|
+
ids: idList?.join(','),
|
|
65
|
+
sort_by: flags['sort-by'],
|
|
66
|
+
sort_direction: flags['sort-direction'],
|
|
67
|
+
updated_since: flags['updated-since'],
|
|
68
|
+
updated_until: flags['updated-until'],
|
|
69
|
+
limit: Math.min(limit, 500),
|
|
32
70
|
}
|
|
33
71
|
|
|
34
72
|
const items = await collectPages(
|
|
35
73
|
this.apiClient.pageV2('/api/v2/organizations', query),
|
|
36
74
|
limit,
|
|
37
75
|
)
|
|
38
|
-
await this.outputResults(items, columns)
|
|
76
|
+
await this.outputResults(items, columns, { entity: 'org' })
|
|
39
77
|
}
|
|
40
78
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Flags } from '@oclif/core'
|
|
2
2
|
import BaseCommand from '../../base-command.js'
|
|
3
3
|
import { collectPages } from '../../lib/pagination.js'
|
|
4
|
+
import { CliError } from '../../lib/errors.js'
|
|
4
5
|
|
|
5
6
|
function primary(list) {
|
|
6
7
|
if (!Array.isArray(list) || list.length === 0) return ''
|
|
@@ -28,22 +29,59 @@ export default class PersonListCommand extends BaseCommand {
|
|
|
28
29
|
...BaseCommand.baseFlags,
|
|
29
30
|
owner: Flags.integer({ description: 'Filter by owner (user) ID' }),
|
|
30
31
|
org: Flags.integer({ description: 'Filter by organization ID' }),
|
|
32
|
+
filter: Flags.integer({ description: 'Filter by saved filter ID' }),
|
|
33
|
+
ids: Flags.string({
|
|
34
|
+
description: 'Comma-separated IDs to fetch (max 100)',
|
|
35
|
+
// The API silently drops `ids` when filter_id is present — refuse
|
|
36
|
+
// the combination instead (matches deal bulk-update).
|
|
37
|
+
exclusive: ['filter'],
|
|
38
|
+
}),
|
|
39
|
+
'sort-by': Flags.string({
|
|
40
|
+
description: 'Sort field',
|
|
41
|
+
options: ['id', 'update_time', 'add_time'],
|
|
42
|
+
}),
|
|
43
|
+
'sort-direction': Flags.string({
|
|
44
|
+
description: 'Sort direction',
|
|
45
|
+
options: ['asc', 'desc'],
|
|
46
|
+
}),
|
|
47
|
+
'updated-since': Flags.string({
|
|
48
|
+
description:
|
|
49
|
+
'Only items updated at/after this RFC3339 time (no fractional seconds)',
|
|
50
|
+
}),
|
|
51
|
+
'updated-until': Flags.string({
|
|
52
|
+
description:
|
|
53
|
+
'Only items updated before this RFC3339 time (no fractional seconds)',
|
|
54
|
+
}),
|
|
31
55
|
}
|
|
32
56
|
|
|
33
57
|
async run() {
|
|
34
58
|
const { flags } = await this.parse(PersonListCommand)
|
|
35
|
-
const limit = flags.limit ??
|
|
59
|
+
const limit = flags.limit ?? 500
|
|
60
|
+
|
|
61
|
+
const idList = flags.ids
|
|
62
|
+
?.split(',')
|
|
63
|
+
.map((v) => v.trim())
|
|
64
|
+
.filter(Boolean)
|
|
65
|
+
if (idList && idList.length > 100) {
|
|
66
|
+
throw new CliError('--ids accepts at most 100 IDs', { exitCode: 64 })
|
|
67
|
+
}
|
|
36
68
|
|
|
37
69
|
const query = {
|
|
38
70
|
owner_id: flags.owner,
|
|
39
71
|
org_id: flags.org,
|
|
40
|
-
|
|
72
|
+
filter_id: flags.filter,
|
|
73
|
+
ids: idList?.join(','),
|
|
74
|
+
sort_by: flags['sort-by'],
|
|
75
|
+
sort_direction: flags['sort-direction'],
|
|
76
|
+
updated_since: flags['updated-since'],
|
|
77
|
+
updated_until: flags['updated-until'],
|
|
78
|
+
limit: Math.min(limit, 500),
|
|
41
79
|
}
|
|
42
80
|
|
|
43
81
|
const items = await collectPages(
|
|
44
82
|
this.apiClient.pageV2('/api/v2/persons', query),
|
|
45
83
|
limit,
|
|
46
84
|
)
|
|
47
|
-
await this.outputResults(items, columns)
|
|
85
|
+
await this.outputResults(items, columns, { entity: 'person' })
|
|
48
86
|
}
|
|
49
87
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Flags } from '@oclif/core'
|
|
2
2
|
import BaseCommand from '../../base-command.js'
|
|
3
3
|
import { collectPages } from '../../lib/pagination.js'
|
|
4
|
+
import { CliError } from '../../lib/errors.js'
|
|
4
5
|
|
|
5
6
|
const columns = {
|
|
6
7
|
id: { header: 'ID' },
|
|
@@ -25,21 +26,53 @@ export default class ProductListCommand extends BaseCommand {
|
|
|
25
26
|
static flags = {
|
|
26
27
|
...BaseCommand.baseFlags,
|
|
27
28
|
owner: Flags.integer({ description: 'Filter by owner (user) ID' }),
|
|
29
|
+
filter: Flags.integer({ description: 'Filter by saved filter ID' }),
|
|
30
|
+
ids: Flags.string({
|
|
31
|
+
description: 'Comma-separated IDs to fetch (max 100)',
|
|
32
|
+
// The API silently drops `ids` when filter_id is present — refuse
|
|
33
|
+
// the combination instead (matches deal bulk-update).
|
|
34
|
+
exclusive: ['filter'],
|
|
35
|
+
}),
|
|
36
|
+
'sort-by': Flags.string({
|
|
37
|
+
description: 'Sort field',
|
|
38
|
+
options: ['id', 'name', 'add_time', 'update_time'],
|
|
39
|
+
}),
|
|
40
|
+
'sort-direction': Flags.string({
|
|
41
|
+
description: 'Sort direction',
|
|
42
|
+
options: ['asc', 'desc'],
|
|
43
|
+
}),
|
|
44
|
+
'updated-since': Flags.string({
|
|
45
|
+
description:
|
|
46
|
+
'Only items updated at/after this RFC3339 time (no fractional seconds)',
|
|
47
|
+
}),
|
|
28
48
|
}
|
|
29
49
|
|
|
30
50
|
async run() {
|
|
31
51
|
const { flags } = await this.parse(ProductListCommand)
|
|
32
|
-
const limit = flags.limit ??
|
|
52
|
+
const limit = flags.limit ?? 500
|
|
53
|
+
|
|
54
|
+
const idList = flags.ids
|
|
55
|
+
?.split(',')
|
|
56
|
+
.map((v) => v.trim())
|
|
57
|
+
.filter(Boolean)
|
|
58
|
+
if (idList && idList.length > 100) {
|
|
59
|
+
throw new CliError('--ids accepts at most 100 IDs', { exitCode: 64 })
|
|
60
|
+
}
|
|
33
61
|
|
|
34
62
|
const query = {
|
|
35
63
|
owner_id: flags.owner,
|
|
36
|
-
|
|
64
|
+
filter_id: flags.filter,
|
|
65
|
+
ids: idList?.join(','),
|
|
66
|
+
sort_by: flags['sort-by'],
|
|
67
|
+
sort_direction: flags['sort-direction'],
|
|
68
|
+
updated_since: flags['updated-since'],
|
|
69
|
+
limit: Math.min(limit, 500),
|
|
37
70
|
}
|
|
38
71
|
|
|
39
72
|
const items = await collectPages(
|
|
40
73
|
this.apiClient.pageV2('/api/v2/products', query),
|
|
41
74
|
limit,
|
|
42
75
|
)
|
|
43
|
-
await this.outputResults(items, columns)
|
|
76
|
+
await this.outputResults(items, columns, { entity: 'product' })
|
|
44
77
|
}
|
|
45
78
|
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { Args, Flags } from '@oclif/core'
|
|
2
|
+
import BaseCommand from '../../base-command.js'
|
|
3
|
+
|
|
4
|
+
const columns = {
|
|
5
|
+
id: { header: 'ID' },
|
|
6
|
+
name: { header: 'Name' },
|
|
7
|
+
email: { header: 'Email' },
|
|
8
|
+
active_flag: {
|
|
9
|
+
header: 'Active',
|
|
10
|
+
get: (row) => (row.active_flag ? 'yes' : 'no'),
|
|
11
|
+
},
|
|
12
|
+
is_admin: { header: 'Admin', get: (row) => (row.is_admin ? 'yes' : 'no') },
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default class UserFindCommand extends BaseCommand {
|
|
16
|
+
static description = 'Find users by name'
|
|
17
|
+
|
|
18
|
+
static examples = [
|
|
19
|
+
'<%= config.bin %> user find "jane"',
|
|
20
|
+
'<%= config.bin %> user find "jane@acme.com" --by-email --output json',
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
static args = {
|
|
24
|
+
term: Args.string({ required: true, description: 'Search term' }),
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
static flags = {
|
|
28
|
+
...BaseCommand.baseFlags,
|
|
29
|
+
'by-email': Flags.boolean({
|
|
30
|
+
description: 'Match the term against email addresses only',
|
|
31
|
+
default: false,
|
|
32
|
+
}),
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async run() {
|
|
36
|
+
const { args, flags } = await this.parse(UserFindCommand)
|
|
37
|
+
|
|
38
|
+
// Users API is v1-only; /users/find returns one unpaginated array.
|
|
39
|
+
const body = await this.apiClient.get('/api/v1/users/find', {
|
|
40
|
+
query: {
|
|
41
|
+
term: args.term,
|
|
42
|
+
search_by_email: flags['by-email'] ? 1 : undefined,
|
|
43
|
+
},
|
|
44
|
+
})
|
|
45
|
+
let users = body.data ?? []
|
|
46
|
+
if (flags.limit) users = users.slice(0, flags.limit)
|
|
47
|
+
|
|
48
|
+
await this.outputResults(users, columns)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import BaseCommand from '../../base-command.js'
|
|
2
|
+
|
|
3
|
+
const columns = {
|
|
4
|
+
id: { header: 'ID' },
|
|
5
|
+
name: { header: 'Name' },
|
|
6
|
+
email: { header: 'Email' },
|
|
7
|
+
active_flag: {
|
|
8
|
+
header: 'Active',
|
|
9
|
+
get: (row) => (row.active_flag ? 'yes' : 'no'),
|
|
10
|
+
},
|
|
11
|
+
is_admin: { header: 'Admin', get: (row) => (row.is_admin ? 'yes' : 'no') },
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export default class UserListCommand extends BaseCommand {
|
|
15
|
+
static description = 'List all users'
|
|
16
|
+
|
|
17
|
+
static examples = [
|
|
18
|
+
'<%= config.bin %> user list',
|
|
19
|
+
'<%= config.bin %> user list --output json',
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
static flags = {
|
|
23
|
+
...BaseCommand.baseFlags,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async run() {
|
|
27
|
+
const { flags } = await this.parse(UserListCommand)
|
|
28
|
+
|
|
29
|
+
// Users API is v1-only and returns every user in one unpaginated array.
|
|
30
|
+
const body = await this.apiClient.get('/api/v1/users')
|
|
31
|
+
let users = body.data ?? []
|
|
32
|
+
if (flags.limit) users = users.slice(0, flags.limit)
|
|
33
|
+
|
|
34
|
+
await this.outputResults(users, columns)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -27,15 +27,21 @@ export default class WebhookCreateCommand extends BaseCommand {
|
|
|
27
27
|
description: 'Event object to subscribe to',
|
|
28
28
|
options: [
|
|
29
29
|
'activity',
|
|
30
|
+
'board',
|
|
30
31
|
'deal',
|
|
32
|
+
'deal_installment',
|
|
33
|
+
'deal_product',
|
|
31
34
|
'lead',
|
|
32
35
|
'note',
|
|
33
36
|
'organization',
|
|
34
37
|
'person',
|
|
35
|
-
'
|
|
36
|
-
'user',
|
|
38
|
+
'phase',
|
|
37
39
|
'pipeline',
|
|
40
|
+
'product',
|
|
41
|
+
'project',
|
|
38
42
|
'stage',
|
|
43
|
+
'task',
|
|
44
|
+
'user',
|
|
39
45
|
'*',
|
|
40
46
|
],
|
|
41
47
|
}),
|
package/src/lib/aliases.js
CHANGED
|
@@ -1,4 +1,78 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
1
2
|
import { getConf } from './config.js'
|
|
3
|
+
import { CliError } from './errors.js'
|
|
4
|
+
|
|
5
|
+
const LOCK_STALE_MS = 5000
|
|
6
|
+
const LOCK_RETRIES = 8
|
|
7
|
+
const LOCK_RETRY_MS = 50
|
|
8
|
+
|
|
9
|
+
/** Synchronous sleep for the short lock-retry loop (no event-loop yield needed). */
|
|
10
|
+
function sleepSync(ms) {
|
|
11
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Release the lock. Never throws: the mutation already persisted, so a
|
|
16
|
+
* failed release must not convert success into a reported failure (a
|
|
17
|
+
* leftover dir goes stale in 5s and is reaped by the next writer).
|
|
18
|
+
*/
|
|
19
|
+
function releaseLock(lockDir) {
|
|
20
|
+
try {
|
|
21
|
+
fs.rmdirSync(lockDir)
|
|
22
|
+
} catch {
|
|
23
|
+
/* benign: stale-broken concurrently, or unremovable — reaped later */
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Advisory lock around alias mutations: a lock DIRECTORY next to the conf
|
|
29
|
+
* file (mkdir is atomic on every platform we support). Protects concurrent
|
|
30
|
+
* pdcli processes from clobbering each other's read-modify-write of the
|
|
31
|
+
* aliases object — advisory only; other writers aren't covered. A lock left
|
|
32
|
+
* behind by a crashed process goes stale after 5s and is broken.
|
|
33
|
+
* @param {() => void} fn the mutation to run while holding the lock
|
|
34
|
+
*/
|
|
35
|
+
function withAliasLock(fn) {
|
|
36
|
+
const lockDir = `${getConf().path}.aliases.lock`
|
|
37
|
+
|
|
38
|
+
for (let attempt = 0; attempt <= LOCK_RETRIES; attempt++) {
|
|
39
|
+
try {
|
|
40
|
+
fs.mkdirSync(lockDir)
|
|
41
|
+
try {
|
|
42
|
+
fn()
|
|
43
|
+
return
|
|
44
|
+
} finally {
|
|
45
|
+
releaseLock(lockDir)
|
|
46
|
+
}
|
|
47
|
+
} catch (err) {
|
|
48
|
+
if (err?.code !== 'EEXIST') {
|
|
49
|
+
// Not contention — the config dir itself is unusable. Name the real
|
|
50
|
+
// problem instead of leaking the lock implementation detail.
|
|
51
|
+
throw new CliError(
|
|
52
|
+
`Cannot update aliases: the config directory is not writable (${err?.code ?? err?.message})`,
|
|
53
|
+
{ exitCode: 78 },
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
// Held by someone else — break it if stale, otherwise wait and retry.
|
|
57
|
+
let stale
|
|
58
|
+
try {
|
|
59
|
+
stale = Date.now() - fs.statSync(lockDir).mtimeMs > LOCK_STALE_MS
|
|
60
|
+
} catch {
|
|
61
|
+
continue // lock vanished between mkdir and stat — retry immediately
|
|
62
|
+
}
|
|
63
|
+
if (stale) {
|
|
64
|
+
fs.rmdirSync(lockDir) // a real failure here must surface, not retry
|
|
65
|
+
continue
|
|
66
|
+
}
|
|
67
|
+
if (attempt < LOCK_RETRIES) sleepSync(LOCK_RETRY_MS)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
throw new CliError(
|
|
72
|
+
'another pdcli process is updating aliases — retry in a moment',
|
|
73
|
+
{ exitCode: 75 },
|
|
74
|
+
)
|
|
75
|
+
}
|
|
2
76
|
|
|
3
77
|
export function getAliases() {
|
|
4
78
|
return getConf().get('aliases') ?? {}
|
|
@@ -17,19 +91,25 @@ export function getAlias(name) {
|
|
|
17
91
|
* dotted-path write. conf splits `set('aliases.<name>', …)` on every '.', so a
|
|
18
92
|
* dotted-path write would corrupt any name containing a dot (store/read
|
|
19
93
|
* mismatch). Mutating the object and writing it whole keeps odd names flat.
|
|
94
|
+
* The read-modify-write runs under an advisory lock so concurrent pdcli
|
|
95
|
+
* processes can't clobber each other.
|
|
20
96
|
* @param {string} name
|
|
21
97
|
* @param {string} command
|
|
22
98
|
*/
|
|
23
99
|
export function setAlias(name, command) {
|
|
24
|
-
|
|
25
|
-
|
|
100
|
+
withAliasLock(() => {
|
|
101
|
+
const aliases = { ...getAliases(), [name]: command }
|
|
102
|
+
getConf().set('aliases', aliases)
|
|
103
|
+
})
|
|
26
104
|
}
|
|
27
105
|
|
|
28
106
|
/**
|
|
29
107
|
* @param {string} name
|
|
30
108
|
*/
|
|
31
109
|
export function unsetAlias(name) {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
110
|
+
withAliasLock(() => {
|
|
111
|
+
const aliases = { ...getAliases() }
|
|
112
|
+
delete aliases[name]
|
|
113
|
+
getConf().set('aliases', aliases)
|
|
114
|
+
})
|
|
35
115
|
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { collectPages } from './pagination.js'
|
|
2
|
+
import { ApiError } from './errors.js'
|
|
3
|
+
|
|
4
|
+
/** Token cost of one GET /deals/{id}/changelog request (rate-limit budget). */
|
|
5
|
+
export const CHANGELOG_COST = 20
|
|
6
|
+
/** Above this deal count, mining gets expensive — warn before proceeding. */
|
|
7
|
+
export const MINE_WARN_THRESHOLD = 100
|
|
8
|
+
/** Pipedrive caps list page sizes at 500; the changelog uses the same cap. */
|
|
9
|
+
const MAX_PAGE_LIMIT = 500
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Fetch a single deal's field-change history. The deal changelog lives on a v1
|
|
13
|
+
* path but pages with a flat v2-style cursor (additional_data.next_cursor), so
|
|
14
|
+
* the v2 pager works directly. Rows arrive newest-first (the API's native order)
|
|
15
|
+
* and carry { field_key, old_value, new_value, actor_user_id, time, ... }.
|
|
16
|
+
* @param {ReturnType<import('./client.js').createClient>} client
|
|
17
|
+
* @param {number} dealId
|
|
18
|
+
* @param {{ limit?: number }} [options]
|
|
19
|
+
* @returns {Promise<object[]>}
|
|
20
|
+
*/
|
|
21
|
+
export async function fetchChangelog(client, dealId, { limit } = {}) {
|
|
22
|
+
return collectPages(
|
|
23
|
+
client.pageV2(`/api/v1/deals/${dealId}/changelog`, {
|
|
24
|
+
limit: limit ?? MAX_PAGE_LIMIT,
|
|
25
|
+
}),
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Mine the changelog of many deals, one request each. Warns on stderr before
|
|
31
|
+
* mining a large set — each request costs tokens — then lets the client's rate
|
|
32
|
+
* limiter pace it. A single bad changelog request must not abort the whole mine:
|
|
33
|
+
* deals whose fetch throws an ApiError are skipped, counted, and reported once
|
|
34
|
+
* after mining completes. Non-ApiError failures (e.g. socket hangups) rethrow.
|
|
35
|
+
* @param {ReturnType<import('./client.js').createClient>} client
|
|
36
|
+
* @param {object[]} deals deals to mine (id + current stage_id needed per deal)
|
|
37
|
+
* @param {{ limit?: number, warnThreshold?: number, costPerRequest?: number }} [options]
|
|
38
|
+
* @returns {Promise<{ dealId: number, stageId: number, rows: object[] }[]>}
|
|
39
|
+
*/
|
|
40
|
+
export async function mineMany(
|
|
41
|
+
client,
|
|
42
|
+
deals,
|
|
43
|
+
{
|
|
44
|
+
limit,
|
|
45
|
+
warnThreshold = MINE_WARN_THRESHOLD,
|
|
46
|
+
costPerRequest = CHANGELOG_COST,
|
|
47
|
+
} = {},
|
|
48
|
+
) {
|
|
49
|
+
if (deals.length > warnThreshold) {
|
|
50
|
+
process.stderr.write(
|
|
51
|
+
`Mining stage history for ${deals.length} deals ` +
|
|
52
|
+
`(~${deals.length} requests, ${costPerRequest} tokens each); ` +
|
|
53
|
+
`rate limiting may slow this down.\n`,
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const transitionsByDeal = []
|
|
58
|
+
let skipped = 0
|
|
59
|
+
for (const deal of deals) {
|
|
60
|
+
try {
|
|
61
|
+
const rows = await fetchChangelog(client, deal.id, { limit })
|
|
62
|
+
transitionsByDeal.push({
|
|
63
|
+
dealId: deal.id,
|
|
64
|
+
stageId: deal.stage_id,
|
|
65
|
+
rows,
|
|
66
|
+
})
|
|
67
|
+
} catch (err) {
|
|
68
|
+
// One bad changelog request must not abort the whole mine: skip the
|
|
69
|
+
// deal, count it, and warn once after mining completes.
|
|
70
|
+
if (err instanceof ApiError) {
|
|
71
|
+
skipped++
|
|
72
|
+
continue
|
|
73
|
+
}
|
|
74
|
+
throw err
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (skipped > 0) {
|
|
79
|
+
process.stderr.write(
|
|
80
|
+
`skipped ${skipped} deal(s) whose changelog could not be fetched\n`,
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return transitionsByDeal
|
|
85
|
+
}
|