confluence-cli 1.16.0 → 1.18.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 +14 -0
- package/README.md +55 -2
- package/bin/confluence.js +308 -0
- package/lib/confluence-client.js +155 -0
- package/package.json +2 -1
- package/tests/confluence-client.test.js +323 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
# [1.18.0](https://github.com/pchuri/confluence-cli/compare/v1.17.0...v1.18.0) (2026-02-15)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* add content property commands (list, get, set, delete) ([#38](https://github.com/pchuri/confluence-cli/issues/38)) ([506515d](https://github.com/pchuri/confluence-cli/commit/506515dd0c271e5385ede3a49225d4b4707f225d)), closes [#37](https://github.com/pchuri/confluence-cli/issues/37)
|
|
7
|
+
|
|
8
|
+
# [1.17.0](https://github.com/pchuri/confluence-cli/compare/v1.16.0...v1.17.0) (2026-02-13)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* add attachment upload and delete commands ([#36](https://github.com/pchuri/confluence-cli/issues/36)) ([ed62bb4](https://github.com/pchuri/confluence-cli/commit/ed62bb45468566c128f066016615048e31ed1775))
|
|
14
|
+
|
|
1
15
|
# [1.16.0](https://github.com/pchuri/confluence-cli/compare/v1.15.1...v1.16.0) (2026-02-13)
|
|
2
16
|
|
|
3
17
|
|
package/README.md
CHANGED
|
@@ -11,7 +11,8 @@ A powerful command-line interface for Atlassian Confluence that allows you to re
|
|
|
11
11
|
- ✏️ **Create pages** - Create new pages with support for Markdown, HTML, or Storage format
|
|
12
12
|
- 📝 **Update pages** - Update existing page content and titles
|
|
13
13
|
- 🗑️ **Delete pages** - Delete (or move to trash) pages by ID or URL
|
|
14
|
-
- 📎 **Attachments** - List or
|
|
14
|
+
- 📎 **Attachments** - List, download, upload, or delete page attachments
|
|
15
|
+
- 🏷️ **Properties** - List, get, set, and delete content properties (key-value metadata)
|
|
15
16
|
- 💬 **Comments** - List, create, and delete page comments (footer or inline)
|
|
16
17
|
- 📦 **Export** - Save a page and its attachments to a local folder
|
|
17
18
|
- 🛠️ **Edit workflow** - Export page content for editing and re-import
|
|
@@ -160,6 +161,48 @@ confluence attachments 123456789 --pattern "*.png" --limit 5
|
|
|
160
161
|
confluence attachments 123456789 --pattern "*.png" --download --dest ./downloads
|
|
161
162
|
```
|
|
162
163
|
|
|
164
|
+
### Upload Attachments
|
|
165
|
+
```bash
|
|
166
|
+
# Upload a single attachment
|
|
167
|
+
confluence attachment-upload 123456789 --file ./report.pdf
|
|
168
|
+
|
|
169
|
+
# Upload multiple files with a comment
|
|
170
|
+
confluence attachment-upload 123456789 --file ./a.pdf --file ./b.png --comment "v2"
|
|
171
|
+
|
|
172
|
+
# Replace an existing attachment by filename
|
|
173
|
+
confluence attachment-upload 123456789 --file ./diagram.png --replace
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### Delete Attachments
|
|
177
|
+
```bash
|
|
178
|
+
# Delete an attachment by ID
|
|
179
|
+
confluence attachment-delete 123456789 998877
|
|
180
|
+
|
|
181
|
+
# Skip confirmation
|
|
182
|
+
confluence attachment-delete 123456789 998877 --yes
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### Content Properties
|
|
186
|
+
```bash
|
|
187
|
+
# List all properties on a page
|
|
188
|
+
confluence property-list 123456789
|
|
189
|
+
|
|
190
|
+
# Get a specific property
|
|
191
|
+
confluence property-get 123456789 my-key
|
|
192
|
+
|
|
193
|
+
# Set a property (creates or updates with auto-versioning)
|
|
194
|
+
confluence property-set 123456789 my-key --value '{"color":"#ff0000"}'
|
|
195
|
+
|
|
196
|
+
# Set a property from a JSON file
|
|
197
|
+
confluence property-set 123456789 my-key --file ./property.json
|
|
198
|
+
|
|
199
|
+
# Delete a property
|
|
200
|
+
confluence property-delete 123456789 my-key
|
|
201
|
+
|
|
202
|
+
# Skip confirmation on delete
|
|
203
|
+
confluence property-delete 123456789 my-key --yes
|
|
204
|
+
```
|
|
205
|
+
|
|
163
206
|
### Comments
|
|
164
207
|
```bash
|
|
165
208
|
# List all comments (footer + inline)
|
|
@@ -361,9 +404,15 @@ confluence stats
|
|
|
361
404
|
| `delete <pageId_or_url>` | Delete a page by ID or URL | `--yes` |
|
|
362
405
|
| `edit <pageId>` | Export page content for editing | `--output <file>` |
|
|
363
406
|
| `attachments <pageId_or_url>` | List or download attachments for a page | `--limit <number>`, `--pattern <glob>`, `--download`, `--dest <directory>` |
|
|
407
|
+
| `attachment-upload <pageId_or_url>` | Upload attachments to a page | `--file <path>`, `--comment <text>`, `--replace`, `--minor-edit` |
|
|
408
|
+
| `attachment-delete <pageId_or_url> <attachmentId>` | Delete an attachment from a page | `--yes` |
|
|
364
409
|
| `comments <pageId_or_url>` | List comments for a page | `--format <text\|markdown\|json>`, `--limit <number>`, `--start <number>`, `--location <inline\|footer\|resolved>`, `--depth <root\|all>`, `--all` |
|
|
365
410
|
| `comment <pageId_or_url>` | Create a comment on a page | `--content <string>`, `--file <path>`, `--format <storage\|html\|markdown>`, `--parent <commentId>`, `--location <inline\|footer>`, `--inline-selection <text>`, `--inline-original-selection <text>`, `--inline-marker-ref <ref>`, `--inline-properties <json>` |
|
|
366
411
|
| `comment-delete <commentId>` | Delete a comment by ID | `--yes` |
|
|
412
|
+
| `property-list <pageId_or_url>` | List all content properties for a page | `--format <text\|json>`, `--limit <number>`, `--start <number>`, `--all` |
|
|
413
|
+
| `property-get <pageId_or_url> <key>` | Get a content property by key | `--format <text\|json>` |
|
|
414
|
+
| `property-set <pageId_or_url> <key>` | Set a content property (create or update) | `--value <json>`, `--file <path>`, `--format <text\|json>` |
|
|
415
|
+
| `property-delete <pageId_or_url> <key>` | Delete a content property by key | `--yes` |
|
|
367
416
|
| `export <pageId_or_url>` | Export a page to a directory with its attachments | `--format <html\|text\|markdown>`, `--dest <directory>`, `--file <filename>`, `--attachments-dir <name>`, `--pattern <glob>`, `--referenced-only`, `--skip-attachments` |
|
|
368
417
|
| `stats` | View your usage statistics | |
|
|
369
418
|
|
|
@@ -394,6 +443,10 @@ confluence move 123456789 987654321
|
|
|
394
443
|
# Move and rename
|
|
395
444
|
confluence move 123456789 987654321 --title "New Title"
|
|
396
445
|
|
|
446
|
+
# Upload and delete an attachment
|
|
447
|
+
confluence attachment-upload 123456789 --file ./report.pdf
|
|
448
|
+
confluence attachment-delete 123456789 998877 --yes
|
|
449
|
+
|
|
397
450
|
# View usage statistics
|
|
398
451
|
confluence stats
|
|
399
452
|
```
|
|
@@ -437,7 +490,7 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file
|
|
|
437
490
|
- [ ] Bulk operations
|
|
438
491
|
- [ ] Export pages to different formats
|
|
439
492
|
- [ ] Integration with other Atlassian tools (Jira)
|
|
440
|
-
- [
|
|
493
|
+
- [x] Page attachments management (list, download, upload, delete)
|
|
441
494
|
- [x] Comments
|
|
442
495
|
- [ ] Reviews
|
|
443
496
|
|
package/bin/confluence.js
CHANGED
|
@@ -495,6 +495,314 @@ program
|
|
|
495
495
|
}
|
|
496
496
|
});
|
|
497
497
|
|
|
498
|
+
// Attachment upload command
|
|
499
|
+
program
|
|
500
|
+
.command('attachment-upload <pageId>')
|
|
501
|
+
.description('Upload one or more attachments to a page')
|
|
502
|
+
.option('-f, --file <file>', 'File to upload (repeatable)', (value, previous) => {
|
|
503
|
+
const files = Array.isArray(previous) ? previous : [];
|
|
504
|
+
files.push(value);
|
|
505
|
+
return files;
|
|
506
|
+
}, [])
|
|
507
|
+
.option('--comment <comment>', 'Comment for the attachment(s)')
|
|
508
|
+
.option('--replace', 'Replace an existing attachment with the same filename')
|
|
509
|
+
.option('--minor-edit', 'Mark the upload as a minor edit')
|
|
510
|
+
.action(async (pageId, options) => {
|
|
511
|
+
const analytics = new Analytics();
|
|
512
|
+
try {
|
|
513
|
+
const files = Array.isArray(options.file) ? options.file.filter(Boolean) : [];
|
|
514
|
+
if (files.length === 0) {
|
|
515
|
+
throw new Error('At least one --file option is required.');
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
const fs = require('fs');
|
|
519
|
+
const path = require('path');
|
|
520
|
+
const config = getConfig();
|
|
521
|
+
const client = new ConfluenceClient(config);
|
|
522
|
+
|
|
523
|
+
const resolvedFiles = files.map((filePath) => ({
|
|
524
|
+
original: filePath,
|
|
525
|
+
resolved: path.resolve(filePath)
|
|
526
|
+
}));
|
|
527
|
+
|
|
528
|
+
resolvedFiles.forEach((file) => {
|
|
529
|
+
if (!fs.existsSync(file.resolved)) {
|
|
530
|
+
throw new Error(`File not found: ${file.original}`);
|
|
531
|
+
}
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
let uploaded = 0;
|
|
535
|
+
for (const file of resolvedFiles) {
|
|
536
|
+
const result = await client.uploadAttachment(pageId, file.resolved, {
|
|
537
|
+
comment: options.comment,
|
|
538
|
+
replace: options.replace,
|
|
539
|
+
minorEdit: options.minorEdit === true ? true : undefined
|
|
540
|
+
});
|
|
541
|
+
const attachment = result.results[0];
|
|
542
|
+
if (attachment) {
|
|
543
|
+
console.log(`⬆️ ${chalk.green(attachment.title)} (ID: ${attachment.id}, Version: ${attachment.version})`);
|
|
544
|
+
} else {
|
|
545
|
+
console.log(`⬆️ ${chalk.green(path.basename(file.resolved))}`);
|
|
546
|
+
}
|
|
547
|
+
uploaded += 1;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
console.log(chalk.green(`Uploaded ${uploaded} attachment${uploaded === 1 ? '' : 's'} to page ${pageId}`));
|
|
551
|
+
analytics.track('attachment_upload', true);
|
|
552
|
+
} catch (error) {
|
|
553
|
+
analytics.track('attachment_upload', false);
|
|
554
|
+
console.error(chalk.red('Error:'), error.message);
|
|
555
|
+
process.exit(1);
|
|
556
|
+
}
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
// Attachment delete command
|
|
560
|
+
program
|
|
561
|
+
.command('attachment-delete <pageId> <attachmentId>')
|
|
562
|
+
.description('Delete an attachment by ID from a page')
|
|
563
|
+
.option('-y, --yes', 'Skip confirmation prompt')
|
|
564
|
+
.action(async (pageId, attachmentId, options) => {
|
|
565
|
+
const analytics = new Analytics();
|
|
566
|
+
try {
|
|
567
|
+
const config = getConfig();
|
|
568
|
+
const client = new ConfluenceClient(config);
|
|
569
|
+
|
|
570
|
+
if (!options.yes) {
|
|
571
|
+
const { confirmed } = await inquirer.prompt([
|
|
572
|
+
{
|
|
573
|
+
type: 'confirm',
|
|
574
|
+
name: 'confirmed',
|
|
575
|
+
default: false,
|
|
576
|
+
message: `Delete attachment ${attachmentId} from page ${pageId}?`
|
|
577
|
+
}
|
|
578
|
+
]);
|
|
579
|
+
|
|
580
|
+
if (!confirmed) {
|
|
581
|
+
console.log(chalk.yellow('Cancelled.'));
|
|
582
|
+
analytics.track('attachment_delete_cancel', true);
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
const result = await client.deleteAttachment(pageId, attachmentId);
|
|
588
|
+
|
|
589
|
+
console.log(chalk.green('✅ Attachment deleted successfully!'));
|
|
590
|
+
console.log(`ID: ${chalk.blue(result.id)}`);
|
|
591
|
+
console.log(`Page ID: ${chalk.blue(result.pageId)}`);
|
|
592
|
+
analytics.track('attachment_delete', true);
|
|
593
|
+
} catch (error) {
|
|
594
|
+
analytics.track('attachment_delete', false);
|
|
595
|
+
console.error(chalk.red('Error:'), error.message);
|
|
596
|
+
process.exit(1);
|
|
597
|
+
}
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
// Property list command
|
|
601
|
+
program
|
|
602
|
+
.command('property-list <pageId>')
|
|
603
|
+
.description('List all content properties for a page')
|
|
604
|
+
.option('-f, --format <format>', 'Output format (text, json)', 'text')
|
|
605
|
+
.option('-l, --limit <limit>', 'Maximum number of properties to fetch (default: 25)')
|
|
606
|
+
.option('--start <start>', 'Start index for results (default: 0)', '0')
|
|
607
|
+
.option('--all', 'Fetch all properties (ignores pagination)')
|
|
608
|
+
.action(async (pageId, options) => {
|
|
609
|
+
const analytics = new Analytics();
|
|
610
|
+
try {
|
|
611
|
+
const config = getConfig();
|
|
612
|
+
const client = new ConfluenceClient(config);
|
|
613
|
+
|
|
614
|
+
const format = (options.format || 'text').toLowerCase();
|
|
615
|
+
if (!['text', 'json'].includes(format)) {
|
|
616
|
+
throw new Error('Format must be one of: text, json');
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
const limit = options.limit ? parseInt(options.limit, 10) : null;
|
|
620
|
+
if (options.limit && (Number.isNaN(limit) || limit <= 0)) {
|
|
621
|
+
throw new Error('Limit must be a positive number.');
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
const start = options.start ? parseInt(options.start, 10) : 0;
|
|
625
|
+
if (options.start && (Number.isNaN(start) || start < 0)) {
|
|
626
|
+
throw new Error('Start must be a non-negative number.');
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
let properties = [];
|
|
630
|
+
let nextStart = null;
|
|
631
|
+
|
|
632
|
+
if (options.all) {
|
|
633
|
+
properties = await client.getAllProperties(pageId, {
|
|
634
|
+
maxResults: limit || null,
|
|
635
|
+
start
|
|
636
|
+
});
|
|
637
|
+
} else {
|
|
638
|
+
const response = await client.listProperties(pageId, {
|
|
639
|
+
limit: limit || undefined,
|
|
640
|
+
start
|
|
641
|
+
});
|
|
642
|
+
properties = response.results;
|
|
643
|
+
nextStart = response.nextStart;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
if (format === 'json') {
|
|
647
|
+
const output = { properties };
|
|
648
|
+
if (!options.all) {
|
|
649
|
+
output.nextStart = nextStart;
|
|
650
|
+
}
|
|
651
|
+
console.log(JSON.stringify(output, null, 2));
|
|
652
|
+
} else if (properties.length === 0) {
|
|
653
|
+
console.log(chalk.yellow('No properties found.'));
|
|
654
|
+
} else {
|
|
655
|
+
properties.forEach((prop, i) => {
|
|
656
|
+
const preview = JSON.stringify(prop.value);
|
|
657
|
+
const truncated = preview.length > 80 ? preview.slice(0, 77) + '...' : preview;
|
|
658
|
+
console.log(`${chalk.blue(i + 1 + '.')} ${chalk.green(prop.key)} (v${prop.version.number}): ${truncated}`);
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
if (!options.all && nextStart !== null && nextStart !== undefined) {
|
|
662
|
+
console.log(chalk.gray(`Next start: ${nextStart}`));
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
analytics.track('property_list', true);
|
|
666
|
+
} catch (error) {
|
|
667
|
+
analytics.track('property_list', false);
|
|
668
|
+
console.error(chalk.red('Error:'), error.message);
|
|
669
|
+
process.exit(1);
|
|
670
|
+
}
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
// Property get command
|
|
674
|
+
program
|
|
675
|
+
.command('property-get <pageId> <key>')
|
|
676
|
+
.description('Get a content property by key')
|
|
677
|
+
.option('-f, --format <format>', 'Output format (text, json)', 'text')
|
|
678
|
+
.action(async (pageId, key, options) => {
|
|
679
|
+
const analytics = new Analytics();
|
|
680
|
+
try {
|
|
681
|
+
const config = getConfig();
|
|
682
|
+
const client = new ConfluenceClient(config);
|
|
683
|
+
|
|
684
|
+
const format = (options.format || 'text').toLowerCase();
|
|
685
|
+
if (!['text', 'json'].includes(format)) {
|
|
686
|
+
throw new Error('Format must be one of: text, json');
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
const property = await client.getProperty(pageId, key);
|
|
690
|
+
|
|
691
|
+
if (format === 'json') {
|
|
692
|
+
console.log(JSON.stringify(property, null, 2));
|
|
693
|
+
} else {
|
|
694
|
+
console.log(`${chalk.green('Key:')} ${property.key}`);
|
|
695
|
+
console.log(`${chalk.green('Version:')} ${property.version.number}`);
|
|
696
|
+
console.log(`${chalk.green('Value:')}`);
|
|
697
|
+
console.log(JSON.stringify(property.value, null, 2));
|
|
698
|
+
}
|
|
699
|
+
analytics.track('property_get', true);
|
|
700
|
+
} catch (error) {
|
|
701
|
+
analytics.track('property_get', false);
|
|
702
|
+
console.error(chalk.red('Error:'), error.message);
|
|
703
|
+
process.exit(1);
|
|
704
|
+
}
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
// Property set command
|
|
708
|
+
program
|
|
709
|
+
.command('property-set <pageId> <key>')
|
|
710
|
+
.description('Set a content property (create or update)')
|
|
711
|
+
.option('-v, --value <json>', 'Property value as JSON')
|
|
712
|
+
.option('--file <file>', 'Read property value from a JSON file')
|
|
713
|
+
.option('-f, --format <format>', 'Output format (text, json)', 'text')
|
|
714
|
+
.action(async (pageId, key, options) => {
|
|
715
|
+
const analytics = new Analytics();
|
|
716
|
+
try {
|
|
717
|
+
const config = getConfig();
|
|
718
|
+
const client = new ConfluenceClient(config);
|
|
719
|
+
|
|
720
|
+
if (!options.value && !options.file) {
|
|
721
|
+
throw new Error('Provide a value with --value or --file.');
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
let value;
|
|
725
|
+
if (options.file) {
|
|
726
|
+
const fs = require('fs');
|
|
727
|
+
const raw = fs.readFileSync(options.file, 'utf-8');
|
|
728
|
+
try {
|
|
729
|
+
value = JSON.parse(raw);
|
|
730
|
+
} catch {
|
|
731
|
+
throw new Error(`Invalid JSON in file ${options.file}`);
|
|
732
|
+
}
|
|
733
|
+
} else {
|
|
734
|
+
try {
|
|
735
|
+
value = JSON.parse(options.value);
|
|
736
|
+
} catch {
|
|
737
|
+
throw new Error('Invalid JSON in --value');
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
const format = (options.format || 'text').toLowerCase();
|
|
742
|
+
if (!['text', 'json'].includes(format)) {
|
|
743
|
+
throw new Error('Format must be one of: text, json');
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
const result = await client.setProperty(pageId, key, value);
|
|
747
|
+
|
|
748
|
+
if (format === 'json') {
|
|
749
|
+
console.log(JSON.stringify(result, null, 2));
|
|
750
|
+
} else {
|
|
751
|
+
console.log(chalk.green('✅ Property set successfully!'));
|
|
752
|
+
console.log(`${chalk.green('Key:')} ${result.key}`);
|
|
753
|
+
console.log(`${chalk.green('Version:')} ${result.version.number}`);
|
|
754
|
+
console.log(`${chalk.green('Value:')}`);
|
|
755
|
+
console.log(JSON.stringify(result.value, null, 2));
|
|
756
|
+
}
|
|
757
|
+
analytics.track('property_set', true);
|
|
758
|
+
} catch (error) {
|
|
759
|
+
analytics.track('property_set', false);
|
|
760
|
+
console.error(chalk.red('Error:'), error.message);
|
|
761
|
+
process.exit(1);
|
|
762
|
+
}
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
// Property delete command
|
|
766
|
+
program
|
|
767
|
+
.command('property-delete <pageId> <key>')
|
|
768
|
+
.description('Delete a content property by key')
|
|
769
|
+
.option('-y, --yes', 'Skip confirmation prompt')
|
|
770
|
+
.action(async (pageId, key, options) => {
|
|
771
|
+
const analytics = new Analytics();
|
|
772
|
+
try {
|
|
773
|
+
const config = getConfig();
|
|
774
|
+
const client = new ConfluenceClient(config);
|
|
775
|
+
|
|
776
|
+
if (!options.yes) {
|
|
777
|
+
const { confirmed } = await inquirer.prompt([
|
|
778
|
+
{
|
|
779
|
+
type: 'confirm',
|
|
780
|
+
name: 'confirmed',
|
|
781
|
+
default: false,
|
|
782
|
+
message: `Delete property "${key}" from page ${pageId}?`
|
|
783
|
+
}
|
|
784
|
+
]);
|
|
785
|
+
|
|
786
|
+
if (!confirmed) {
|
|
787
|
+
console.log(chalk.yellow('Cancelled.'));
|
|
788
|
+
analytics.track('property_delete_cancel', true);
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
const result = await client.deleteProperty(pageId, key);
|
|
794
|
+
|
|
795
|
+
console.log(chalk.green('✅ Property deleted successfully!'));
|
|
796
|
+
console.log(`${chalk.green('Key:')} ${chalk.blue(result.key)}`);
|
|
797
|
+
console.log(`${chalk.green('Page ID:')} ${chalk.blue(result.pageId)}`);
|
|
798
|
+
analytics.track('property_delete', true);
|
|
799
|
+
} catch (error) {
|
|
800
|
+
analytics.track('property_delete', false);
|
|
801
|
+
console.error(chalk.red('Error:'), error.message);
|
|
802
|
+
process.exit(1);
|
|
803
|
+
}
|
|
804
|
+
});
|
|
805
|
+
|
|
498
806
|
// Comments command
|
|
499
807
|
program
|
|
500
808
|
.command('comments <pageId>')
|
package/lib/confluence-client.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
const axios = require('axios');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const FormData = require('form-data');
|
|
2
5
|
const { convert } = require('html-to-text');
|
|
3
6
|
const MarkdownIt = require('markdown-it');
|
|
4
7
|
|
|
@@ -808,6 +811,158 @@ class ConfluenceClient {
|
|
|
808
811
|
return downloadResponse.data;
|
|
809
812
|
}
|
|
810
813
|
|
|
814
|
+
/**
|
|
815
|
+
* Upload an attachment to a page
|
|
816
|
+
*/
|
|
817
|
+
async uploadAttachment(pageIdOrUrl, filePath, options = {}) {
|
|
818
|
+
if (!filePath || typeof filePath !== 'string') {
|
|
819
|
+
throw new Error('File path is required for attachment upload.');
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
const resolvedPath = path.resolve(filePath);
|
|
823
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
824
|
+
throw new Error(`File not found: ${filePath}`);
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
const pageId = await this.extractPageId(pageIdOrUrl);
|
|
828
|
+
const form = new FormData();
|
|
829
|
+
form.append('file', fs.createReadStream(resolvedPath), { filename: path.basename(resolvedPath) });
|
|
830
|
+
|
|
831
|
+
if (options.comment !== undefined && options.comment !== null) {
|
|
832
|
+
form.append('comment', options.comment, { contentType: 'text/plain; charset=utf-8' });
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
if (typeof options.minorEdit === 'boolean') {
|
|
836
|
+
form.append('minorEdit', options.minorEdit ? 'true' : 'false');
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
const method = options.replace ? 'put' : 'post';
|
|
840
|
+
const response = await this.client.request({
|
|
841
|
+
url: `/content/${pageId}/child/attachment`,
|
|
842
|
+
method,
|
|
843
|
+
headers: {
|
|
844
|
+
...form.getHeaders(),
|
|
845
|
+
'X-Atlassian-Token': 'nocheck'
|
|
846
|
+
},
|
|
847
|
+
data: form,
|
|
848
|
+
maxBodyLength: Infinity,
|
|
849
|
+
maxContentLength: Infinity
|
|
850
|
+
});
|
|
851
|
+
|
|
852
|
+
const results = Array.isArray(response.data?.results)
|
|
853
|
+
? response.data.results.map((item) => this.normalizeAttachment(item))
|
|
854
|
+
: [];
|
|
855
|
+
|
|
856
|
+
return {
|
|
857
|
+
results,
|
|
858
|
+
raw: response.data
|
|
859
|
+
};
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
/**
|
|
863
|
+
* Delete an attachment by ID
|
|
864
|
+
*/
|
|
865
|
+
async deleteAttachment(pageIdOrUrl, attachmentId) {
|
|
866
|
+
if (!attachmentId) {
|
|
867
|
+
throw new Error('Attachment ID is required.');
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
const pageId = await this.extractPageId(pageIdOrUrl);
|
|
871
|
+
await this.client.delete(`/content/${pageId}/child/attachment/${attachmentId}`);
|
|
872
|
+
return { id: String(attachmentId), pageId: String(pageId) };
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
/**
|
|
876
|
+
* List content properties for a page with pagination support
|
|
877
|
+
*/
|
|
878
|
+
async listProperties(pageIdOrUrl, options = {}) {
|
|
879
|
+
const pageId = await this.extractPageId(pageIdOrUrl);
|
|
880
|
+
const limit = this.parsePositiveInt(options.limit, 25);
|
|
881
|
+
const start = this.parsePositiveInt(options.start, 0);
|
|
882
|
+
const params = { limit, start };
|
|
883
|
+
|
|
884
|
+
const response = await this.client.get(`/content/${pageId}/property`, { params });
|
|
885
|
+
const results = Array.isArray(response.data.results) ? response.data.results : [];
|
|
886
|
+
|
|
887
|
+
return {
|
|
888
|
+
results,
|
|
889
|
+
nextStart: this.parseNextStart(response.data?._links?.next)
|
|
890
|
+
};
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
/**
|
|
894
|
+
* Fetch all content properties for a page, honoring an optional maxResults cap
|
|
895
|
+
*/
|
|
896
|
+
async getAllProperties(pageIdOrUrl, options = {}) {
|
|
897
|
+
const pageSize = this.parsePositiveInt(options.pageSize || options.limit, 25);
|
|
898
|
+
const maxResults = this.parsePositiveInt(options.maxResults, null);
|
|
899
|
+
let start = this.parsePositiveInt(options.start, 0);
|
|
900
|
+
const properties = [];
|
|
901
|
+
|
|
902
|
+
let hasNext = true;
|
|
903
|
+
while (hasNext) {
|
|
904
|
+
const page = await this.listProperties(pageIdOrUrl, {
|
|
905
|
+
limit: pageSize,
|
|
906
|
+
start
|
|
907
|
+
});
|
|
908
|
+
properties.push(...page.results);
|
|
909
|
+
|
|
910
|
+
if (maxResults && properties.length >= maxResults) {
|
|
911
|
+
return properties.slice(0, maxResults);
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
hasNext = page.nextStart !== null && page.nextStart !== undefined;
|
|
915
|
+
if (hasNext) {
|
|
916
|
+
start = page.nextStart;
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
return properties;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
/**
|
|
924
|
+
* Get a single content property by key
|
|
925
|
+
*/
|
|
926
|
+
async getProperty(pageIdOrUrl, key) {
|
|
927
|
+
const pageId = await this.extractPageId(pageIdOrUrl);
|
|
928
|
+
const response = await this.client.get(`/content/${pageId}/property/${encodeURIComponent(key)}`);
|
|
929
|
+
return response.data;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
/**
|
|
933
|
+
* Set (create or update) a content property
|
|
934
|
+
*/
|
|
935
|
+
async setProperty(pageIdOrUrl, key, value) {
|
|
936
|
+
const pageId = await this.extractPageId(pageIdOrUrl);
|
|
937
|
+
const encodedKey = encodeURIComponent(key);
|
|
938
|
+
|
|
939
|
+
let version = 1;
|
|
940
|
+
try {
|
|
941
|
+
const existing = await this.client.get(`/content/${pageId}/property/${encodedKey}`);
|
|
942
|
+
version = existing.data.version.number + 1;
|
|
943
|
+
} catch (err) {
|
|
944
|
+
if (!err.response || err.response.status !== 404) {
|
|
945
|
+
throw err;
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
const response = await this.client.put(`/content/${pageId}/property/${encodedKey}`, {
|
|
950
|
+
key,
|
|
951
|
+
value,
|
|
952
|
+
version: { number: version }
|
|
953
|
+
});
|
|
954
|
+
return response.data;
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
/**
|
|
958
|
+
* Delete a content property by key
|
|
959
|
+
*/
|
|
960
|
+
async deleteProperty(pageIdOrUrl, key) {
|
|
961
|
+
const pageId = await this.extractPageId(pageIdOrUrl);
|
|
962
|
+
await this.client.delete(`/content/${pageId}/property/${encodeURIComponent(key)}`);
|
|
963
|
+
return { pageId: String(pageId), key };
|
|
964
|
+
}
|
|
965
|
+
|
|
811
966
|
/**
|
|
812
967
|
* Convert markdown to Confluence storage format
|
|
813
968
|
*/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "confluence-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.18.0",
|
|
4
4
|
"description": "A command-line interface for Atlassian Confluence with page creation and editing capabilities",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
"axios": "^1.12.0",
|
|
27
27
|
"chalk": "^4.1.2",
|
|
28
28
|
"commander": "^11.1.0",
|
|
29
|
+
"form-data": "^4.0.5",
|
|
29
30
|
"html-to-text": "^9.0.5",
|
|
30
31
|
"inquirer": "^8.2.6",
|
|
31
32
|
"markdown-it": "^14.1.0",
|
|
@@ -1,6 +1,44 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const os = require('os');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const FormData = require('form-data');
|
|
1
5
|
const ConfluenceClient = require('../lib/confluence-client');
|
|
2
6
|
const MockAdapter = require('axios-mock-adapter');
|
|
3
7
|
|
|
8
|
+
const removeDirRecursive = (dir) => {
|
|
9
|
+
if (!dir) return;
|
|
10
|
+
try {
|
|
11
|
+
if (fs.rmSync) {
|
|
12
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
} catch (error) {
|
|
16
|
+
void error;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (!fs.existsSync(dir)) return;
|
|
20
|
+
|
|
21
|
+
fs.readdirSync(dir).forEach((entry) => {
|
|
22
|
+
const entryPath = path.join(dir, entry);
|
|
23
|
+
const stats = fs.lstatSync(entryPath);
|
|
24
|
+
if (stats.isDirectory()) {
|
|
25
|
+
removeDirRecursive(entryPath);
|
|
26
|
+
} else {
|
|
27
|
+
try {
|
|
28
|
+
fs.unlinkSync(entryPath);
|
|
29
|
+
} catch (error) {
|
|
30
|
+
void error;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
fs.rmdirSync(dir);
|
|
37
|
+
} catch (error) {
|
|
38
|
+
void error;
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
4
42
|
describe('ConfluenceClient', () => {
|
|
5
43
|
let client;
|
|
6
44
|
|
|
@@ -599,6 +637,8 @@ describe('ConfluenceClient', () => {
|
|
|
599
637
|
expect(typeof client.listAttachments).toBe('function');
|
|
600
638
|
expect(typeof client.getAllAttachments).toBe('function');
|
|
601
639
|
expect(typeof client.downloadAttachment).toBe('function');
|
|
640
|
+
expect(typeof client.uploadAttachment).toBe('function');
|
|
641
|
+
expect(typeof client.deleteAttachment).toBe('function');
|
|
602
642
|
});
|
|
603
643
|
|
|
604
644
|
test('matchesPattern should respect glob patterns', () => {
|
|
@@ -614,5 +654,288 @@ describe('ConfluenceClient', () => {
|
|
|
614
654
|
expect(client.parseNextStart('/rest/api/content/1/child/attachment?limit=50')).toBeNull();
|
|
615
655
|
expect(client.parseNextStart(null)).toBeNull();
|
|
616
656
|
});
|
|
657
|
+
|
|
658
|
+
test('uploadAttachment should send multipart request with Atlassian token header', async () => {
|
|
659
|
+
const mock = new MockAdapter(client.client);
|
|
660
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'confluence-cli-'));
|
|
661
|
+
const tempFile = path.join(tempDir, 'upload.txt');
|
|
662
|
+
fs.writeFileSync(tempFile, 'hello');
|
|
663
|
+
|
|
664
|
+
try {
|
|
665
|
+
mock.onPost('/content/123/child/attachment').reply((config) => {
|
|
666
|
+
expect(config.headers['X-Atlassian-Token']).toBe('nocheck');
|
|
667
|
+
const contentType = config.headers['content-type'] || config.headers['Content-Type'];
|
|
668
|
+
expect(contentType).toContain('multipart/form-data');
|
|
669
|
+
expect(config.data).toBeInstanceOf(FormData);
|
|
670
|
+
return [200, {
|
|
671
|
+
results: [{
|
|
672
|
+
id: '1',
|
|
673
|
+
title: 'upload.txt',
|
|
674
|
+
version: { number: 2 },
|
|
675
|
+
_links: { download: '/download' }
|
|
676
|
+
}]
|
|
677
|
+
}];
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
const response = await client.uploadAttachment('123', tempFile, { comment: 'note', minorEdit: true });
|
|
681
|
+
expect(response.results[0].title).toBe('upload.txt');
|
|
682
|
+
} finally {
|
|
683
|
+
mock.restore();
|
|
684
|
+
removeDirRecursive(tempDir);
|
|
685
|
+
}
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
test('uploadAttachment should use PUT when replace is true', async () => {
|
|
689
|
+
const mock = new MockAdapter(client.client);
|
|
690
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'confluence-cli-'));
|
|
691
|
+
const tempFile = path.join(tempDir, 'replace.txt');
|
|
692
|
+
fs.writeFileSync(tempFile, 'replace');
|
|
693
|
+
|
|
694
|
+
try {
|
|
695
|
+
mock.onPut('/content/456/child/attachment').reply(200, {
|
|
696
|
+
results: [{
|
|
697
|
+
id: '2',
|
|
698
|
+
title: 'replace.txt',
|
|
699
|
+
version: { number: 3 },
|
|
700
|
+
_links: { download: '/download' }
|
|
701
|
+
}]
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
const response = await client.uploadAttachment('456', tempFile, { replace: true });
|
|
705
|
+
expect(response.results[0].title).toBe('replace.txt');
|
|
706
|
+
} finally {
|
|
707
|
+
mock.restore();
|
|
708
|
+
removeDirRecursive(tempDir);
|
|
709
|
+
}
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
test('deleteAttachment should call delete endpoint', async () => {
|
|
713
|
+
const mock = new MockAdapter(client.client);
|
|
714
|
+
mock.onDelete('/content/123/child/attachment/999').reply(204);
|
|
715
|
+
|
|
716
|
+
await expect(client.deleteAttachment('123', '999')).resolves.toEqual({ id: '999', pageId: '123' });
|
|
717
|
+
|
|
718
|
+
mock.restore();
|
|
719
|
+
});
|
|
720
|
+
});
|
|
721
|
+
|
|
722
|
+
describe('content properties', () => {
|
|
723
|
+
test('should have required methods for property handling', () => {
|
|
724
|
+
expect(typeof client.listProperties).toBe('function');
|
|
725
|
+
expect(typeof client.getProperty).toBe('function');
|
|
726
|
+
expect(typeof client.setProperty).toBe('function');
|
|
727
|
+
expect(typeof client.deleteProperty).toBe('function');
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
test('listProperties should return results with pagination info', async () => {
|
|
731
|
+
const mock = new MockAdapter(client.client);
|
|
732
|
+
mock.onGet('/content/123/property').reply(200, {
|
|
733
|
+
results: [
|
|
734
|
+
{ key: 'color', value: { hex: '#ff0000' }, version: { number: 1 } },
|
|
735
|
+
{ key: 'status', value: 'active', version: { number: 3 } }
|
|
736
|
+
],
|
|
737
|
+
_links: { next: '/rest/api/content/123/property?start=2&limit=25' }
|
|
738
|
+
});
|
|
739
|
+
|
|
740
|
+
const response = await client.listProperties('123');
|
|
741
|
+
expect(response.results).toHaveLength(2);
|
|
742
|
+
expect(response.results[0].key).toBe('color');
|
|
743
|
+
expect(response.results[1].key).toBe('status');
|
|
744
|
+
expect(response.nextStart).toBe(2);
|
|
745
|
+
|
|
746
|
+
mock.restore();
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
test('listProperties should return empty results when no properties exist', async () => {
|
|
750
|
+
const mock = new MockAdapter(client.client);
|
|
751
|
+
mock.onGet('/content/456/property').reply(200, { results: [] });
|
|
752
|
+
|
|
753
|
+
const response = await client.listProperties('456');
|
|
754
|
+
expect(response.results).toEqual([]);
|
|
755
|
+
expect(response.nextStart).toBeNull();
|
|
756
|
+
|
|
757
|
+
mock.restore();
|
|
758
|
+
});
|
|
759
|
+
|
|
760
|
+
test('listProperties should resolve page URLs', async () => {
|
|
761
|
+
const mock = new MockAdapter(client.client);
|
|
762
|
+
mock.onGet('/content/789/property').reply(200, { results: [] });
|
|
763
|
+
|
|
764
|
+
const response = await client.listProperties('https://test.atlassian.net/wiki/viewpage.action?pageId=789');
|
|
765
|
+
expect(response.results).toEqual([]);
|
|
766
|
+
|
|
767
|
+
mock.restore();
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
test('listProperties should pass limit and start as query params', async () => {
|
|
771
|
+
const mock = new MockAdapter(client.client);
|
|
772
|
+
mock.onGet('/content/123/property').reply((config) => {
|
|
773
|
+
expect(config.params.limit).toBe(5);
|
|
774
|
+
expect(config.params.start).toBe(10);
|
|
775
|
+
return [200, { results: [] }];
|
|
776
|
+
});
|
|
777
|
+
|
|
778
|
+
await client.listProperties('123', { limit: 5, start: 10 });
|
|
779
|
+
|
|
780
|
+
mock.restore();
|
|
781
|
+
});
|
|
782
|
+
|
|
783
|
+
test('getAllProperties should accumulate results across pages', async () => {
|
|
784
|
+
const mock = new MockAdapter(client.client);
|
|
785
|
+
let callCount = 0;
|
|
786
|
+
mock.onGet('/content/123/property').reply((config) => {
|
|
787
|
+
callCount++;
|
|
788
|
+
if (callCount === 1) {
|
|
789
|
+
expect(config.params.start).toBe(0);
|
|
790
|
+
return [200, {
|
|
791
|
+
results: [{ key: 'a', value: 1, version: { number: 1 } }],
|
|
792
|
+
_links: { next: '/rest/api/content/123/property?start=1&limit=1' }
|
|
793
|
+
}];
|
|
794
|
+
}
|
|
795
|
+
expect(config.params.start).toBe(1);
|
|
796
|
+
return [200, {
|
|
797
|
+
results: [{ key: 'b', value: 2, version: { number: 1 } }]
|
|
798
|
+
}];
|
|
799
|
+
});
|
|
800
|
+
|
|
801
|
+
const results = await client.getAllProperties('123', { pageSize: 1 });
|
|
802
|
+
expect(results).toHaveLength(2);
|
|
803
|
+
expect(results[0].key).toBe('a');
|
|
804
|
+
expect(results[1].key).toBe('b');
|
|
805
|
+
|
|
806
|
+
mock.restore();
|
|
807
|
+
});
|
|
808
|
+
|
|
809
|
+
test('getProperty should return property data', async () => {
|
|
810
|
+
const mock = new MockAdapter(client.client);
|
|
811
|
+
mock.onGet('/content/123/property/color').reply(200, {
|
|
812
|
+
key: 'color',
|
|
813
|
+
value: { hex: '#ff0000' },
|
|
814
|
+
version: { number: 2 }
|
|
815
|
+
});
|
|
816
|
+
|
|
817
|
+
const result = await client.getProperty('123', 'color');
|
|
818
|
+
expect(result.key).toBe('color');
|
|
819
|
+
expect(result.value.hex).toBe('#ff0000');
|
|
820
|
+
|
|
821
|
+
mock.restore();
|
|
822
|
+
});
|
|
823
|
+
|
|
824
|
+
test('getProperty should throw on 404', async () => {
|
|
825
|
+
const mock = new MockAdapter(client.client);
|
|
826
|
+
mock.onGet('/content/123/property/missing').reply(404, { message: 'Not found' });
|
|
827
|
+
|
|
828
|
+
await expect(client.getProperty('123', 'missing')).rejects.toThrow();
|
|
829
|
+
|
|
830
|
+
mock.restore();
|
|
831
|
+
});
|
|
832
|
+
|
|
833
|
+
test('setProperty should create new property with version 1', async () => {
|
|
834
|
+
const mock = new MockAdapter(client.client);
|
|
835
|
+
mock.onGet('/content/123/property/newkey').reply(404);
|
|
836
|
+
mock.onPut('/content/123/property/newkey').reply((config) => {
|
|
837
|
+
const body = JSON.parse(config.data);
|
|
838
|
+
expect(body.version.number).toBe(1);
|
|
839
|
+
expect(body.key).toBe('newkey');
|
|
840
|
+
return [200, body];
|
|
841
|
+
});
|
|
842
|
+
|
|
843
|
+
const result = await client.setProperty('123', 'newkey', { data: true });
|
|
844
|
+
expect(result.version.number).toBe(1);
|
|
845
|
+
|
|
846
|
+
mock.restore();
|
|
847
|
+
});
|
|
848
|
+
|
|
849
|
+
test('setProperty should auto-increment version for existing property', async () => {
|
|
850
|
+
const mock = new MockAdapter(client.client);
|
|
851
|
+
mock.onGet('/content/123/property/existing').reply(200, {
|
|
852
|
+
key: 'existing',
|
|
853
|
+
value: 'old',
|
|
854
|
+
version: { number: 5 }
|
|
855
|
+
});
|
|
856
|
+
mock.onPut('/content/123/property/existing').reply((config) => {
|
|
857
|
+
const body = JSON.parse(config.data);
|
|
858
|
+
expect(body.version.number).toBe(6);
|
|
859
|
+
return [200, body];
|
|
860
|
+
});
|
|
861
|
+
|
|
862
|
+
const result = await client.setProperty('123', 'existing', 'new');
|
|
863
|
+
expect(result.version.number).toBe(6);
|
|
864
|
+
|
|
865
|
+
mock.restore();
|
|
866
|
+
});
|
|
867
|
+
|
|
868
|
+
test('setProperty should propagate non-404 errors', async () => {
|
|
869
|
+
const mock = new MockAdapter(client.client);
|
|
870
|
+
mock.onGet('/content/123/property/broken').reply(500);
|
|
871
|
+
|
|
872
|
+
await expect(client.setProperty('123', 'broken', 'val')).rejects.toThrow();
|
|
873
|
+
|
|
874
|
+
mock.restore();
|
|
875
|
+
});
|
|
876
|
+
|
|
877
|
+
test('deleteProperty should call delete endpoint', async () => {
|
|
878
|
+
const mock = new MockAdapter(client.client);
|
|
879
|
+
mock.onDelete('/content/123/property/color').reply(204);
|
|
880
|
+
|
|
881
|
+
const result = await client.deleteProperty('123', 'color');
|
|
882
|
+
expect(result).toEqual({ pageId: '123', key: 'color' });
|
|
883
|
+
|
|
884
|
+
mock.restore();
|
|
885
|
+
});
|
|
886
|
+
|
|
887
|
+
test('getProperty should URL-encode keys with reserved characters', async () => {
|
|
888
|
+
const mock = new MockAdapter(client.client);
|
|
889
|
+
mock.onGet('/content/123/property/my%20prop%2Fkey').reply(200, {
|
|
890
|
+
key: 'my prop/key',
|
|
891
|
+
value: { ok: true },
|
|
892
|
+
version: { number: 1 }
|
|
893
|
+
});
|
|
894
|
+
|
|
895
|
+
const result = await client.getProperty('123', 'my prop/key');
|
|
896
|
+
expect(result.key).toBe('my prop/key');
|
|
897
|
+
expect(result.value.ok).toBe(true);
|
|
898
|
+
|
|
899
|
+
mock.restore();
|
|
900
|
+
});
|
|
901
|
+
|
|
902
|
+
test('setProperty should URL-encode keys with reserved characters', async () => {
|
|
903
|
+
const mock = new MockAdapter(client.client);
|
|
904
|
+
mock.onGet('/content/123/property/my%20prop%2Fkey').reply(404);
|
|
905
|
+
mock.onPut('/content/123/property/my%20prop%2Fkey').reply((config) => {
|
|
906
|
+
const body = JSON.parse(config.data);
|
|
907
|
+
expect(body.key).toBe('my prop/key');
|
|
908
|
+
expect(body.version.number).toBe(1);
|
|
909
|
+
return [200, body];
|
|
910
|
+
});
|
|
911
|
+
|
|
912
|
+
const result = await client.setProperty('123', 'my prop/key', { test: true });
|
|
913
|
+
expect(result.key).toBe('my prop/key');
|
|
914
|
+
|
|
915
|
+
mock.restore();
|
|
916
|
+
});
|
|
917
|
+
|
|
918
|
+
test('deleteProperty should URL-encode keys with reserved characters', async () => {
|
|
919
|
+
const mock = new MockAdapter(client.client);
|
|
920
|
+
mock.onDelete('/content/123/property/my%20prop%2Fkey').reply(204);
|
|
921
|
+
|
|
922
|
+
const result = await client.deleteProperty('123', 'my prop/key');
|
|
923
|
+
expect(result).toEqual({ pageId: '123', key: 'my prop/key' });
|
|
924
|
+
|
|
925
|
+
mock.restore();
|
|
926
|
+
});
|
|
927
|
+
|
|
928
|
+
test('deleteProperty should resolve page URLs', async () => {
|
|
929
|
+
const mock = new MockAdapter(client.client);
|
|
930
|
+
mock.onDelete('/content/789/property/status').reply(204);
|
|
931
|
+
|
|
932
|
+
const result = await client.deleteProperty(
|
|
933
|
+
'https://test.atlassian.net/wiki/viewpage.action?pageId=789',
|
|
934
|
+
'status'
|
|
935
|
+
);
|
|
936
|
+
expect(result).toEqual({ pageId: '789', key: 'status' });
|
|
937
|
+
|
|
938
|
+
mock.restore();
|
|
939
|
+
});
|
|
617
940
|
});
|
|
618
941
|
});
|