@jordancoin/notioncli 1.2.0 → 1.3.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/README.md +111 -423
- package/TECHNICAL.md +125 -0
- package/bin/notion.js +925 -814
- package/lib/config.js +68 -0
- package/lib/filters.js +213 -0
- package/lib/format.js +225 -0
- package/lib/helpers.js +9 -334
- package/lib/markdown.js +347 -0
- package/package.json +1 -1
- package/skill/SKILL.md +44 -10
- package/skill/marketplace.json +2 -2
- package/test/unit.test.js +392 -0
- package/test/debug-parent.js +0 -32
- package/test/live-relations-test.js +0 -309
package/skill/SKILL.md
CHANGED
|
@@ -61,6 +61,9 @@ notion alias list # Show configured aliases with IDs
|
|
|
61
61
|
```bash
|
|
62
62
|
notion query tasks # Query all rows
|
|
63
63
|
notion query tasks --filter Status=Active # Filter by property
|
|
64
|
+
notion query tasks --filter "Count>10" # Greater than
|
|
65
|
+
notion query tasks --filter "Count>=5" --filter "Status!=Draft" # AND filters
|
|
66
|
+
notion query tasks --filter "Due>=today" # Relative dates
|
|
64
67
|
notion query tasks --sort Date:desc # Sort results
|
|
65
68
|
notion query tasks --filter Status=Active --limit 10 # Combine options
|
|
66
69
|
notion query tasks --output csv # CSV output
|
|
@@ -69,6 +72,10 @@ notion query tasks --output json # JSON output
|
|
|
69
72
|
notion --json query tasks # JSON (shorthand)
|
|
70
73
|
```
|
|
71
74
|
|
|
75
|
+
**Filter operators:** `=` (equals/contains), `!=` (not equal), `>`, `<`, `>=`, `<=`
|
|
76
|
+
**Relative dates:** `today`, `yesterday`, `tomorrow`, `last_week`, `next_week`
|
|
77
|
+
**Multiple filters** combine as AND.
|
|
78
|
+
|
|
72
79
|
**Output formats:**
|
|
73
80
|
- `table` — formatted ASCII table (default)
|
|
74
81
|
- `csv` — header row + comma-separated values
|
|
@@ -78,24 +85,29 @@ notion --json query tasks # JSON (shorthand)
|
|
|
78
85
|
### Creating Pages
|
|
79
86
|
|
|
80
87
|
```bash
|
|
88
|
+
# Use property names as flags (recommended — reads like English)
|
|
89
|
+
notion add tasks --name "Buy groceries" --status "Todo"
|
|
90
|
+
notion add projects --name "New Feature" --priority "High" --due "2026-03-01"
|
|
91
|
+
|
|
92
|
+
# Or use --prop for programmatic use
|
|
81
93
|
notion add tasks --prop "Name=Buy groceries" --prop "Status=Todo"
|
|
82
|
-
|
|
94
|
+
|
|
95
|
+
# Create with markdown body
|
|
96
|
+
notion add tasks --name "Sprint Notes" --from notes.md
|
|
83
97
|
```
|
|
84
98
|
|
|
85
|
-
|
|
99
|
+
Property names from your database schema become CLI flags automatically. Multi-word properties use kebab-case: `--due-date` → "Due Date".
|
|
86
100
|
|
|
87
101
|
### Updating Pages
|
|
88
102
|
|
|
89
|
-
By page ID:
|
|
90
103
|
```bash
|
|
91
|
-
|
|
92
|
-
notion update
|
|
93
|
-
|
|
104
|
+
# Dynamic flags (recommended)
|
|
105
|
+
notion update tasks --filter "Name=Ship feature" --status "Done"
|
|
106
|
+
notion update workouts --filter "Name=LEGS #5" --notes "Great session"
|
|
94
107
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
notion update tasks --filter "Name=Ship feature" --prop "Status=Done"
|
|
98
|
-
notion update workouts --filter "Name=LEGS #5" --prop "Notes=Great session"
|
|
108
|
+
# Or --prop syntax
|
|
109
|
+
notion update <page-id> --prop "Status=Done"
|
|
110
|
+
notion update tasks --filter "Name=Ship feature" --prop "Status=Done" --prop "Notes=Updated"
|
|
99
111
|
```
|
|
100
112
|
|
|
101
113
|
### Reading Pages & Content
|
|
@@ -205,6 +217,28 @@ Supports images, PDFs, text files, documents. MIME types auto-detected from exte
|
|
|
205
217
|
notion search "quarterly report" # Search across all pages and databases
|
|
206
218
|
```
|
|
207
219
|
|
|
220
|
+
### Import
|
|
221
|
+
|
|
222
|
+
```bash
|
|
223
|
+
# CSV → database pages (headers become property names)
|
|
224
|
+
notion import data.csv --to tasks
|
|
225
|
+
|
|
226
|
+
# JSON → database pages (array of objects)
|
|
227
|
+
notion import data.json --to tasks
|
|
228
|
+
|
|
229
|
+
# Markdown → page with blocks
|
|
230
|
+
notion import notes.md --to tasks --title "Sprint Notes"
|
|
231
|
+
notion import doc.md --parent <page-id> --title "My Document"
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### Export
|
|
235
|
+
|
|
236
|
+
```bash
|
|
237
|
+
# Export page content as markdown
|
|
238
|
+
notion export tasks --filter "Name=Sprint Notes"
|
|
239
|
+
notion export <page-id>
|
|
240
|
+
```
|
|
241
|
+
|
|
208
242
|
### JSON Output
|
|
209
243
|
|
|
210
244
|
Add `--json` before any command to get the raw Notion API response:
|
package/skill/marketplace.json
CHANGED
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
"id": "notioncli",
|
|
3
3
|
"name": "notioncli — Notion CLI",
|
|
4
4
|
"description": "Query databases, manage pages, explore relations, and edit blocks in your Notion workspace from the terminal.",
|
|
5
|
-
"longDescription": "A powerful CLI for the Notion API built for both humans and AI agents. Auto-discovers databases and creates aliases so you never type UUIDs.
|
|
5
|
+
"longDescription": "A powerful CLI for the Notion API built for both humans and AI agents. Auto-discovers databases and creates aliases so you never type UUIDs. Full CRUD: query, add, update, delete, get, search. Relations & rollups, blocks CRUD, multi-workspace profiles, database management (db-create, db-update), file uploads, templates, page moves. Correctly routes property changes through dataSources.update() for the 2025 Notion API. Zero-UUID workflow via alias + filter on every command.",
|
|
6
6
|
"category": "Productivity",
|
|
7
7
|
"price": 0,
|
|
8
8
|
"icon": "📝",
|
|
9
9
|
"author": "JordanCoin",
|
|
10
10
|
"authorUrl": "https://github.com/JordanCoin",
|
|
11
11
|
"repository": "https://github.com/JordanCoin/notioncli",
|
|
12
|
-
"version": "1.
|
|
12
|
+
"version": "1.3.0",
|
|
13
13
|
"tags": ["notion", "cli", "api", "database", "productivity", "workspace", "automation", "ai-agent"],
|
|
14
14
|
"requirements": ["Node.js >= 18", "Notion API key (free)"],
|
|
15
15
|
"optional": []
|
package/test/unit.test.js
CHANGED
|
@@ -10,7 +10,16 @@ const {
|
|
|
10
10
|
pagesToRows,
|
|
11
11
|
formatCsv,
|
|
12
12
|
formatYaml,
|
|
13
|
+
parseFilterOperator,
|
|
14
|
+
resolveRelativeDate,
|
|
13
15
|
buildFilterFromSchema,
|
|
16
|
+
buildCompoundFilter,
|
|
17
|
+
markdownToBlocks,
|
|
18
|
+
parseInlineFormatting,
|
|
19
|
+
blocksToMarkdown,
|
|
20
|
+
parseCsv,
|
|
21
|
+
kebabToProperty,
|
|
22
|
+
extractDynamicProps,
|
|
14
23
|
UUID_REGEX,
|
|
15
24
|
} = require('../lib/helpers');
|
|
16
25
|
|
|
@@ -699,4 +708,387 @@ describe('buildFilterFromSchema', () => {
|
|
|
699
708
|
title: { contains: 'a=b=c' },
|
|
700
709
|
});
|
|
701
710
|
});
|
|
711
|
+
|
|
712
|
+
// ─── Rich filter operators ─────────────────────────────────────────────────
|
|
713
|
+
|
|
714
|
+
it('builds number greater than filter', () => {
|
|
715
|
+
const result = buildFilterFromSchema(schema, 'Count>100');
|
|
716
|
+
assert.deepEqual(result.filter, {
|
|
717
|
+
property: 'Count',
|
|
718
|
+
number: { greater_than: 100 },
|
|
719
|
+
});
|
|
720
|
+
});
|
|
721
|
+
|
|
722
|
+
it('builds number less than filter', () => {
|
|
723
|
+
const result = buildFilterFromSchema(schema, 'Count<50');
|
|
724
|
+
assert.deepEqual(result.filter, {
|
|
725
|
+
property: 'Count',
|
|
726
|
+
number: { less_than: 50 },
|
|
727
|
+
});
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
it('builds number greater than or equal filter', () => {
|
|
731
|
+
const result = buildFilterFromSchema(schema, 'Count>=10');
|
|
732
|
+
assert.deepEqual(result.filter, {
|
|
733
|
+
property: 'Count',
|
|
734
|
+
number: { greater_than_or_equal_to: 10 },
|
|
735
|
+
});
|
|
736
|
+
});
|
|
737
|
+
|
|
738
|
+
it('builds number less than or equal filter', () => {
|
|
739
|
+
const result = buildFilterFromSchema(schema, 'Count<=99');
|
|
740
|
+
assert.deepEqual(result.filter, {
|
|
741
|
+
property: 'Count',
|
|
742
|
+
number: { less_than_or_equal_to: 99 },
|
|
743
|
+
});
|
|
744
|
+
});
|
|
745
|
+
|
|
746
|
+
it('builds not-equal filter for select', () => {
|
|
747
|
+
const result = buildFilterFromSchema(schema, 'Status!=Draft');
|
|
748
|
+
assert.deepEqual(result.filter, {
|
|
749
|
+
property: 'Status',
|
|
750
|
+
select: { does_not_equal: 'Draft' },
|
|
751
|
+
});
|
|
752
|
+
});
|
|
753
|
+
|
|
754
|
+
it('builds not-equal filter for title', () => {
|
|
755
|
+
const result = buildFilterFromSchema(schema, 'Name!=Untitled');
|
|
756
|
+
assert.deepEqual(result.filter, {
|
|
757
|
+
property: 'Name',
|
|
758
|
+
title: { does_not_contain: 'Untitled' },
|
|
759
|
+
});
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
it('builds date after filter', () => {
|
|
763
|
+
const result = buildFilterFromSchema(schema, 'Due>2024-01-01');
|
|
764
|
+
assert.deepEqual(result.filter, {
|
|
765
|
+
property: 'Due',
|
|
766
|
+
date: { after: '2024-01-01' },
|
|
767
|
+
});
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
it('builds date before filter', () => {
|
|
771
|
+
const result = buildFilterFromSchema(schema, 'Due<2024-12-31');
|
|
772
|
+
assert.deepEqual(result.filter, {
|
|
773
|
+
property: 'Due',
|
|
774
|
+
date: { before: '2024-12-31' },
|
|
775
|
+
});
|
|
776
|
+
});
|
|
777
|
+
|
|
778
|
+
it('builds date on_or_after filter', () => {
|
|
779
|
+
const result = buildFilterFromSchema(schema, 'Due>=2024-06-01');
|
|
780
|
+
assert.deepEqual(result.filter, {
|
|
781
|
+
property: 'Due',
|
|
782
|
+
date: { on_or_after: '2024-06-01' },
|
|
783
|
+
});
|
|
784
|
+
});
|
|
785
|
+
});
|
|
786
|
+
|
|
787
|
+
// ─── parseFilterOperator ─────────────────────────────────────────────────────
|
|
788
|
+
|
|
789
|
+
describe('parseFilterOperator', () => {
|
|
790
|
+
it('parses = operator', () => {
|
|
791
|
+
const result = parseFilterOperator('Name=Hello');
|
|
792
|
+
assert.deepEqual(result, { key: 'Name', operator: '=', value: 'Hello' });
|
|
793
|
+
});
|
|
794
|
+
|
|
795
|
+
it('parses > operator', () => {
|
|
796
|
+
const result = parseFilterOperator('Amount>100');
|
|
797
|
+
assert.deepEqual(result, { key: 'Amount', operator: '>', value: '100' });
|
|
798
|
+
});
|
|
799
|
+
|
|
800
|
+
it('parses >= operator', () => {
|
|
801
|
+
const result = parseFilterOperator('Amount>=50');
|
|
802
|
+
assert.deepEqual(result, { key: 'Amount', operator: '>=', value: '50' });
|
|
803
|
+
});
|
|
804
|
+
|
|
805
|
+
it('parses != operator', () => {
|
|
806
|
+
const result = parseFilterOperator('Status!=Done');
|
|
807
|
+
assert.deepEqual(result, { key: 'Status', operator: '!=', value: 'Done' });
|
|
808
|
+
});
|
|
809
|
+
|
|
810
|
+
it('parses <= operator', () => {
|
|
811
|
+
const result = parseFilterOperator('Day<=14');
|
|
812
|
+
assert.deepEqual(result, { key: 'Day', operator: '<=', value: '14' });
|
|
813
|
+
});
|
|
814
|
+
|
|
815
|
+
it('returns error for missing operator', () => {
|
|
816
|
+
const result = parseFilterOperator('justtext');
|
|
817
|
+
assert.ok(result.error);
|
|
818
|
+
});
|
|
819
|
+
});
|
|
820
|
+
|
|
821
|
+
// ─── resolveRelativeDate ───────────────────────────────────────────────────────
|
|
822
|
+
|
|
823
|
+
describe('resolveRelativeDate', () => {
|
|
824
|
+
it('resolves today', () => {
|
|
825
|
+
const result = resolveRelativeDate('today');
|
|
826
|
+
// Use local date (same as the function does)
|
|
827
|
+
const now = new Date();
|
|
828
|
+
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
829
|
+
assert.equal(result, today.toISOString().split('T')[0]);
|
|
830
|
+
});
|
|
831
|
+
|
|
832
|
+
it('resolves yesterday', () => {
|
|
833
|
+
const result = resolveRelativeDate('yesterday');
|
|
834
|
+
const now = new Date();
|
|
835
|
+
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
836
|
+
today.setDate(today.getDate() - 1);
|
|
837
|
+
assert.equal(result, today.toISOString().split('T')[0]);
|
|
838
|
+
});
|
|
839
|
+
|
|
840
|
+
it('passes through non-keyword values', () => {
|
|
841
|
+
assert.equal(resolveRelativeDate('2024-06-15'), '2024-06-15');
|
|
842
|
+
});
|
|
843
|
+
});
|
|
844
|
+
|
|
845
|
+
// ─── buildCompoundFilter ───────────────────────────────────────────────────────
|
|
846
|
+
|
|
847
|
+
describe('buildCompoundFilter', () => {
|
|
848
|
+
const schema = {
|
|
849
|
+
name: { type: 'title', name: 'Name' },
|
|
850
|
+
status: { type: 'select', name: 'Status' },
|
|
851
|
+
amount: { type: 'number', name: 'Amount' },
|
|
852
|
+
};
|
|
853
|
+
|
|
854
|
+
it('returns single filter for one entry', () => {
|
|
855
|
+
const result = buildCompoundFilter(schema, ['Status=Active']);
|
|
856
|
+
assert.ok(!result.error);
|
|
857
|
+
assert.deepEqual(result.filter, { property: 'Status', select: { equals: 'Active' } });
|
|
858
|
+
});
|
|
859
|
+
|
|
860
|
+
it('returns AND compound for multiple filters', () => {
|
|
861
|
+
const result = buildCompoundFilter(schema, ['Status=Active', 'Amount>10']);
|
|
862
|
+
assert.ok(!result.error);
|
|
863
|
+
assert.ok(result.filter.and);
|
|
864
|
+
assert.equal(result.filter.and.length, 2);
|
|
865
|
+
assert.deepEqual(result.filter.and[0], { property: 'Status', select: { equals: 'Active' } });
|
|
866
|
+
assert.deepEqual(result.filter.and[1], { property: 'Amount', number: { greater_than: 10 } });
|
|
867
|
+
});
|
|
868
|
+
|
|
869
|
+
it('returns error if any filter is invalid', () => {
|
|
870
|
+
const result = buildCompoundFilter(schema, ['Status=Active', 'bogus']);
|
|
871
|
+
assert.ok(result.error);
|
|
872
|
+
});
|
|
873
|
+
});
|
|
874
|
+
|
|
875
|
+
// ─── markdownToBlocks ──────────────────────────────────────────────────────────
|
|
876
|
+
|
|
877
|
+
describe('markdownToBlocks', () => {
|
|
878
|
+
it('parses headings', () => {
|
|
879
|
+
const blocks = markdownToBlocks('# Title\n## Subtitle\n### Section');
|
|
880
|
+
assert.equal(blocks.length, 3);
|
|
881
|
+
assert.equal(blocks[0].type, 'heading_1');
|
|
882
|
+
assert.equal(blocks[1].type, 'heading_2');
|
|
883
|
+
assert.equal(blocks[2].type, 'heading_3');
|
|
884
|
+
});
|
|
885
|
+
|
|
886
|
+
it('parses paragraphs', () => {
|
|
887
|
+
const blocks = markdownToBlocks('Hello world');
|
|
888
|
+
assert.equal(blocks.length, 1);
|
|
889
|
+
assert.equal(blocks[0].type, 'paragraph');
|
|
890
|
+
assert.equal(blocks[0].paragraph.rich_text[0].text.content, 'Hello world');
|
|
891
|
+
});
|
|
892
|
+
|
|
893
|
+
it('parses bullet lists', () => {
|
|
894
|
+
const blocks = markdownToBlocks('- Item 1\n- Item 2\n* Item 3');
|
|
895
|
+
assert.equal(blocks.length, 3);
|
|
896
|
+
blocks.forEach(b => assert.equal(b.type, 'bulleted_list_item'));
|
|
897
|
+
});
|
|
898
|
+
|
|
899
|
+
it('parses numbered lists', () => {
|
|
900
|
+
const blocks = markdownToBlocks('1. First\n2. Second');
|
|
901
|
+
assert.equal(blocks.length, 2);
|
|
902
|
+
blocks.forEach(b => assert.equal(b.type, 'numbered_list_item'));
|
|
903
|
+
});
|
|
904
|
+
|
|
905
|
+
it('parses code blocks', () => {
|
|
906
|
+
const blocks = markdownToBlocks('```javascript\nconst x = 1;\n```');
|
|
907
|
+
assert.equal(blocks.length, 1);
|
|
908
|
+
assert.equal(blocks[0].type, 'code');
|
|
909
|
+
assert.equal(blocks[0].code.language, 'javascript');
|
|
910
|
+
assert.equal(blocks[0].code.rich_text[0].text.content, 'const x = 1;');
|
|
911
|
+
});
|
|
912
|
+
|
|
913
|
+
it('parses quotes', () => {
|
|
914
|
+
const blocks = markdownToBlocks('> This is a quote');
|
|
915
|
+
assert.equal(blocks[0].type, 'quote');
|
|
916
|
+
});
|
|
917
|
+
|
|
918
|
+
it('parses dividers', () => {
|
|
919
|
+
const blocks = markdownToBlocks('---');
|
|
920
|
+
assert.equal(blocks[0].type, 'divider');
|
|
921
|
+
});
|
|
922
|
+
|
|
923
|
+
it('parses todo items', () => {
|
|
924
|
+
const blocks = markdownToBlocks('- [ ] Not done\n- [x] Done');
|
|
925
|
+
assert.equal(blocks[0].type, 'to_do');
|
|
926
|
+
assert.equal(blocks[0].to_do.checked, false);
|
|
927
|
+
assert.equal(blocks[1].to_do.checked, true);
|
|
928
|
+
});
|
|
929
|
+
|
|
930
|
+
it('skips empty lines', () => {
|
|
931
|
+
const blocks = markdownToBlocks('Line 1\n\nLine 2');
|
|
932
|
+
assert.equal(blocks.length, 2);
|
|
933
|
+
});
|
|
934
|
+
});
|
|
935
|
+
|
|
936
|
+
// ─── parseInlineFormatting ─────────────────────────────────────────────────────
|
|
937
|
+
|
|
938
|
+
describe('parseInlineFormatting', () => {
|
|
939
|
+
it('parses bold text', () => {
|
|
940
|
+
const result = parseInlineFormatting('Hello **bold** world');
|
|
941
|
+
assert.equal(result.length, 3);
|
|
942
|
+
assert.equal(result[1].annotations.bold, true);
|
|
943
|
+
assert.equal(result[1].text.content, 'bold');
|
|
944
|
+
});
|
|
945
|
+
|
|
946
|
+
it('parses italic text', () => {
|
|
947
|
+
const result = parseInlineFormatting('Hello *italic* world');
|
|
948
|
+
assert.equal(result.length, 3);
|
|
949
|
+
assert.equal(result[1].annotations.italic, true);
|
|
950
|
+
});
|
|
951
|
+
|
|
952
|
+
it('parses inline code', () => {
|
|
953
|
+
const result = parseInlineFormatting('Use `notion query` here');
|
|
954
|
+
assert.equal(result.length, 3);
|
|
955
|
+
assert.equal(result[1].annotations.code, true);
|
|
956
|
+
assert.equal(result[1].text.content, 'notion query');
|
|
957
|
+
});
|
|
958
|
+
|
|
959
|
+
it('parses links', () => {
|
|
960
|
+
const result = parseInlineFormatting('Visit [GitHub](https://github.com) now');
|
|
961
|
+
assert.equal(result.length, 3);
|
|
962
|
+
assert.equal(result[1].text.content, 'GitHub');
|
|
963
|
+
assert.equal(result[1].text.link.url, 'https://github.com');
|
|
964
|
+
});
|
|
965
|
+
|
|
966
|
+
it('returns plain text when no formatting', () => {
|
|
967
|
+
const result = parseInlineFormatting('Just plain text');
|
|
968
|
+
assert.equal(result.length, 1);
|
|
969
|
+
assert.equal(result[0].text.content, 'Just plain text');
|
|
970
|
+
});
|
|
971
|
+
});
|
|
972
|
+
|
|
973
|
+
// ─── blocksToMarkdown ──────────────────────────────────────────────────────────
|
|
974
|
+
|
|
975
|
+
describe('blocksToMarkdown', () => {
|
|
976
|
+
it('converts heading blocks', () => {
|
|
977
|
+
const blocks = [
|
|
978
|
+
{ type: 'heading_1', heading_1: { rich_text: [{ plain_text: 'Title' }] } },
|
|
979
|
+
{ type: 'heading_2', heading_2: { rich_text: [{ plain_text: 'Sub' }] } },
|
|
980
|
+
];
|
|
981
|
+
const md = blocksToMarkdown(blocks);
|
|
982
|
+
assert.ok(md.includes('# Title'));
|
|
983
|
+
assert.ok(md.includes('## Sub'));
|
|
984
|
+
});
|
|
985
|
+
|
|
986
|
+
it('converts paragraph blocks', () => {
|
|
987
|
+
const blocks = [
|
|
988
|
+
{ type: 'paragraph', paragraph: { rich_text: [{ plain_text: 'Hello' }] } },
|
|
989
|
+
];
|
|
990
|
+
assert.equal(blocksToMarkdown(blocks), 'Hello');
|
|
991
|
+
});
|
|
992
|
+
|
|
993
|
+
it('converts code blocks', () => {
|
|
994
|
+
const blocks = [
|
|
995
|
+
{ type: 'code', code: { rich_text: [{ plain_text: 'const x = 1;' }], language: 'js' } },
|
|
996
|
+
];
|
|
997
|
+
const md = blocksToMarkdown(blocks);
|
|
998
|
+
assert.ok(md.includes('```js'));
|
|
999
|
+
assert.ok(md.includes('const x = 1;'));
|
|
1000
|
+
});
|
|
1001
|
+
});
|
|
1002
|
+
|
|
1003
|
+
// ─── parseCsv ──────────────────────────────────────────────────────────────────
|
|
1004
|
+
|
|
1005
|
+
describe('parseCsv', () => {
|
|
1006
|
+
it('parses simple CSV', () => {
|
|
1007
|
+
const rows = parseCsv('Name,Status\nTask 1,Done\nTask 2,Todo');
|
|
1008
|
+
assert.equal(rows.length, 2);
|
|
1009
|
+
assert.equal(rows[0].Name, 'Task 1');
|
|
1010
|
+
assert.equal(rows[0].Status, 'Done');
|
|
1011
|
+
assert.equal(rows[1].Name, 'Task 2');
|
|
1012
|
+
});
|
|
1013
|
+
|
|
1014
|
+
it('handles quoted fields with commas', () => {
|
|
1015
|
+
const rows = parseCsv('Name,Notes\n"Task, Important","Note, here"');
|
|
1016
|
+
assert.equal(rows.length, 1);
|
|
1017
|
+
assert.equal(rows[0].Name, 'Task, Important');
|
|
1018
|
+
assert.equal(rows[0].Notes, 'Note, here');
|
|
1019
|
+
});
|
|
1020
|
+
|
|
1021
|
+
it('handles escaped quotes', () => {
|
|
1022
|
+
const rows = parseCsv('Name\n"He said ""hello"""');
|
|
1023
|
+
assert.equal(rows[0].Name, 'He said "hello"');
|
|
1024
|
+
});
|
|
1025
|
+
|
|
1026
|
+
it('returns empty for single line', () => {
|
|
1027
|
+
const rows = parseCsv('Name,Status');
|
|
1028
|
+
assert.equal(rows.length, 0);
|
|
1029
|
+
});
|
|
1030
|
+
});
|
|
1031
|
+
|
|
1032
|
+
// ─── kebabToProperty ───────────────────────────────────────────────────────────
|
|
1033
|
+
|
|
1034
|
+
describe('kebabToProperty', () => {
|
|
1035
|
+
const schema = {
|
|
1036
|
+
name: { type: 'title', name: 'Name' },
|
|
1037
|
+
status: { type: 'select', name: 'Status' },
|
|
1038
|
+
'due date': { type: 'date', name: 'Due Date' },
|
|
1039
|
+
};
|
|
1040
|
+
|
|
1041
|
+
it('matches exact lowercase', () => {
|
|
1042
|
+
const result = kebabToProperty('name', schema);
|
|
1043
|
+
assert.equal(result.name, 'Name');
|
|
1044
|
+
});
|
|
1045
|
+
|
|
1046
|
+
it('matches kebab to space', () => {
|
|
1047
|
+
const result = kebabToProperty('due-date', schema);
|
|
1048
|
+
assert.equal(result.name, 'Due Date');
|
|
1049
|
+
});
|
|
1050
|
+
|
|
1051
|
+
it('returns null for no match', () => {
|
|
1052
|
+
const result = kebabToProperty('nonexistent', schema);
|
|
1053
|
+
assert.equal(result, null);
|
|
1054
|
+
});
|
|
1055
|
+
|
|
1056
|
+
it('strips leading dashes', () => {
|
|
1057
|
+
const result = kebabToProperty('--status', schema);
|
|
1058
|
+
assert.equal(result.name, 'Status');
|
|
1059
|
+
});
|
|
1060
|
+
});
|
|
1061
|
+
|
|
1062
|
+
// ─── extractDynamicProps ───────────────────────────────────────────────────────
|
|
1063
|
+
|
|
1064
|
+
describe('extractDynamicProps', () => {
|
|
1065
|
+
const schema = {
|
|
1066
|
+
name: { type: 'title', name: 'Name' },
|
|
1067
|
+
status: { type: 'select', name: 'Status' },
|
|
1068
|
+
'due date': { type: 'date', name: 'Due Date' },
|
|
1069
|
+
};
|
|
1070
|
+
|
|
1071
|
+
it('extracts dynamic property flags', () => {
|
|
1072
|
+
const argv = ['node', 'notion', 'add', 'tasks', '--name', 'Ship it', '--status', 'Done'];
|
|
1073
|
+
const result = extractDynamicProps(argv, ['prop', 'from'], schema);
|
|
1074
|
+
assert.deepEqual(result, ['Name=Ship it', 'Status=Done']);
|
|
1075
|
+
});
|
|
1076
|
+
|
|
1077
|
+
it('skips known flags', () => {
|
|
1078
|
+
const argv = ['node', 'notion', 'add', 'tasks', '--prop', 'Name=Hello', '--name', 'World'];
|
|
1079
|
+
const result = extractDynamicProps(argv, ['prop', 'from'], schema);
|
|
1080
|
+
assert.deepEqual(result, ['Name=World']);
|
|
1081
|
+
});
|
|
1082
|
+
|
|
1083
|
+
it('handles kebab-case properties', () => {
|
|
1084
|
+
const argv = ['node', 'notion', 'add', 'tasks', '--due-date', '2024-06-15'];
|
|
1085
|
+
const result = extractDynamicProps(argv, ['prop'], schema);
|
|
1086
|
+
assert.deepEqual(result, ['Due Date=2024-06-15']);
|
|
1087
|
+
});
|
|
1088
|
+
|
|
1089
|
+
it('ignores flags not in schema', () => {
|
|
1090
|
+
const argv = ['node', 'notion', 'add', 'tasks', '--bogus', 'value'];
|
|
1091
|
+
const result = extractDynamicProps(argv, ['prop'], schema);
|
|
1092
|
+
assert.deepEqual(result, []);
|
|
1093
|
+
});
|
|
702
1094
|
});
|
package/test/debug-parent.js
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
const { Client } = require('@notionhq/client');
|
|
2
|
-
const h = require('../lib/helpers');
|
|
3
|
-
const notion = new Client({ auth: h.loadConfig(h.getConfigPaths().CONFIG_PATH).apiKey });
|
|
4
|
-
const parentId = '302903e2-cff4-8059-b808-e3c953fd7d26';
|
|
5
|
-
|
|
6
|
-
(async () => {
|
|
7
|
-
const db = await notion.databases.create({
|
|
8
|
-
parent: { type: 'page_id', page_id: parentId },
|
|
9
|
-
title: [{ text: { content: 'Quick Test DB' } }],
|
|
10
|
-
properties: { Name: { title: {} }, Priority: { number: {} } }
|
|
11
|
-
});
|
|
12
|
-
console.log('DB created:', db.id.slice(0,8));
|
|
13
|
-
console.log('DB keys with id:', Object.keys(db).filter(k => k.includes('id')));
|
|
14
|
-
console.log('database_id:', db.database_id);
|
|
15
|
-
|
|
16
|
-
await new Promise(r => setTimeout(r, 2000));
|
|
17
|
-
|
|
18
|
-
for (const pt of ['data_source_id', 'database_id']) {
|
|
19
|
-
const pid = pt === 'database_id' && db.database_id ? db.database_id : db.id;
|
|
20
|
-
try {
|
|
21
|
-
const page = await notion.pages.create({
|
|
22
|
-
parent: { type: pt, [pt]: pid },
|
|
23
|
-
properties: { Name: { title: [{ text: { content: 'test ' + pt } }] } }
|
|
24
|
-
});
|
|
25
|
-
console.log(pt, 'WORKS:', page.id.slice(0,8));
|
|
26
|
-
await notion.pages.update({ page_id: page.id, archived: true });
|
|
27
|
-
} catch(e) { console.log(pt, 'FAILED:', e.code, e.message.slice(0,100)); }
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
await notion.blocks.delete({ block_id: db.id });
|
|
31
|
-
console.log('cleaned');
|
|
32
|
-
})();
|