@wavyx/pdcli 0.2.0 → 0.4.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 +32 -0
- package/README.md +30 -2
- package/oclif.manifest.json +5579 -686
- package/package.json +3 -1
- package/src/base-command.js +30 -2
- package/src/commands/api.js +6 -2
- package/src/commands/backup.js +53 -0
- package/src/commands/deal/bulk-update.js +131 -0
- package/src/commands/file/download.js +35 -0
- package/src/commands/file/get.js +26 -0
- package/src/commands/file/list.js +40 -0
- package/src/commands/file/upload.js +42 -0
- package/src/commands/filter/get.js +26 -0
- package/src/commands/filter/list.js +43 -0
- package/src/commands/goal/list.js +37 -0
- package/src/commands/lead/create.js +58 -0
- package/src/commands/lead/delete.js +39 -0
- package/src/commands/lead/get.js +26 -0
- package/src/commands/lead/list.js +50 -0
- package/src/commands/lead/update.js +71 -0
- package/src/commands/note/create.js +42 -0
- package/src/commands/note/delete.js +39 -0
- package/src/commands/note/get.js +26 -0
- package/src/commands/note/list.js +49 -0
- package/src/commands/note/update.js +45 -0
- package/src/commands/org/import.js +109 -0
- package/src/commands/person/import.js +118 -0
- package/src/commands/pipeline/get.js +26 -0
- package/src/commands/pipeline/list.js +37 -0
- package/src/commands/project/create.js +48 -0
- package/src/commands/project/delete.js +39 -0
- package/src/commands/project/get.js +26 -0
- package/src/commands/project/list.js +39 -0
- package/src/commands/project/update.js +63 -0
- package/src/commands/stage/get.js +26 -0
- package/src/commands/stage/list.js +41 -0
- package/src/commands/webhook/create.js +75 -0
- package/src/commands/webhook/delete.js +39 -0
- package/src/commands/webhook/list.js +33 -0
- package/src/lib/backup.js +122 -0
- package/src/lib/bulk.js +106 -0
- package/src/lib/client.js +67 -0
- package/src/lib/csv-parse.js +88 -0
- package/src/lib/entity-view.js +7 -2
- package/src/lib/import.js +49 -0
- package/src/lib/output/csv.js +26 -0
- package/src/lib/output/index.js +9 -1
- package/src/lib/output/yaml.js +9 -0
package/src/lib/client.js
CHANGED
|
@@ -195,12 +195,79 @@ export function createClient({
|
|
|
195
195
|
}
|
|
196
196
|
}
|
|
197
197
|
|
|
198
|
+
function lockedUrl(path) {
|
|
199
|
+
const url = new URL(path, baseOrigin)
|
|
200
|
+
if (url.origin !== baseOrigin) {
|
|
201
|
+
throw new CliError(
|
|
202
|
+
`Refusing to send request outside your Pipedrive company host ` +
|
|
203
|
+
`(${baseOrigin}): ${url.origin}`,
|
|
204
|
+
{ exitCode: 78 },
|
|
205
|
+
)
|
|
206
|
+
}
|
|
207
|
+
return url
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function authHeaders() {
|
|
211
|
+
return authMode === 'oauth'
|
|
212
|
+
? { authorization: `Bearer ${token}`, 'user-agent': userAgent }
|
|
213
|
+
: { 'x-api-token': token, 'user-agent': userAgent }
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Download a binary resource (e.g. /api/v1/files/:id/download).
|
|
218
|
+
* @param {string} path
|
|
219
|
+
* @returns {Promise<{buffer: ArrayBuffer, contentType: string | null}>}
|
|
220
|
+
*/
|
|
221
|
+
async function download(path) {
|
|
222
|
+
const url = lockedUrl(path)
|
|
223
|
+
const res = await fetch(url, {
|
|
224
|
+
headers: authHeaders(),
|
|
225
|
+
signal: AbortSignal.timeout(timeout),
|
|
226
|
+
})
|
|
227
|
+
debug('GET (binary) %s → %d', path, res.status)
|
|
228
|
+
if (!res.ok) {
|
|
229
|
+
throw ApiError.fromResponse(res.status, await res.text(), path)
|
|
230
|
+
}
|
|
231
|
+
return {
|
|
232
|
+
buffer: await res.arrayBuffer(),
|
|
233
|
+
contentType: res.headers.get('content-type'),
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* POST multipart/form-data (file uploads — v1 files API).
|
|
239
|
+
* @param {string} path
|
|
240
|
+
* @param {{ file: { name: string, data: Buffer | Uint8Array }, fields?: Record<string, unknown> }} options
|
|
241
|
+
*/
|
|
242
|
+
async function postMultipart(path, { file, fields = {} }) {
|
|
243
|
+
const url = lockedUrl(path)
|
|
244
|
+
const form = new FormData()
|
|
245
|
+
form.set('file', new Blob([file.data]), file.name)
|
|
246
|
+
for (const [k, v] of Object.entries(fields)) {
|
|
247
|
+
if (v != null) form.set(k, String(v))
|
|
248
|
+
}
|
|
249
|
+
const res = await fetch(url, {
|
|
250
|
+
method: 'POST',
|
|
251
|
+
headers: authHeaders(), // fetch sets the multipart boundary itself
|
|
252
|
+
body: form,
|
|
253
|
+
signal: AbortSignal.timeout(timeout),
|
|
254
|
+
})
|
|
255
|
+
debug('POST (multipart) %s → %d', path, res.status)
|
|
256
|
+
const text = await res.text()
|
|
257
|
+
if (!res.ok) {
|
|
258
|
+
throw ApiError.fromResponse(res.status, text, path)
|
|
259
|
+
}
|
|
260
|
+
return text ? JSON.parse(text) : null
|
|
261
|
+
}
|
|
262
|
+
|
|
198
263
|
return {
|
|
199
264
|
get: (path, opts) => request('GET', path, opts),
|
|
200
265
|
post: (path, opts) => request('POST', path, opts),
|
|
201
266
|
put: (path, opts) => request('PUT', path, opts),
|
|
202
267
|
patch: (path, opts) => request('PATCH', path, opts),
|
|
203
268
|
del: (path, opts) => request('DELETE', path, opts),
|
|
269
|
+
download,
|
|
270
|
+
postMultipart,
|
|
204
271
|
pageV1,
|
|
205
272
|
pageV2,
|
|
206
273
|
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { CliError } from './errors.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Minimal RFC 4180 CSV parser (quoted fields, escaped quotes, embedded
|
|
5
|
+
* commas/newlines, CRLF). First record is the header row.
|
|
6
|
+
* @param {string} text
|
|
7
|
+
* @returns {{ headers: string[], rows: string[][] }}
|
|
8
|
+
*/
|
|
9
|
+
export function parseCsv(text) {
|
|
10
|
+
const records = []
|
|
11
|
+
let record = []
|
|
12
|
+
let field = ''
|
|
13
|
+
let inQuotes = false
|
|
14
|
+
let i = 0
|
|
15
|
+
|
|
16
|
+
while (i < text.length) {
|
|
17
|
+
const char = text[i]
|
|
18
|
+
|
|
19
|
+
if (inQuotes) {
|
|
20
|
+
if (char === '"') {
|
|
21
|
+
if (text[i + 1] === '"') {
|
|
22
|
+
field += '"'
|
|
23
|
+
i += 2
|
|
24
|
+
continue
|
|
25
|
+
}
|
|
26
|
+
inQuotes = false
|
|
27
|
+
i++
|
|
28
|
+
continue
|
|
29
|
+
}
|
|
30
|
+
field += char
|
|
31
|
+
i++
|
|
32
|
+
continue
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (char === '"') {
|
|
36
|
+
inQuotes = true
|
|
37
|
+
i++
|
|
38
|
+
continue
|
|
39
|
+
}
|
|
40
|
+
if (char === ',') {
|
|
41
|
+
record.push(field)
|
|
42
|
+
field = ''
|
|
43
|
+
i++
|
|
44
|
+
continue
|
|
45
|
+
}
|
|
46
|
+
if (char === '\n' || char === '\r') {
|
|
47
|
+
if (char === '\r' && text[i + 1] === '\n') i++
|
|
48
|
+
record.push(field)
|
|
49
|
+
field = ''
|
|
50
|
+
if (record.length > 1 || record[0] !== '') records.push(record)
|
|
51
|
+
record = []
|
|
52
|
+
i++
|
|
53
|
+
continue
|
|
54
|
+
}
|
|
55
|
+
field += char
|
|
56
|
+
i++
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (inQuotes) {
|
|
60
|
+
throw new CliError('Unterminated quoted field in CSV', { exitCode: 65 })
|
|
61
|
+
}
|
|
62
|
+
if (field !== '' || record.length > 0) {
|
|
63
|
+
// A tail record only reaches here when non-empty (a bare trailing
|
|
64
|
+
// newline never starts a record), so push unconditionally.
|
|
65
|
+
record.push(field)
|
|
66
|
+
records.push(record)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (records.length === 0) {
|
|
70
|
+
throw new CliError('CSV file is empty', { exitCode: 65 })
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const [headers, ...rows] = records
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
headers,
|
|
77
|
+
rows: rows.map((row, index) => {
|
|
78
|
+
if (row.length > headers.length) {
|
|
79
|
+
throw new CliError(
|
|
80
|
+
`CSV row ${index + 2} has ${row.length} cells but the header has ${headers.length}`,
|
|
81
|
+
{ exitCode: 65 },
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
while (row.length < headers.length) row.push('')
|
|
85
|
+
return row
|
|
86
|
+
}),
|
|
87
|
+
}
|
|
88
|
+
}
|
package/src/lib/entity-view.js
CHANGED
|
@@ -6,11 +6,16 @@ import { flattenRecord } from './output/record.js'
|
|
|
6
6
|
* field/value table with custom-field hash keys and option IDs resolved.
|
|
7
7
|
* @param {import('../base-command.js').default} cmd
|
|
8
8
|
* @param {object} record
|
|
9
|
-
* @param {string} entity deal | person | org | activity | product
|
|
9
|
+
* @param {string} [entity] deal | person | org | activity | product — omit
|
|
10
|
+
* for entities without resolvable custom fields (notes, files, webhooks, …)
|
|
10
11
|
*/
|
|
11
12
|
export async function outputRecord(cmd, record, entity) {
|
|
12
13
|
if (cmd.resolveFormat() === 'table') {
|
|
13
|
-
if (
|
|
14
|
+
if (
|
|
15
|
+
entity &&
|
|
16
|
+
record.custom_fields &&
|
|
17
|
+
Object.keys(record.custom_fields).length
|
|
18
|
+
) {
|
|
14
19
|
const defs = await getFields(cmd.apiClient, entity)
|
|
15
20
|
record = makeResolver(defs).resolveCustomFields(record)
|
|
16
21
|
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { buildWriteBody } from './input.js'
|
|
2
|
+
import { CliError } from './errors.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Turn parsed CSV rows into write bodies. Special columns (matched
|
|
6
|
+
* case-insensitively) build typed values; every other header resolves
|
|
7
|
+
* through the entity's field definitions — names, hash keys, and option
|
|
8
|
+
* labels included. Empty cells are skipped.
|
|
9
|
+
* @param {object} options
|
|
10
|
+
* @param {string[]} options.headers
|
|
11
|
+
* @param {string[][]} options.rows
|
|
12
|
+
* @param {Record<string, (typed: object, value: string) => void>} [options.specialColumns]
|
|
13
|
+
* @param {object[]} [options.defs] field definitions for non-special headers
|
|
14
|
+
* @returns {object[]} one request body per row
|
|
15
|
+
*/
|
|
16
|
+
export function prepareImportBodies({
|
|
17
|
+
headers,
|
|
18
|
+
rows,
|
|
19
|
+
specialColumns = {},
|
|
20
|
+
defs,
|
|
21
|
+
}) {
|
|
22
|
+
const specials = Object.fromEntries(
|
|
23
|
+
Object.entries(specialColumns).map(([k, v]) => [k.toLowerCase(), v]),
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
return rows.map((row, index) => {
|
|
27
|
+
const typed = {}
|
|
28
|
+
const fields = []
|
|
29
|
+
|
|
30
|
+
headers.forEach((header, i) => {
|
|
31
|
+
const value = row[i]
|
|
32
|
+
if (value === '') return
|
|
33
|
+
const special = specials[header.toLowerCase()]
|
|
34
|
+
if (special) {
|
|
35
|
+
special(typed, value)
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
fields.push(`${header}=${value}`)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
return buildWriteBody({ typed, fields, defs })
|
|
43
|
+
} catch (err) {
|
|
44
|
+
throw new CliError(`CSV row ${index + 2}: ${err.message}`, {
|
|
45
|
+
exitCode: 65,
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
})
|
|
49
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {object[]} data
|
|
3
|
+
* @param {Record<string, import('./table.js').Column>} columns
|
|
4
|
+
* @returns {string}
|
|
5
|
+
*/
|
|
6
|
+
export function formatCsv(data, columns) {
|
|
7
|
+
if (!data || data.length === 0) return ''
|
|
8
|
+
const entries = Object.entries(columns)
|
|
9
|
+
const header = entries.map(([, col]) => col.header).join(',')
|
|
10
|
+
const rows = data.map((row) =>
|
|
11
|
+
entries
|
|
12
|
+
.map(([key, col]) => {
|
|
13
|
+
const val = col.get ? col.get(row) : row[col.key ?? key]
|
|
14
|
+
return csvEscape(val != null ? String(val) : '')
|
|
15
|
+
})
|
|
16
|
+
.join(','),
|
|
17
|
+
)
|
|
18
|
+
return [header, ...rows].join('\n')
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function csvEscape(val) {
|
|
22
|
+
if (val.includes(',') || val.includes('"') || val.includes('\n')) {
|
|
23
|
+
return '"' + val.replace(/"/g, '""') + '"'
|
|
24
|
+
}
|
|
25
|
+
return val
|
|
26
|
+
}
|
package/src/lib/output/index.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { formatTable } from './table.js'
|
|
2
2
|
import { formatJson } from './json.js'
|
|
3
|
+
import { formatYaml } from './yaml.js'
|
|
4
|
+
import { formatCsv } from './csv.js'
|
|
3
5
|
|
|
4
6
|
/**
|
|
5
7
|
* @param {object | object[]} data
|
|
6
8
|
* @param {Record<string, import('./table.js').Column>} columns
|
|
7
|
-
* @param {'table' | 'json'} format
|
|
9
|
+
* @param {'table' | 'json' | 'yaml' | 'csv'} format
|
|
8
10
|
* @param {import('@oclif/core').Command} cmd
|
|
9
11
|
*/
|
|
10
12
|
export function formatOutput(data, columns, format, cmd) {
|
|
@@ -14,6 +16,12 @@ export function formatOutput(data, columns, format, cmd) {
|
|
|
14
16
|
case 'json':
|
|
15
17
|
cmd.log(formatJson(data))
|
|
16
18
|
break
|
|
19
|
+
case 'yaml':
|
|
20
|
+
cmd.log(formatYaml(data))
|
|
21
|
+
break
|
|
22
|
+
case 'csv':
|
|
23
|
+
cmd.log(formatCsv(items, columns))
|
|
24
|
+
break
|
|
17
25
|
case 'table':
|
|
18
26
|
default:
|
|
19
27
|
cmd.log(formatTable(items, columns))
|