@wavyx/pdcli 0.9.0 → 0.11.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 +73 -0
- package/README.md +15 -0
- package/oclif.manifest.json +7590 -1565
- package/package.json +32 -2
- package/src/commands/activity/list.js +47 -5
- package/src/commands/activity/type/list.js +36 -0
- package/src/commands/deal/convert.js +112 -0
- package/src/commands/deal/follower/add.js +31 -0
- package/src/commands/deal/follower/list.js +38 -0
- package/src/commands/deal/follower/remove.js +44 -0
- package/src/commands/deal/history.js +73 -0
- package/src/commands/deal/list.js +47 -6
- package/src/commands/deal/participant/add.js +31 -0
- package/src/commands/deal/participant/list.js +46 -0
- package/src/commands/deal/participant/remove.js +53 -0
- 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/deal/summary.js +69 -0
- package/src/commands/field/create.js +72 -0
- package/src/commands/field/delete.js +56 -0
- package/src/commands/field/get.js +10 -1
- package/src/commands/field/list.js +10 -1
- package/src/commands/field/option/add.js +49 -0
- package/src/commands/field/option/remove.js +72 -0
- package/src/commands/field/update.js +53 -0
- package/src/commands/file/delete.js +41 -0
- package/src/commands/file/update.js +39 -0
- package/src/commands/filter/delete.js +41 -0
- package/src/commands/funnel.js +7 -49
- package/src/commands/lead/convert.js +103 -0
- package/src/commands/note/comment/add.js +31 -0
- package/src/commands/note/comment/delete.js +51 -0
- package/src/commands/note/comment/list.js +43 -0
- package/src/commands/note/comment/update.js +37 -0
- package/src/commands/org/follower/add.js +31 -0
- package/src/commands/org/follower/list.js +41 -0
- package/src/commands/org/follower/remove.js +50 -0
- package/src/commands/org/list.js +40 -2
- package/src/commands/org/relationship/add.js +44 -0
- package/src/commands/org/relationship/list.js +52 -0
- package/src/commands/org/relationship/remove.js +46 -0
- package/src/commands/person/follower/add.js +31 -0
- package/src/commands/person/follower/list.js +38 -0
- package/src/commands/person/follower/remove.js +48 -0
- package/src/commands/person/list.js +40 -2
- package/src/commands/product/list.js +35 -2
- package/src/commands/project/list.js +10 -4
- package/src/commands/search.js +66 -9
- package/src/commands/task/create.js +44 -0
- package/src/commands/task/delete.js +41 -0
- package/src/commands/task/get.js +26 -0
- package/src/commands/task/list.js +60 -0
- package/src/commands/task/update.js +72 -0
- 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/changelog.js +85 -0
- package/src/lib/client.js +44 -0
- package/src/lib/fields.js +43 -12
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Args } from '@oclif/core'
|
|
2
|
+
import BaseCommand from '../../base-command.js'
|
|
3
|
+
import { outputRecord } from '../../lib/entity-view.js'
|
|
4
|
+
|
|
5
|
+
export default class TaskGetCommand extends BaseCommand {
|
|
6
|
+
static description = 'Get a task by ID'
|
|
7
|
+
|
|
8
|
+
static examples = [
|
|
9
|
+
'<%= config.bin %> task get 9',
|
|
10
|
+
'<%= config.bin %> task get 9 --output json',
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
static flags = {
|
|
14
|
+
...BaseCommand.baseFlags,
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
static args = {
|
|
18
|
+
id: Args.integer({ required: true, description: 'Task ID' }),
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async run() {
|
|
22
|
+
const { args } = await this.parse(TaskGetCommand)
|
|
23
|
+
const body = await this.apiClient.get(`/api/v2/tasks/${args.id}`)
|
|
24
|
+
await outputRecord(this, body.data)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { Flags } from '@oclif/core'
|
|
2
|
+
import BaseCommand from '../../base-command.js'
|
|
3
|
+
import { collectPages } from '../../lib/pagination.js'
|
|
4
|
+
|
|
5
|
+
const columns = {
|
|
6
|
+
id: { header: 'ID' },
|
|
7
|
+
title: { header: 'Title' },
|
|
8
|
+
project_id: { header: 'Project' },
|
|
9
|
+
assignee_id: {
|
|
10
|
+
header: 'Assignee',
|
|
11
|
+
// v2 returns assignee_ids (array); surface the first for the column.
|
|
12
|
+
get: (row) => row.assignee_ids?.[0] ?? '',
|
|
13
|
+
},
|
|
14
|
+
due_date: { header: 'Due' },
|
|
15
|
+
done: { header: 'Done', get: (row) => (row.is_done ? 'yes' : 'no') },
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export default class TaskListCommand extends BaseCommand {
|
|
19
|
+
static description = 'List tasks'
|
|
20
|
+
|
|
21
|
+
static examples = [
|
|
22
|
+
'<%= config.bin %> task list',
|
|
23
|
+
'<%= config.bin %> task list --project 3 --todo',
|
|
24
|
+
'<%= config.bin %> task list --assignee 7 --output json',
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
static flags = {
|
|
28
|
+
...BaseCommand.baseFlags,
|
|
29
|
+
project: Flags.integer({ description: 'Filter by project ID' }),
|
|
30
|
+
assignee: Flags.integer({ description: 'Filter by assignee (user) ID' }),
|
|
31
|
+
parent: Flags.integer({ description: 'Filter by parent task ID' }),
|
|
32
|
+
done: Flags.boolean({
|
|
33
|
+
description: 'Only completed tasks',
|
|
34
|
+
exclusive: ['todo'],
|
|
35
|
+
}),
|
|
36
|
+
todo: Flags.boolean({
|
|
37
|
+
description: 'Only open (not done) tasks',
|
|
38
|
+
exclusive: ['done'],
|
|
39
|
+
}),
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async run() {
|
|
43
|
+
const { flags } = await this.parse(TaskListCommand)
|
|
44
|
+
const limit = flags.limit ?? 500
|
|
45
|
+
|
|
46
|
+
const query = {
|
|
47
|
+
project_id: flags.project,
|
|
48
|
+
assignee_id: flags.assignee,
|
|
49
|
+
parent_task_id: flags.parent,
|
|
50
|
+
is_done: flags.done ? true : flags.todo ? false : undefined,
|
|
51
|
+
limit: Math.min(limit, 500),
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const items = await collectPages(
|
|
55
|
+
this.apiClient.pageV2('/api/v2/tasks', query),
|
|
56
|
+
limit,
|
|
57
|
+
)
|
|
58
|
+
await this.outputResults(items, columns)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { Args, Flags } from '@oclif/core'
|
|
2
|
+
import BaseCommand from '../../base-command.js'
|
|
3
|
+
import { buildWriteBody } from '../../lib/input.js'
|
|
4
|
+
import { outputRecord } from '../../lib/entity-view.js'
|
|
5
|
+
import { CliError } from '../../lib/errors.js'
|
|
6
|
+
|
|
7
|
+
export default class TaskUpdateCommand extends BaseCommand {
|
|
8
|
+
static description = 'Update a task (v2 PATCH — only provided fields change)'
|
|
9
|
+
|
|
10
|
+
static examples = [
|
|
11
|
+
'<%= config.bin %> task update 7 --title "Renamed"',
|
|
12
|
+
'<%= config.bin %> task update 7 --done',
|
|
13
|
+
'<%= config.bin %> task update 7 --assignee 9',
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
static args = {
|
|
17
|
+
id: Args.integer({ required: true, description: 'Task ID' }),
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
static flags = {
|
|
21
|
+
...BaseCommand.baseFlags,
|
|
22
|
+
title: Flags.string({ description: 'Task title' }),
|
|
23
|
+
project: Flags.integer({ description: 'Project ID' }),
|
|
24
|
+
description: Flags.string({ description: 'Task description' }),
|
|
25
|
+
assignee: Flags.integer({ description: 'Assignee (user) ID' }),
|
|
26
|
+
'due-date': Flags.string({ description: 'Due date (YYYY-MM-DD)' }),
|
|
27
|
+
parent: Flags.integer({ description: 'Parent task ID' }),
|
|
28
|
+
done: Flags.boolean({
|
|
29
|
+
description: 'Mark the task as done',
|
|
30
|
+
exclusive: ['undone'],
|
|
31
|
+
}),
|
|
32
|
+
undone: Flags.boolean({
|
|
33
|
+
description: 'Mark the task as not done',
|
|
34
|
+
exclusive: ['done'],
|
|
35
|
+
}),
|
|
36
|
+
body: Flags.string({ description: 'Raw JSON body to merge (flags win)' }),
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async run() {
|
|
40
|
+
const { args, flags } = await this.parse(TaskUpdateCommand)
|
|
41
|
+
|
|
42
|
+
// The v2 task body takes `done` as an integer enum (0 = not done, 1 = done).
|
|
43
|
+
let done
|
|
44
|
+
if (flags.done) done = 1
|
|
45
|
+
else if (flags.undone) done = 0
|
|
46
|
+
|
|
47
|
+
const body = buildWriteBody({
|
|
48
|
+
typed: {
|
|
49
|
+
title: flags.title,
|
|
50
|
+
project_id: flags.project,
|
|
51
|
+
description: flags.description,
|
|
52
|
+
assignee_id: flags.assignee,
|
|
53
|
+
due_date: flags['due-date'],
|
|
54
|
+
parent_task_id: flags.parent,
|
|
55
|
+
done,
|
|
56
|
+
},
|
|
57
|
+
rawBody: flags.body,
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
if (Object.keys(body).length === 0) {
|
|
61
|
+
throw new CliError(
|
|
62
|
+
'Nothing to update — pass at least one field flag or --body',
|
|
63
|
+
{ exitCode: 64 },
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const res = await this.apiClient.patch(`/api/v2/tasks/${args.id}`, {
|
|
68
|
+
body,
|
|
69
|
+
})
|
|
70
|
+
await outputRecord(this, res.data)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -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
|
}),
|
|
@@ -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
|
+
}
|
package/src/lib/client.js
CHANGED
|
@@ -177,6 +177,21 @@ export function createClient({
|
|
|
177
177
|
debug('%s %s → %d', method, path, res.status)
|
|
178
178
|
|
|
179
179
|
if (res.status === 429) {
|
|
180
|
+
// Daily-budget exhaustion has no useful reset window — backoff would
|
|
181
|
+
// stall until the daily reset. Fail fast with an actionable message.
|
|
182
|
+
// The live API reports the token budget as
|
|
183
|
+
// x-daily-ratelimit-token-remaining (verified on the sandbox);
|
|
184
|
+
// x-daily-requests-left is the older POST/PUT fair-use header.
|
|
185
|
+
const dailyRemaining =
|
|
186
|
+
res.headers.get('x-daily-ratelimit-token-remaining') ??
|
|
187
|
+
res.headers.get('x-daily-requests-left')
|
|
188
|
+
if (dailyRemaining === '0') {
|
|
189
|
+
const err = new RateLimitError(0)
|
|
190
|
+
err.message =
|
|
191
|
+
'Daily API token budget exhausted — resets at midnight server ' +
|
|
192
|
+
'time (UTC-based; may differ from your local timezone)'
|
|
193
|
+
throw err
|
|
194
|
+
}
|
|
180
195
|
const wait = Number(
|
|
181
196
|
res.headers.get('x-ratelimit-reset') ||
|
|
182
197
|
res.headers.get('retry-after') ||
|
|
@@ -189,6 +204,16 @@ export function createClient({
|
|
|
189
204
|
continue
|
|
190
205
|
}
|
|
191
206
|
|
|
207
|
+
// Surface the remaining daily budget under --verbose (DEBUG=pd:*).
|
|
208
|
+
const dailyLeft = res.headers.get('x-daily-ratelimit-token-remaining')
|
|
209
|
+
if (dailyLeft != null) {
|
|
210
|
+
debug(
|
|
211
|
+
'daily token budget: %s remaining of %s',
|
|
212
|
+
dailyLeft,
|
|
213
|
+
res.headers.get('x-daily-ratelimit-token-limit') ?? '?',
|
|
214
|
+
)
|
|
215
|
+
}
|
|
216
|
+
|
|
192
217
|
// OAuth access tokens expire (~1h) — refresh once and retry.
|
|
193
218
|
if (res.status === 401 && onRefresh && attempts === 1) {
|
|
194
219
|
debug('401, attempting OAuth token refresh')
|
|
@@ -282,6 +307,24 @@ export function createClient({
|
|
|
282
307
|
})
|
|
283
308
|
}
|
|
284
309
|
|
|
310
|
+
/**
|
|
311
|
+
* PUT application/x-www-form-urlencoded (v1 form endpoints, e.g.
|
|
312
|
+
* /api/v1/files/:id — JSON is not accepted there).
|
|
313
|
+
* @param {string} path
|
|
314
|
+
* @param {Record<string, unknown>} fields Null/undefined values are omitted.
|
|
315
|
+
*/
|
|
316
|
+
async function putForm(path, fields = {}) {
|
|
317
|
+
const params = new URLSearchParams()
|
|
318
|
+
for (const [k, v] of Object.entries(fields)) {
|
|
319
|
+
if (v != null) params.set(k, String(v))
|
|
320
|
+
}
|
|
321
|
+
return transport('PUT', lockedUrl(path), {
|
|
322
|
+
path,
|
|
323
|
+
makeBody: () => params.toString(),
|
|
324
|
+
extraHeaders: { 'content-type': 'application/x-www-form-urlencoded' },
|
|
325
|
+
})
|
|
326
|
+
}
|
|
327
|
+
|
|
285
328
|
return {
|
|
286
329
|
get: (path, opts) => request('GET', path, opts),
|
|
287
330
|
post: (path, opts) => request('POST', path, opts),
|
|
@@ -291,6 +334,7 @@ export function createClient({
|
|
|
291
334
|
download,
|
|
292
335
|
postMultipart,
|
|
293
336
|
postForm,
|
|
337
|
+
putForm,
|
|
294
338
|
pageV1,
|
|
295
339
|
pageV2,
|
|
296
340
|
}
|
package/src/lib/fields.js
CHANGED
|
@@ -13,19 +13,35 @@ const ENTITY_FIELDS = {
|
|
|
13
13
|
activity: 'activityFields',
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Entities whose fields live ONLY on v1 — offset-paginated and returning the
|
|
18
|
+
* legacy key/name shape that getFields normalizes to field_code/field_name.
|
|
19
|
+
*/
|
|
20
|
+
const V1_ENTITY_FIELDS = {
|
|
21
|
+
lead: 'leadFields',
|
|
22
|
+
note: 'noteFields',
|
|
23
|
+
}
|
|
24
|
+
|
|
16
25
|
/**
|
|
17
26
|
* @param {string} entity deal | person | org(anization) | product | activity
|
|
18
|
-
*
|
|
27
|
+
* | lead | note
|
|
28
|
+
* @returns {string} fields endpoint path (v2 for core entities, v1 for
|
|
29
|
+
* lead/note)
|
|
19
30
|
*/
|
|
20
31
|
export function entityToFieldsPath(entity) {
|
|
21
|
-
const
|
|
22
|
-
if (
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
32
|
+
const v2 = ENTITY_FIELDS[entity]
|
|
33
|
+
if (v2) return `/api/v2/${v2}`
|
|
34
|
+
|
|
35
|
+
const v1 = V1_ENTITY_FIELDS[entity]
|
|
36
|
+
if (v1) return `/api/v1/${v1}`
|
|
37
|
+
|
|
38
|
+
throw new CliError(
|
|
39
|
+
`Unknown entity "${entity}". Use one of: ${[
|
|
40
|
+
...Object.keys(ENTITY_FIELDS),
|
|
41
|
+
...Object.keys(V1_ENTITY_FIELDS),
|
|
42
|
+
].join(', ')}`,
|
|
43
|
+
{ exitCode: 64 },
|
|
44
|
+
)
|
|
29
45
|
}
|
|
30
46
|
|
|
31
47
|
/** @type {Map<string, object[]>} per-run field-definition cache */
|
|
@@ -35,9 +51,22 @@ export function clearFieldsCache() {
|
|
|
35
51
|
cache.clear()
|
|
36
52
|
}
|
|
37
53
|
|
|
54
|
+
/**
|
|
55
|
+
* Normalize a v1 field definition (key/name) to the v2 shape
|
|
56
|
+
* (field_code/field_name) so callers can treat both alike.
|
|
57
|
+
* @param {object} def
|
|
58
|
+
*/
|
|
59
|
+
function normalizeV1Field(def) {
|
|
60
|
+
const { key, name, ...rest } = def
|
|
61
|
+
return { ...rest, field_code: key, field_name: name }
|
|
62
|
+
}
|
|
63
|
+
|
|
38
64
|
/**
|
|
39
65
|
* Fetch (and memoize for this run) all field definitions for an entity.
|
|
40
|
-
*
|
|
66
|
+
* Core entities use the v2 cursor pager; lead/note use the v1 offset pager
|
|
67
|
+
* and are normalized to the v2 field_code/field_name shape.
|
|
68
|
+
* @param {{ pageV2: (path: string) => AsyncGenerator<object>,
|
|
69
|
+
* pageV1: (path: string) => AsyncGenerator<object> }} client
|
|
41
70
|
* @param {string} entity
|
|
42
71
|
* @returns {Promise<object[]>}
|
|
43
72
|
*/
|
|
@@ -45,10 +74,12 @@ export async function getFields(client, entity) {
|
|
|
45
74
|
const path = entityToFieldsPath(entity)
|
|
46
75
|
if (cache.has(path)) return cache.get(path)
|
|
47
76
|
|
|
77
|
+
const isV1 = path.startsWith('/api/v1/')
|
|
48
78
|
debug('fetching field definitions: %s', path)
|
|
49
79
|
const defs = []
|
|
50
|
-
|
|
51
|
-
|
|
80
|
+
const pager = isV1 ? client.pageV1(path) : client.pageV2(path)
|
|
81
|
+
for await (const def of pager) {
|
|
82
|
+
defs.push(isV1 ? normalizeV1Field(def) : def)
|
|
52
83
|
}
|
|
53
84
|
cache.set(path, defs)
|
|
54
85
|
return defs
|