@catafal/notion-cli 5.9.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/LICENSE +21 -0
- package/README.md +552 -0
- package/bin/dev +17 -0
- package/bin/dev.cmd +3 -0
- package/bin/run +14 -0
- package/bin/run.cmd +3 -0
- package/dist/base-command.d.ts +73 -0
- package/dist/base-command.js +179 -0
- package/dist/base-flags.d.ts +14 -0
- package/dist/base-flags.js +59 -0
- package/dist/cache.d.ts +84 -0
- package/dist/cache.js +351 -0
- package/dist/commands/append.d.ts +37 -0
- package/dist/commands/append.js +120 -0
- package/dist/commands/batch/delete.d.ts +42 -0
- package/dist/commands/batch/delete.js +199 -0
- package/dist/commands/batch/retrieve.d.ts +43 -0
- package/dist/commands/batch/retrieve.js +272 -0
- package/dist/commands/block/append.d.ts +42 -0
- package/dist/commands/block/append.js +219 -0
- package/dist/commands/block/delete.d.ts +30 -0
- package/dist/commands/block/delete.js +97 -0
- package/dist/commands/block/retrieve/children.d.ts +31 -0
- package/dist/commands/block/retrieve/children.js +177 -0
- package/dist/commands/block/retrieve.d.ts +30 -0
- package/dist/commands/block/retrieve.js +101 -0
- package/dist/commands/block/update.d.ts +45 -0
- package/dist/commands/block/update.js +242 -0
- package/dist/commands/bookmark/list.d.ts +30 -0
- package/dist/commands/bookmark/list.js +60 -0
- package/dist/commands/bookmark/remove.d.ts +26 -0
- package/dist/commands/bookmark/remove.js +47 -0
- package/dist/commands/bookmark/set.d.ts +29 -0
- package/dist/commands/bookmark/set.js +96 -0
- package/dist/commands/browse.d.ts +13 -0
- package/dist/commands/browse.js +44 -0
- package/dist/commands/cache/info.d.ts +19 -0
- package/dist/commands/cache/info.js +145 -0
- package/dist/commands/config/set-token.d.ts +22 -0
- package/dist/commands/config/set-token.js +137 -0
- package/dist/commands/daily/index.d.ts +32 -0
- package/dist/commands/daily/index.js +135 -0
- package/dist/commands/daily/setup.d.ts +42 -0
- package/dist/commands/daily/setup.js +149 -0
- package/dist/commands/db/create.d.ts +31 -0
- package/dist/commands/db/create.js +124 -0
- package/dist/commands/db/query.d.ts +41 -0
- package/dist/commands/db/query.js +360 -0
- package/dist/commands/db/retrieve.d.ts +33 -0
- package/dist/commands/db/retrieve.js +134 -0
- package/dist/commands/db/schema.d.ts +32 -0
- package/dist/commands/db/schema.js +308 -0
- package/dist/commands/db/update.d.ts +31 -0
- package/dist/commands/db/update.js +117 -0
- package/dist/commands/doctor.d.ts +50 -0
- package/dist/commands/doctor.js +420 -0
- package/dist/commands/init.d.ts +65 -0
- package/dist/commands/init.js +479 -0
- package/dist/commands/list.d.ts +29 -0
- package/dist/commands/list.js +219 -0
- package/dist/commands/open.d.ts +29 -0
- package/dist/commands/open.js +100 -0
- package/dist/commands/page/create.d.ts +33 -0
- package/dist/commands/page/create.js +261 -0
- package/dist/commands/page/delete.d.ts +36 -0
- package/dist/commands/page/delete.js +107 -0
- package/dist/commands/page/export.d.ts +38 -0
- package/dist/commands/page/export.js +120 -0
- package/dist/commands/page/retrieve/property_item.d.ts +24 -0
- package/dist/commands/page/retrieve/property_item.js +75 -0
- package/dist/commands/page/retrieve.d.ts +36 -0
- package/dist/commands/page/retrieve.js +244 -0
- package/dist/commands/page/update.d.ts +34 -0
- package/dist/commands/page/update.js +184 -0
- package/dist/commands/quick.d.ts +35 -0
- package/dist/commands/quick.js +168 -0
- package/dist/commands/search.d.ts +43 -0
- package/dist/commands/search.js +361 -0
- package/dist/commands/stats.d.ts +35 -0
- package/dist/commands/stats.js +274 -0
- package/dist/commands/sync.d.ts +24 -0
- package/dist/commands/sync.js +183 -0
- package/dist/commands/template/get.d.ts +28 -0
- package/dist/commands/template/get.js +59 -0
- package/dist/commands/template/list.d.ts +32 -0
- package/dist/commands/template/list.js +62 -0
- package/dist/commands/template/remove.d.ts +27 -0
- package/dist/commands/template/remove.js +48 -0
- package/dist/commands/template/save.d.ts +32 -0
- package/dist/commands/template/save.js +92 -0
- package/dist/commands/template/use.d.ts +34 -0
- package/dist/commands/template/use.js +142 -0
- package/dist/commands/user/list.d.ts +27 -0
- package/dist/commands/user/list.js +99 -0
- package/dist/commands/user/retrieve/bot.d.ts +28 -0
- package/dist/commands/user/retrieve/bot.js +96 -0
- package/dist/commands/user/retrieve.d.ts +30 -0
- package/dist/commands/user/retrieve.js +103 -0
- package/dist/commands/whoami.d.ts +19 -0
- package/dist/commands/whoami.js +175 -0
- package/dist/deduplication.d.ts +41 -0
- package/dist/deduplication.js +71 -0
- package/dist/envelope.d.ts +169 -0
- package/dist/envelope.js +257 -0
- package/dist/errors/enhanced-errors.d.ts +168 -0
- package/dist/errors/enhanced-errors.js +567 -0
- package/dist/errors/index.d.ts +18 -0
- package/dist/errors/index.js +33 -0
- package/dist/examples/cache-retry-examples.d.ts +64 -0
- package/dist/examples/cache-retry-examples.js +375 -0
- package/dist/helper.d.ts +102 -0
- package/dist/helper.js +885 -0
- package/dist/http-agent.d.ts +38 -0
- package/dist/http-agent.js +60 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +4 -0
- package/dist/interface.d.ts +4 -0
- package/dist/interface.js +2 -0
- package/dist/notion.d.ts +144 -0
- package/dist/notion.js +547 -0
- package/dist/retry.d.ts +72 -0
- package/dist/retry.js +381 -0
- package/dist/utils/bookmarks.d.ts +32 -0
- package/dist/utils/bookmarks.js +98 -0
- package/dist/utils/daily-config.d.ts +22 -0
- package/dist/utils/daily-config.js +60 -0
- package/dist/utils/disk-cache.d.ts +80 -0
- package/dist/utils/disk-cache.js +291 -0
- package/dist/utils/fuzzy.d.ts +36 -0
- package/dist/utils/fuzzy.js +69 -0
- package/dist/utils/interactive-navigator.d.ts +63 -0
- package/dist/utils/interactive-navigator.js +123 -0
- package/dist/utils/markdown-to-blocks.d.ts +21 -0
- package/dist/utils/markdown-to-blocks.js +333 -0
- package/dist/utils/notion-resolver.d.ts +49 -0
- package/dist/utils/notion-resolver.js +278 -0
- package/dist/utils/notion-url-parser.d.ts +48 -0
- package/dist/utils/notion-url-parser.js +121 -0
- package/dist/utils/property-expander.d.ts +45 -0
- package/dist/utils/property-expander.js +323 -0
- package/dist/utils/schema-examples.d.ts +40 -0
- package/dist/utils/schema-examples.js +359 -0
- package/dist/utils/schema-extractor.d.ts +65 -0
- package/dist/utils/schema-extractor.js +235 -0
- package/dist/utils/shell-config.d.ts +30 -0
- package/dist/utils/shell-config.js +84 -0
- package/dist/utils/table-formatter.d.ts +36 -0
- package/dist/utils/table-formatter.js +125 -0
- package/dist/utils/templates.d.ts +30 -0
- package/dist/utils/templates.js +82 -0
- package/dist/utils/terminal-banner.d.ts +24 -0
- package/dist/utils/terminal-banner.js +34 -0
- package/dist/utils/token-validator.d.ts +42 -0
- package/dist/utils/token-validator.js +66 -0
- package/dist/utils/update-notifier.d.ts +26 -0
- package/dist/utils/update-notifier.js +54 -0
- package/dist/utils/workspace-cache.d.ts +58 -0
- package/dist/utils/workspace-cache.js +185 -0
- package/oclif.manifest.json +6471 -0
- package/package.json +118 -0
- package/scripts/banner.js +38 -0
- package/scripts/postinstall.js +44 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Notion URL Parser
|
|
3
|
+
*
|
|
4
|
+
* Extracts clean Notion IDs from various input formats:
|
|
5
|
+
* - Title-slug URLs: https://www.notion.so/My-Page-Title-1fb79d4c71bb8032b722c82305b63a00
|
|
6
|
+
* - Workspace URLs: https://www.notion.so/workspace/Page-Title-1fb79d4c71bb8032b722c82305b63a00
|
|
7
|
+
* - Bare ID URLs: https://www.notion.so/1fb79d4c71bb8032b722c82305b63a00?v=...
|
|
8
|
+
* - Short URLs: notion.so/1fb79d4c71bb8032b722c82305b63a00
|
|
9
|
+
* - Raw IDs with dashes: 1fb79d4c-71bb-8032-b722-c82305b63a00
|
|
10
|
+
* - Raw IDs without dashes: 1fb79d4c71bb8032b722c82305b63a00
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* Extract Notion ID from URL or raw ID
|
|
14
|
+
*
|
|
15
|
+
* @param input - Full Notion URL, partial URL, or raw ID
|
|
16
|
+
* @returns Clean Notion ID (32 hex characters without dashes)
|
|
17
|
+
* @throws Error if input is invalid
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* // URL with page title slug (most common format)
|
|
21
|
+
* extractNotionId('https://www.notion.so/My-Page-1fb79d4c71bb8032b722c82305b63a00')
|
|
22
|
+
* // Returns: '1fb79d4c71bb8032b722c82305b63a00'
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* // Bare ID URL
|
|
26
|
+
* extractNotionId('https://www.notion.so/1fb79d4c71bb8032b722c82305b63a00?v=...')
|
|
27
|
+
* // Returns: '1fb79d4c71bb8032b722c82305b63a00'
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* // Raw ID with dashes
|
|
31
|
+
* extractNotionId('1fb79d4c-71bb-8032-b722-c82305b63a00')
|
|
32
|
+
* // Returns: '1fb79d4c71bb8032b722c82305b63a00'
|
|
33
|
+
*/
|
|
34
|
+
export declare function extractNotionId(input: string): string;
|
|
35
|
+
/**
|
|
36
|
+
* Check if a string looks like a Notion URL
|
|
37
|
+
*
|
|
38
|
+
* @param input - String to check
|
|
39
|
+
* @returns True if input appears to be a Notion URL
|
|
40
|
+
*/
|
|
41
|
+
export declare function isNotionUrl(input: string): boolean;
|
|
42
|
+
/**
|
|
43
|
+
* Check if a string looks like a valid Notion ID
|
|
44
|
+
*
|
|
45
|
+
* @param input - String to check
|
|
46
|
+
* @returns True if input appears to be a valid Notion ID
|
|
47
|
+
*/
|
|
48
|
+
export declare function isValidNotionId(input: string): boolean;
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Notion URL Parser
|
|
4
|
+
*
|
|
5
|
+
* Extracts clean Notion IDs from various input formats:
|
|
6
|
+
* - Title-slug URLs: https://www.notion.so/My-Page-Title-1fb79d4c71bb8032b722c82305b63a00
|
|
7
|
+
* - Workspace URLs: https://www.notion.so/workspace/Page-Title-1fb79d4c71bb8032b722c82305b63a00
|
|
8
|
+
* - Bare ID URLs: https://www.notion.so/1fb79d4c71bb8032b722c82305b63a00?v=...
|
|
9
|
+
* - Short URLs: notion.so/1fb79d4c71bb8032b722c82305b63a00
|
|
10
|
+
* - Raw IDs with dashes: 1fb79d4c-71bb-8032-b722-c82305b63a00
|
|
11
|
+
* - Raw IDs without dashes: 1fb79d4c71bb8032b722c82305b63a00
|
|
12
|
+
*/
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.extractNotionId = extractNotionId;
|
|
15
|
+
exports.isNotionUrl = isNotionUrl;
|
|
16
|
+
exports.isValidNotionId = isValidNotionId;
|
|
17
|
+
/**
|
|
18
|
+
* Extract Notion ID from URL or raw ID
|
|
19
|
+
*
|
|
20
|
+
* @param input - Full Notion URL, partial URL, or raw ID
|
|
21
|
+
* @returns Clean Notion ID (32 hex characters without dashes)
|
|
22
|
+
* @throws Error if input is invalid
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* // URL with page title slug (most common format)
|
|
26
|
+
* extractNotionId('https://www.notion.so/My-Page-1fb79d4c71bb8032b722c82305b63a00')
|
|
27
|
+
* // Returns: '1fb79d4c71bb8032b722c82305b63a00'
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* // Bare ID URL
|
|
31
|
+
* extractNotionId('https://www.notion.so/1fb79d4c71bb8032b722c82305b63a00?v=...')
|
|
32
|
+
* // Returns: '1fb79d4c71bb8032b722c82305b63a00'
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* // Raw ID with dashes
|
|
36
|
+
* extractNotionId('1fb79d4c-71bb-8032-b722-c82305b63a00')
|
|
37
|
+
* // Returns: '1fb79d4c71bb8032b722c82305b63a00'
|
|
38
|
+
*/
|
|
39
|
+
function extractNotionId(input) {
|
|
40
|
+
if (!input || typeof input !== 'string') {
|
|
41
|
+
throw new Error('Input must be a non-empty string');
|
|
42
|
+
}
|
|
43
|
+
const trimmed = input.trim();
|
|
44
|
+
// Check if it's a URL (contains notion.so or http)
|
|
45
|
+
if (trimmed.includes('notion.so') || trimmed.includes('http')) {
|
|
46
|
+
return extractIdFromUrl(trimmed);
|
|
47
|
+
}
|
|
48
|
+
// Not a URL, treat as raw ID
|
|
49
|
+
return cleanRawId(trimmed);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Extract ID from Notion URL
|
|
53
|
+
*/
|
|
54
|
+
function extractIdFromUrl(url) {
|
|
55
|
+
// Notion URL formats (the ID is always the last 32 hex chars in the path):
|
|
56
|
+
// https://www.notion.so/1fb79d4c71bb8032b722c82305b63a00
|
|
57
|
+
// https://www.notion.so/1fb79d4c71bb8032b722c82305b63a00?v=view_id
|
|
58
|
+
// https://www.notion.so/My-Page-Title-1fb79d4c71bb8032b722c82305b63a00
|
|
59
|
+
// https://www.notion.so/workspace/Page-Title-1fb79d4c71bb8032b722c82305b63a00
|
|
60
|
+
// Strip query params and hash before matching
|
|
61
|
+
const pathOnly = url.split(/[?#]/)[0];
|
|
62
|
+
// Extract the last 32 hex characters from the URL path.
|
|
63
|
+
// Notion always appends the ID at the end of the slug (after the last dash).
|
|
64
|
+
const match = pathOnly.match(/([a-f0-9]{32})$/i);
|
|
65
|
+
if (match) {
|
|
66
|
+
return match[1].toLowerCase();
|
|
67
|
+
}
|
|
68
|
+
// Fallback: try matching a dashed UUID anywhere in the path
|
|
69
|
+
const dashedMatch = pathOnly.match(/([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})/i);
|
|
70
|
+
if (dashedMatch) {
|
|
71
|
+
return cleanRawId(dashedMatch[1]);
|
|
72
|
+
}
|
|
73
|
+
throw new Error(`Could not extract Notion ID from URL: ${url}\n\n` +
|
|
74
|
+
`Expected format: https://www.notion.so/Page-Title-{id}\n` +
|
|
75
|
+
`Example: https://www.notion.so/My-Page-1fb79d4c71bb8032b722c82305b63a00`);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Clean raw ID by removing dashes and validating format
|
|
79
|
+
*/
|
|
80
|
+
function cleanRawId(id) {
|
|
81
|
+
// Remove all dashes
|
|
82
|
+
const cleaned = id.replace(/-/g, '');
|
|
83
|
+
// Validate: must be exactly 32 hex characters
|
|
84
|
+
if (!/^[a-f0-9]{32}$/i.test(cleaned)) {
|
|
85
|
+
throw new Error(`Invalid Notion ID format: ${id}\n\n` +
|
|
86
|
+
`Expected: 32 hexadecimal characters (with or without dashes)\n` +
|
|
87
|
+
`Example: 1fb79d4c71bb8032b722c82305b63a00\n` +
|
|
88
|
+
`Example: 1fb79d4c-71bb-8032-b722-c82305b63a00`);
|
|
89
|
+
}
|
|
90
|
+
return cleaned.toLowerCase();
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Check if a string looks like a Notion URL
|
|
94
|
+
*
|
|
95
|
+
* @param input - String to check
|
|
96
|
+
* @returns True if input appears to be a Notion URL
|
|
97
|
+
*/
|
|
98
|
+
function isNotionUrl(input) {
|
|
99
|
+
if (!input || typeof input !== 'string') {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
return input.includes('notion.so');
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Check if a string looks like a valid Notion ID
|
|
106
|
+
*
|
|
107
|
+
* @param input - String to check
|
|
108
|
+
* @returns True if input appears to be a valid Notion ID
|
|
109
|
+
*/
|
|
110
|
+
function isValidNotionId(input) {
|
|
111
|
+
if (!input || typeof input !== 'string') {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
try {
|
|
115
|
+
extractNotionId(input);
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { GetDataSourceResponse } from '@notionhq/client/build/src/api-endpoints';
|
|
2
|
+
/**
|
|
3
|
+
* Simple flat property format for AI agents
|
|
4
|
+
* Instead of complex Notion nested structures, use simple key-value pairs:
|
|
5
|
+
* { "Name": "Task", "Status": "Done", "Tags": ["urgent", "bug"] }
|
|
6
|
+
*/
|
|
7
|
+
export interface SimpleProperties {
|
|
8
|
+
[key: string]: string | number | boolean | string[] | null;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Notion API property format (deeply nested)
|
|
12
|
+
*/
|
|
13
|
+
export interface NotionProperties {
|
|
14
|
+
[key: string]: any;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Expand simple flat properties to Notion API format
|
|
18
|
+
*
|
|
19
|
+
* This function takes simplified property values and automatically expands them
|
|
20
|
+
* to the correct Notion API structure based on the database schema.
|
|
21
|
+
*
|
|
22
|
+
* @param simple - Flat key-value property object
|
|
23
|
+
* @param schema - Database properties schema from data source
|
|
24
|
+
* @returns Properly formatted Notion properties object
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* // Input (simple):
|
|
28
|
+
* { "Name": "My Task", "Status": "In Progress", "Priority": 5 }
|
|
29
|
+
*
|
|
30
|
+
* // Output (Notion format):
|
|
31
|
+
* {
|
|
32
|
+
* "Name": { "title": [{ "text": { "content": "My Task" } }] },
|
|
33
|
+
* "Status": { "select": { "name": "In Progress" } },
|
|
34
|
+
* "Priority": { "number": 5 }
|
|
35
|
+
* }
|
|
36
|
+
*/
|
|
37
|
+
export declare function expandSimpleProperties(simple: SimpleProperties, schema: GetDataSourceResponse['properties']): Promise<NotionProperties>;
|
|
38
|
+
/**
|
|
39
|
+
* Validate simple properties against schema before expansion
|
|
40
|
+
* This can be called optionally before expandSimpleProperties to get detailed errors
|
|
41
|
+
*/
|
|
42
|
+
export declare function validateSimpleProperties(simple: SimpleProperties, schema: GetDataSourceResponse['properties']): {
|
|
43
|
+
valid: boolean;
|
|
44
|
+
errors: string[];
|
|
45
|
+
};
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.expandSimpleProperties = expandSimpleProperties;
|
|
4
|
+
exports.validateSimpleProperties = validateSimpleProperties;
|
|
5
|
+
/**
|
|
6
|
+
* Expand simple flat properties to Notion API format
|
|
7
|
+
*
|
|
8
|
+
* This function takes simplified property values and automatically expands them
|
|
9
|
+
* to the correct Notion API structure based on the database schema.
|
|
10
|
+
*
|
|
11
|
+
* @param simple - Flat key-value property object
|
|
12
|
+
* @param schema - Database properties schema from data source
|
|
13
|
+
* @returns Properly formatted Notion properties object
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* // Input (simple):
|
|
17
|
+
* { "Name": "My Task", "Status": "In Progress", "Priority": 5 }
|
|
18
|
+
*
|
|
19
|
+
* // Output (Notion format):
|
|
20
|
+
* {
|
|
21
|
+
* "Name": { "title": [{ "text": { "content": "My Task" } }] },
|
|
22
|
+
* "Status": { "select": { "name": "In Progress" } },
|
|
23
|
+
* "Priority": { "number": 5 }
|
|
24
|
+
* }
|
|
25
|
+
*/
|
|
26
|
+
async function expandSimpleProperties(simple, schema) {
|
|
27
|
+
const expanded = {};
|
|
28
|
+
for (const [propName, value] of Object.entries(simple)) {
|
|
29
|
+
// Find property in schema (case-insensitive)
|
|
30
|
+
const propDef = findProperty(schema, propName);
|
|
31
|
+
if (!propDef) {
|
|
32
|
+
throw new Error(`Property "${propName}" not found in database schema.\n` +
|
|
33
|
+
`Available properties: ${Object.keys(schema).join(', ')}`);
|
|
34
|
+
}
|
|
35
|
+
// Expand based on type
|
|
36
|
+
try {
|
|
37
|
+
expanded[propDef.actualName] = expandProperty(value, propDef.type, propDef);
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
throw new Error(`Error expanding property "${propName}": ${error.message}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return expanded;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Find property in schema with case-insensitive matching
|
|
47
|
+
*/
|
|
48
|
+
function findProperty(schema, name) {
|
|
49
|
+
const normalized = name.toLowerCase();
|
|
50
|
+
for (const [key, value] of Object.entries(schema)) {
|
|
51
|
+
if (key.toLowerCase() === normalized) {
|
|
52
|
+
// Cast value to object type to allow spreading
|
|
53
|
+
const propConfig = value;
|
|
54
|
+
return { actualName: key, ...propConfig };
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Expand a single property value to Notion format based on type
|
|
61
|
+
*/
|
|
62
|
+
function expandProperty(value, type, propDef) {
|
|
63
|
+
// Handle null values
|
|
64
|
+
if (value === null) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
switch (type) {
|
|
68
|
+
case 'title':
|
|
69
|
+
return {
|
|
70
|
+
title: [{ text: { content: String(value) } }]
|
|
71
|
+
};
|
|
72
|
+
case 'rich_text':
|
|
73
|
+
return {
|
|
74
|
+
rich_text: [{ text: { content: String(value) } }]
|
|
75
|
+
};
|
|
76
|
+
case 'number': {
|
|
77
|
+
const num = Number(value);
|
|
78
|
+
if (isNaN(num)) {
|
|
79
|
+
throw new Error(`Invalid number value: "${value}"`);
|
|
80
|
+
}
|
|
81
|
+
return { number: num };
|
|
82
|
+
}
|
|
83
|
+
case 'checkbox': {
|
|
84
|
+
// Handle boolean or string representations
|
|
85
|
+
let boolValue;
|
|
86
|
+
if (typeof value === 'boolean') {
|
|
87
|
+
boolValue = value;
|
|
88
|
+
}
|
|
89
|
+
else if (typeof value === 'string') {
|
|
90
|
+
const lower = value.toLowerCase();
|
|
91
|
+
if (lower === 'true' || lower === 'yes' || lower === '1') {
|
|
92
|
+
boolValue = true;
|
|
93
|
+
}
|
|
94
|
+
else if (lower === 'false' || lower === 'no' || lower === '0') {
|
|
95
|
+
boolValue = false;
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
throw new Error(`Invalid checkbox value: "${value}". Use true/false, yes/no, or 1/0`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
boolValue = Boolean(value);
|
|
103
|
+
}
|
|
104
|
+
return { checkbox: boolValue };
|
|
105
|
+
}
|
|
106
|
+
case 'select':
|
|
107
|
+
return expandSelectProperty(value, propDef);
|
|
108
|
+
case 'multi_select':
|
|
109
|
+
return expandMultiSelectProperty(value, propDef);
|
|
110
|
+
case 'status':
|
|
111
|
+
return expandStatusProperty(value, propDef);
|
|
112
|
+
case 'date':
|
|
113
|
+
return expandDateProperty(value);
|
|
114
|
+
case 'url': {
|
|
115
|
+
const urlStr = String(value);
|
|
116
|
+
// Basic URL validation
|
|
117
|
+
if (!urlStr.match(/^https?:\/\/.+/)) {
|
|
118
|
+
throw new Error(`Invalid URL: "${value}". Must start with http:// or https://`);
|
|
119
|
+
}
|
|
120
|
+
return { url: urlStr };
|
|
121
|
+
}
|
|
122
|
+
case 'email': {
|
|
123
|
+
const emailStr = String(value);
|
|
124
|
+
// Basic email validation
|
|
125
|
+
if (!emailStr.match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)) {
|
|
126
|
+
throw new Error(`Invalid email: "${value}"`);
|
|
127
|
+
}
|
|
128
|
+
return { email: emailStr };
|
|
129
|
+
}
|
|
130
|
+
case 'phone_number':
|
|
131
|
+
return { phone_number: String(value) };
|
|
132
|
+
case 'people':
|
|
133
|
+
return expandPeopleProperty(value);
|
|
134
|
+
case 'files': {
|
|
135
|
+
// Files need external URLs
|
|
136
|
+
const files = Array.isArray(value) ? value : [value];
|
|
137
|
+
return {
|
|
138
|
+
files: files.map(f => {
|
|
139
|
+
if (typeof f === 'string') {
|
|
140
|
+
return { name: f, external: { url: f } };
|
|
141
|
+
}
|
|
142
|
+
return f;
|
|
143
|
+
})
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
case 'relation': {
|
|
147
|
+
// Relations need page IDs
|
|
148
|
+
const relations = Array.isArray(value) ? value : [value];
|
|
149
|
+
return {
|
|
150
|
+
relation: relations.map(id => ({ id: String(id) }))
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
default:
|
|
154
|
+
throw new Error(`Unsupported property type: ${type}. ` +
|
|
155
|
+
`Supported types: title, rich_text, number, checkbox, select, multi_select, ` +
|
|
156
|
+
`status, date, url, email, phone_number, people, files, relation`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Expand select property with validation
|
|
161
|
+
*/
|
|
162
|
+
function expandSelectProperty(value, propDef) {
|
|
163
|
+
var _a;
|
|
164
|
+
const selectOptions = ((_a = propDef.select) === null || _a === void 0 ? void 0 : _a.options) || [];
|
|
165
|
+
const strValue = String(value);
|
|
166
|
+
// Case-insensitive matching
|
|
167
|
+
const validOption = selectOptions.find((opt) => opt.name.toLowerCase() === strValue.toLowerCase());
|
|
168
|
+
if (!validOption && selectOptions.length > 0) {
|
|
169
|
+
const optionNames = selectOptions.map((o) => o.name).join(', ');
|
|
170
|
+
throw new Error(`Invalid select value: "${value}"\n` +
|
|
171
|
+
`Valid options: ${optionNames}\n` +
|
|
172
|
+
`Tip: Values are case-insensitive`);
|
|
173
|
+
}
|
|
174
|
+
// Use the exact option name from schema (preserving case)
|
|
175
|
+
const exactName = validOption ? validOption.name : strValue;
|
|
176
|
+
return { select: { name: exactName } };
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Expand multi-select property with validation
|
|
180
|
+
*/
|
|
181
|
+
function expandMultiSelectProperty(value, propDef) {
|
|
182
|
+
var _a;
|
|
183
|
+
const values = Array.isArray(value) ? value : [value];
|
|
184
|
+
const multiOptions = ((_a = propDef.multi_select) === null || _a === void 0 ? void 0 : _a.options) || [];
|
|
185
|
+
const validated = values.map(v => {
|
|
186
|
+
const strValue = String(v);
|
|
187
|
+
// Case-insensitive matching
|
|
188
|
+
const validOption = multiOptions.find((opt) => opt.name.toLowerCase() === strValue.toLowerCase());
|
|
189
|
+
if (!validOption && multiOptions.length > 0) {
|
|
190
|
+
const optionNames = multiOptions.map((o) => o.name).join(', ');
|
|
191
|
+
throw new Error(`Invalid multi-select value: "${v}"\n` +
|
|
192
|
+
`Valid options: ${optionNames}`);
|
|
193
|
+
}
|
|
194
|
+
// Use exact option name from schema
|
|
195
|
+
const exactName = validOption ? validOption.name : strValue;
|
|
196
|
+
return { name: exactName };
|
|
197
|
+
});
|
|
198
|
+
return { multi_select: validated };
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Expand status property with validation
|
|
202
|
+
*/
|
|
203
|
+
function expandStatusProperty(value, propDef) {
|
|
204
|
+
var _a;
|
|
205
|
+
const statusOptions = ((_a = propDef.status) === null || _a === void 0 ? void 0 : _a.options) || [];
|
|
206
|
+
const strValue = String(value);
|
|
207
|
+
// Case-insensitive matching
|
|
208
|
+
const validStatus = statusOptions.find((opt) => opt.name.toLowerCase() === strValue.toLowerCase());
|
|
209
|
+
if (!validStatus && statusOptions.length > 0) {
|
|
210
|
+
const optionNames = statusOptions.map((o) => o.name).join(', ');
|
|
211
|
+
throw new Error(`Invalid status value: "${value}"\n` +
|
|
212
|
+
`Valid options: ${optionNames}`);
|
|
213
|
+
}
|
|
214
|
+
// Use exact status name from schema
|
|
215
|
+
const exactName = validStatus ? validStatus.name : strValue;
|
|
216
|
+
return { status: { name: exactName } };
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Expand date property with support for ISO dates and relative dates
|
|
220
|
+
*/
|
|
221
|
+
function expandDateProperty(value) {
|
|
222
|
+
const dateStr = parseRelativeDate(String(value));
|
|
223
|
+
// Check if it includes time (ISO 8601 with time component)
|
|
224
|
+
if (dateStr.includes('T')) {
|
|
225
|
+
return { date: { start: dateStr } };
|
|
226
|
+
}
|
|
227
|
+
return { date: { start: dateStr } };
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Parse relative date strings like "today", "tomorrow", "+7 days"
|
|
231
|
+
*/
|
|
232
|
+
function parseRelativeDate(value) {
|
|
233
|
+
// Handle ISO dates (YYYY-MM-DD or full ISO 8601)
|
|
234
|
+
if (/^\d{4}-\d{2}-\d{2}/.test(value)) {
|
|
235
|
+
return value;
|
|
236
|
+
}
|
|
237
|
+
// Handle relative dates
|
|
238
|
+
const today = new Date();
|
|
239
|
+
today.setHours(0, 0, 0, 0); // Reset to start of day
|
|
240
|
+
if (value.toLowerCase() === 'today') {
|
|
241
|
+
return today.toISOString().split('T')[0];
|
|
242
|
+
}
|
|
243
|
+
if (value.toLowerCase() === 'tomorrow') {
|
|
244
|
+
today.setDate(today.getDate() + 1);
|
|
245
|
+
return today.toISOString().split('T')[0];
|
|
246
|
+
}
|
|
247
|
+
if (value.toLowerCase() === 'yesterday') {
|
|
248
|
+
today.setDate(today.getDate() - 1);
|
|
249
|
+
return today.toISOString().split('T')[0];
|
|
250
|
+
}
|
|
251
|
+
// Parse "+N days/weeks/months/years" format
|
|
252
|
+
const match = value.match(/^([+-]?\d+)\s*(day|week|month|year)s?$/i);
|
|
253
|
+
if (match) {
|
|
254
|
+
const amount = parseInt(match[1]);
|
|
255
|
+
const unit = match[2].toLowerCase();
|
|
256
|
+
switch (unit) {
|
|
257
|
+
case 'day':
|
|
258
|
+
today.setDate(today.getDate() + amount);
|
|
259
|
+
break;
|
|
260
|
+
case 'week':
|
|
261
|
+
today.setDate(today.getDate() + amount * 7);
|
|
262
|
+
break;
|
|
263
|
+
case 'month':
|
|
264
|
+
today.setMonth(today.getMonth() + amount);
|
|
265
|
+
break;
|
|
266
|
+
case 'year':
|
|
267
|
+
today.setFullYear(today.getFullYear() + amount);
|
|
268
|
+
break;
|
|
269
|
+
}
|
|
270
|
+
return today.toISOString().split('T')[0];
|
|
271
|
+
}
|
|
272
|
+
// If none of the above, assume it's already a valid date string
|
|
273
|
+
return value;
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Expand people property
|
|
277
|
+
*/
|
|
278
|
+
function expandPeopleProperty(value) {
|
|
279
|
+
const users = Array.isArray(value) ? value : [value];
|
|
280
|
+
return {
|
|
281
|
+
people: users.map(u => {
|
|
282
|
+
// Support user ID or email
|
|
283
|
+
if (typeof u === 'string') {
|
|
284
|
+
// Check if it's a UUID (user ID) or email
|
|
285
|
+
if (u.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i)) {
|
|
286
|
+
return { id: u };
|
|
287
|
+
}
|
|
288
|
+
// For email, we can only use ID - throw helpful error
|
|
289
|
+
if (u.includes('@')) {
|
|
290
|
+
throw new Error(`Cannot use email addresses for people property. ` +
|
|
291
|
+
`Use Notion user IDs instead. You can get user IDs with: notion-cli user list`);
|
|
292
|
+
}
|
|
293
|
+
return { id: u };
|
|
294
|
+
}
|
|
295
|
+
return { id: String(u) };
|
|
296
|
+
})
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Validate simple properties against schema before expansion
|
|
301
|
+
* This can be called optionally before expandSimpleProperties to get detailed errors
|
|
302
|
+
*/
|
|
303
|
+
function validateSimpleProperties(simple, schema) {
|
|
304
|
+
const errors = [];
|
|
305
|
+
for (const [propName, value] of Object.entries(simple)) {
|
|
306
|
+
const propDef = findProperty(schema, propName);
|
|
307
|
+
if (!propDef) {
|
|
308
|
+
errors.push(`Property "${propName}" not found in schema`);
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
// Type-specific validation
|
|
312
|
+
try {
|
|
313
|
+
expandProperty(value, propDef.type, propDef);
|
|
314
|
+
}
|
|
315
|
+
catch (error) {
|
|
316
|
+
errors.push(`${propName}: ${error.message}`);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return {
|
|
320
|
+
valid: errors.length === 0,
|
|
321
|
+
errors
|
|
322
|
+
};
|
|
323
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Property Example Generator for Notion API
|
|
3
|
+
*
|
|
4
|
+
* Generates copy-pastable property payload examples based on database schema.
|
|
5
|
+
* Helps AI agents understand the correct format for create/update operations.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Property example with simple value and Notion API payload
|
|
9
|
+
*/
|
|
10
|
+
export interface PropertyExample {
|
|
11
|
+
property_name: string;
|
|
12
|
+
property_type: string;
|
|
13
|
+
simple_value: string | number | boolean | string[] | null;
|
|
14
|
+
notion_payload: Record<string, any>;
|
|
15
|
+
description: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Generate property examples for all properties in a data source schema
|
|
19
|
+
*
|
|
20
|
+
* @param properties - Properties object from GetDataSourceResponse
|
|
21
|
+
* @returns Array of property examples
|
|
22
|
+
*/
|
|
23
|
+
export declare function generatePropertyExamples(properties: Record<string, any>): PropertyExample[];
|
|
24
|
+
/**
|
|
25
|
+
* Format examples for human-readable console output
|
|
26
|
+
*
|
|
27
|
+
* @param examples - Array of property examples
|
|
28
|
+
* @returns Formatted string
|
|
29
|
+
*/
|
|
30
|
+
export declare function formatExamplesForConsole(examples: PropertyExample[]): string;
|
|
31
|
+
/**
|
|
32
|
+
* Group examples by writability (writable vs read-only)
|
|
33
|
+
*
|
|
34
|
+
* @param examples - Array of property examples
|
|
35
|
+
* @returns Grouped examples
|
|
36
|
+
*/
|
|
37
|
+
export declare function groupExamplesByWritability(examples: PropertyExample[]): {
|
|
38
|
+
writable: PropertyExample[];
|
|
39
|
+
readOnly: PropertyExample[];
|
|
40
|
+
};
|