@visus-io/notion-sdk-ts 3.0.2 → 3.1.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.
Files changed (91) hide show
  1. package/README.md +7 -6
  2. package/dist/api/asyncTasks.api.d.ts +70 -0
  3. package/dist/api/asyncTasks.api.js +68 -0
  4. package/dist/api/base.api.js +1 -0
  5. package/dist/api/blocks.api.d.ts +433 -73
  6. package/dist/api/blocks.api.js +39 -0
  7. package/dist/api/comments.api.d.ts +40 -5
  8. package/dist/api/comments.api.js +43 -1
  9. package/dist/api/customEmojis.api.d.ts +36 -0
  10. package/dist/api/customEmojis.api.js +36 -0
  11. package/dist/api/dataSources.api.d.ts +62 -11
  12. package/dist/api/dataSources.api.js +22 -0
  13. package/dist/api/databases.api.d.ts +40 -6
  14. package/dist/api/fileUploads.api.d.ts +1 -1
  15. package/dist/api/index.d.ts +3 -0
  16. package/dist/api/index.js +7 -1
  17. package/dist/api/pages.api.d.ts +142 -10
  18. package/dist/api/pages.api.js +64 -0
  19. package/dist/api/search.api.d.ts +10 -2
  20. package/dist/api/users.api.d.ts +1 -0
  21. package/dist/api/views.api.d.ts +284 -0
  22. package/dist/api/views.api.js +169 -0
  23. package/dist/client.js +3 -3
  24. package/dist/errors.d.ts +5 -1
  25. package/dist/errors.js +6 -0
  26. package/dist/helpers/block.helpers.d.ts +38 -1
  27. package/dist/helpers/block.helpers.js +45 -3
  28. package/dist/helpers/file.helpers.d.ts +36 -1
  29. package/dist/helpers/file.helpers.js +26 -4
  30. package/dist/helpers/filter.helpers.d.ts +6 -6
  31. package/dist/helpers/index.d.ts +3 -2
  32. package/dist/helpers/index.js +5 -1
  33. package/dist/helpers/pagination.helpers.d.ts +56 -0
  34. package/dist/helpers/pagination.helpers.js +86 -0
  35. package/dist/helpers/property.helpers.d.ts +29 -0
  36. package/dist/helpers/property.helpers.js +27 -0
  37. package/dist/helpers/webhook.helpers.d.ts +52 -0
  38. package/dist/helpers/webhook.helpers.js +81 -0
  39. package/dist/models/asyncTask.model.d.ts +59 -0
  40. package/dist/models/asyncTask.model.js +89 -0
  41. package/dist/models/block.model.js +4 -1
  42. package/dist/models/customEmoji.model.d.ts +28 -0
  43. package/dist/models/customEmoji.model.js +42 -0
  44. package/dist/models/dataSource.model.d.ts +3 -3
  45. package/dist/models/dataSource.model.js +1 -1
  46. package/dist/models/database.model.d.ts +7 -3
  47. package/dist/models/database.model.js +7 -1
  48. package/dist/models/index.d.ts +3 -0
  49. package/dist/models/index.js +7 -1
  50. package/dist/models/page.model.d.ts +2 -0
  51. package/dist/models/page.model.js +6 -0
  52. package/dist/models/view.model.d.ts +79 -0
  53. package/dist/models/view.model.js +119 -0
  54. package/dist/notion.d.ts +26 -7
  55. package/dist/notion.js +19 -3
  56. package/dist/schemas/asyncTask.schema.d.ts +42 -0
  57. package/dist/schemas/asyncTask.schema.js +83 -0
  58. package/dist/schemas/block.schema.d.ts +378 -72
  59. package/dist/schemas/block.schema.js +33 -7
  60. package/dist/schemas/comment.schema.d.ts +8 -3
  61. package/dist/schemas/customEmoji.schema.d.ts +13 -0
  62. package/dist/schemas/customEmoji.schema.js +48 -0
  63. package/dist/schemas/dataSource.schema.d.ts +68 -10
  64. package/dist/schemas/dataSource.schema.js +22 -4
  65. package/dist/schemas/database.schema.d.ts +38 -6
  66. package/dist/schemas/database.schema.js +3 -2
  67. package/dist/schemas/fileUpload.schema.d.ts +1 -1
  68. package/dist/schemas/icon.schema.d.ts +92 -0
  69. package/dist/schemas/icon.schema.js +88 -0
  70. package/dist/schemas/index.d.ts +6 -0
  71. package/dist/schemas/index.js +6 -0
  72. package/dist/schemas/meetingNotesQuery.schema.d.ts +4187 -0
  73. package/dist/schemas/meetingNotesQuery.schema.js +62 -0
  74. package/dist/schemas/page.schema.d.ts +46 -9
  75. package/dist/schemas/page.schema.js +4 -2
  76. package/dist/schemas/pageMarkdown.schema.d.ts +59 -0
  77. package/dist/schemas/pageMarkdown.schema.js +65 -0
  78. package/dist/schemas/pageProperties.schema.d.ts +30 -18
  79. package/dist/schemas/pageProperties.schema.js +2 -3
  80. package/dist/schemas/pagination.schema.d.ts +30 -3
  81. package/dist/schemas/pagination.schema.js +18 -1
  82. package/dist/schemas/parent.schema.d.ts +10 -1
  83. package/dist/schemas/parent.schema.js +9 -3
  84. package/dist/schemas/propertyObjects.schema.d.ts +12 -12
  85. package/dist/schemas/richText.schema.d.ts +8 -6
  86. package/dist/schemas/richText.schema.js +1 -1
  87. package/dist/schemas/user.schema.d.ts +2 -0
  88. package/dist/schemas/user.schema.js +2 -1
  89. package/dist/schemas/view.schema.d.ts +1077 -0
  90. package/dist/schemas/view.schema.js +115 -0
  91. package/package.json +2 -2
@@ -29,9 +29,6 @@ function resolveRichText(input) {
29
29
  }
30
30
  return input;
31
31
  }
32
- // ---------------------------------------------------------------------------
33
- // Text blocks
34
- // ---------------------------------------------------------------------------
35
32
  /**
36
33
  * Create a paragraph block.
37
34
  *
@@ -48,6 +45,7 @@ function paragraph(text, options) {
48
45
  paragraph: {
49
46
  rich_text: resolveRichText(text),
50
47
  color: options?.color ?? 'default',
48
+ ...(options?.icon ? { icon: options.icon } : {}),
51
49
  ...(options?.children ? { children: options.children } : {}),
52
50
  },
53
51
  };
@@ -88,6 +86,18 @@ function heading3(text, options) {
88
86
  },
89
87
  };
90
88
  }
89
+ function heading4(text, options) {
90
+ return {
91
+ object: 'block',
92
+ type: 'heading_4',
93
+ heading_4: {
94
+ rich_text: resolveRichText(text),
95
+ color: options?.color ?? 'default',
96
+ is_toggleable: options?.isToggleable ?? false,
97
+ ...(options?.children ? { children: options.children } : {}),
98
+ },
99
+ };
100
+ }
91
101
  function bulletedListItem(text, options) {
92
102
  return {
93
103
  object: 'block',
@@ -170,6 +180,14 @@ function template(text, options) {
170
180
  },
171
181
  };
172
182
  }
183
+ /**
184
+ * @deprecated Meeting-notes blocks are server-managed -- their real shape (`title`,
185
+ * `status`, and child block IDs for the summary/notes/transcript) is populated by
186
+ * Notion, not hand-constructed by clients. This helper's `rich_text`-based output no
187
+ * longer matches `blockSchema`'s `meeting_notes` shape and is not a valid way to
188
+ * create a meeting-notes block. Kept only to avoid an abrupt removal from the helper
189
+ * surface; do not use for new code.
190
+ */
173
191
  function meetingNotes(text, options) {
174
192
  return {
175
193
  object: 'block',
@@ -348,6 +366,28 @@ function column(children) {
348
366
  column: { children },
349
367
  };
350
368
  }
369
+ /**
370
+ * Create a tab block. Only `paragraph` blocks may be direct children of a tab block --
371
+ * each tab is modeled as one paragraph, whose rich text is the tab label and whose
372
+ * `children` hold the tab's content.
373
+ *
374
+ * @example
375
+ * ```ts
376
+ * block.tab([
377
+ * { label: 'Overview', children: [block.paragraph('Intro text')] },
378
+ * { label: 'Details', icon: icon.emoji('📋'), children: [block.paragraph('More info')] },
379
+ * ])
380
+ * ```
381
+ */
382
+ function tab(tabs) {
383
+ return {
384
+ object: 'block',
385
+ type: 'tab',
386
+ tab: {
387
+ children: tabs.map((item) => paragraph(item.label, { color: item.color, icon: item.icon, children: item.children })),
388
+ },
389
+ };
390
+ }
351
391
  // ---------------------------------------------------------------------------
352
392
  // Synced blocks
353
393
  // ---------------------------------------------------------------------------
@@ -406,6 +446,7 @@ exports.block = {
406
446
  heading1,
407
447
  heading2,
408
448
  heading3,
449
+ heading4,
409
450
  bulletedListItem,
410
451
  numberedListItem,
411
452
  toDo,
@@ -435,6 +476,7 @@ exports.block = {
435
476
  tableRow,
436
477
  columnList,
437
478
  column,
479
+ tab,
438
480
  // Synced
439
481
  syncedBlock,
440
482
  };
@@ -1,3 +1,4 @@
1
+ import type { NativeIconColor } from '../schemas/icon.schema';
1
2
  interface EmojiIcon {
2
3
  type: 'emoji';
3
4
  emoji: string;
@@ -14,6 +15,19 @@ interface FileUploadRef {
14
15
  id: string;
15
16
  };
16
17
  }
18
+ interface NativeIconRef {
19
+ type: 'icon';
20
+ icon: {
21
+ name: string;
22
+ color?: NativeIconColor;
23
+ };
24
+ }
25
+ interface CustomEmojiIconRef {
26
+ type: 'custom_emoji';
27
+ custom_emoji: {
28
+ id: string;
29
+ };
30
+ }
17
31
  /**
18
32
  * Create an emoji icon object.
19
33
  *
@@ -42,7 +56,26 @@ declare function externalIcon(url: string): ExternalRef;
42
56
  */
43
57
  declare function fileUploadIcon(id: string): FileUploadRef;
44
58
  /**
45
- * Helpers for constructing icon objects (emoji, external URL, or file upload).
59
+ * Create a native (icon-picker) icon object.
60
+ *
61
+ * @example
62
+ * ```ts
63
+ * icon.native('star circle', 'blue')
64
+ * ```
65
+ */
66
+ declare function nativeIcon(name: string, color?: NativeIconColor): NativeIconRef;
67
+ /**
68
+ * Create a custom emoji icon object, referencing a workspace custom emoji by id.
69
+ *
70
+ * @example
71
+ * ```ts
72
+ * icon.customEmoji('emoji-id')
73
+ * ```
74
+ */
75
+ declare function customEmojiIcon(id: string): CustomEmojiIconRef;
76
+ /**
77
+ * Helpers for constructing icon objects (emoji, external URL, file upload,
78
+ * native icon, or custom emoji).
46
79
  *
47
80
  * @example
48
81
  * ```ts
@@ -59,6 +92,8 @@ export declare const icon: {
59
92
  emoji: typeof emojiIcon;
60
93
  external: typeof externalIcon;
61
94
  fileUpload: typeof fileUploadIcon;
95
+ native: typeof nativeIcon;
96
+ customEmoji: typeof customEmojiIcon;
62
97
  };
63
98
  /**
64
99
  * Create an external cover image object.
@@ -1,7 +1,4 @@
1
1
  "use strict";
2
- // ---------------------------------------------------------------------------
3
- // Return types
4
- // ---------------------------------------------------------------------------
5
2
  Object.defineProperty(exports, "__esModule", { value: true });
6
3
  exports.notionFile = exports.cover = exports.icon = void 0;
7
4
  // ---------------------------------------------------------------------------
@@ -41,7 +38,30 @@ function fileUploadIcon(id) {
41
38
  return { type: 'file_upload', file_upload: { id } };
42
39
  }
43
40
  /**
44
- * Helpers for constructing icon objects (emoji, external URL, or file upload).
41
+ * Create a native (icon-picker) icon object.
42
+ *
43
+ * @example
44
+ * ```ts
45
+ * icon.native('star circle', 'blue')
46
+ * ```
47
+ */
48
+ function nativeIcon(name, color) {
49
+ return { type: 'icon', icon: { name, ...(color ? { color } : {}) } };
50
+ }
51
+ /**
52
+ * Create a custom emoji icon object, referencing a workspace custom emoji by id.
53
+ *
54
+ * @example
55
+ * ```ts
56
+ * icon.customEmoji('emoji-id')
57
+ * ```
58
+ */
59
+ function customEmojiIcon(id) {
60
+ return { type: 'custom_emoji', custom_emoji: { id } };
61
+ }
62
+ /**
63
+ * Helpers for constructing icon objects (emoji, external URL, file upload,
64
+ * native icon, or custom emoji).
45
65
  *
46
66
  * @example
47
67
  * ```ts
@@ -58,6 +78,8 @@ exports.icon = {
58
78
  emoji: emojiIcon,
59
79
  external: externalIcon,
60
80
  fileUpload: fileUploadIcon,
81
+ native: nativeIcon,
82
+ customEmoji: customEmojiIcon,
61
83
  };
62
84
  // ---------------------------------------------------------------------------
63
85
  // Cover helpers
@@ -38,8 +38,8 @@ declare class CheckboxFilter {
38
38
  declare class SelectFilter {
39
39
  private readonly property;
40
40
  constructor(property: string);
41
- equals(value: string): FilterCondition;
42
- doesNotEqual(value: string): FilterCondition;
41
+ equals(value: string | string[]): FilterCondition;
42
+ doesNotEqual(value: string | string[]): FilterCondition;
43
43
  isEmpty(): FilterCondition;
44
44
  isNotEmpty(): FilterCondition;
45
45
  }
@@ -47,8 +47,8 @@ declare class SelectFilter {
47
47
  declare class MultiSelectFilter {
48
48
  private readonly property;
49
49
  constructor(property: string);
50
- contains(value: string): FilterCondition;
51
- doesNotContain(value: string): FilterCondition;
50
+ contains(value: string | string[]): FilterCondition;
51
+ doesNotContain(value: string | string[]): FilterCondition;
52
52
  isEmpty(): FilterCondition;
53
53
  isNotEmpty(): FilterCondition;
54
54
  }
@@ -56,8 +56,8 @@ declare class MultiSelectFilter {
56
56
  declare class StatusFilter {
57
57
  private readonly property;
58
58
  constructor(property: string);
59
- equals(value: string): FilterCondition;
60
- doesNotEqual(value: string): FilterCondition;
59
+ equals(value: string | string[]): FilterCondition;
60
+ doesNotEqual(value: string | string[]): FilterCondition;
61
61
  isEmpty(): FilterCondition;
62
62
  isNotEmpty(): FilterCondition;
63
63
  }
@@ -2,9 +2,10 @@ export { block } from './block.helpers';
2
2
  export type { RichTextInput } from './block.helpers';
3
3
  export { cover, icon, notionFile } from './file.helpers';
4
4
  export { filter } from './filter.helpers';
5
- export { paginate, paginateIterator, paginateWithMetadata } from './pagination.helpers';
6
- export type { PaginatedFetchFunction } from './pagination.helpers';
5
+ export { collectAllDataSourceRows, iterateAllDataSourceRows, paginate, paginateIterator, paginateWithMetadata, } from './pagination.helpers';
6
+ export type { PaginatedFetchFunction, WindowedFetchFunction } from './pagination.helpers';
7
7
  export { parent } from './parent.helpers';
8
8
  export { prop } from './property.helpers';
9
9
  export { RichTextBuilder, richText } from './richText.helpers';
10
10
  export { sort } from './sort.helpers';
11
+ export { webhook } from './webhook.helpers';
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.sort = exports.richText = exports.RichTextBuilder = exports.prop = exports.parent = exports.paginateWithMetadata = exports.paginateIterator = exports.paginate = exports.filter = exports.notionFile = exports.icon = exports.cover = exports.block = void 0;
3
+ exports.webhook = exports.sort = exports.richText = exports.RichTextBuilder = exports.prop = exports.parent = exports.paginateWithMetadata = exports.paginateIterator = exports.paginate = exports.iterateAllDataSourceRows = exports.collectAllDataSourceRows = exports.filter = exports.notionFile = exports.icon = exports.cover = exports.block = void 0;
4
4
  var block_helpers_1 = require("./block.helpers");
5
5
  Object.defineProperty(exports, "block", { enumerable: true, get: function () { return block_helpers_1.block; } });
6
6
  var file_helpers_1 = require("./file.helpers");
@@ -10,6 +10,8 @@ Object.defineProperty(exports, "notionFile", { enumerable: true, get: function (
10
10
  var filter_helpers_1 = require("./filter.helpers");
11
11
  Object.defineProperty(exports, "filter", { enumerable: true, get: function () { return filter_helpers_1.filter; } });
12
12
  var pagination_helpers_1 = require("./pagination.helpers");
13
+ Object.defineProperty(exports, "collectAllDataSourceRows", { enumerable: true, get: function () { return pagination_helpers_1.collectAllDataSourceRows; } });
14
+ Object.defineProperty(exports, "iterateAllDataSourceRows", { enumerable: true, get: function () { return pagination_helpers_1.iterateAllDataSourceRows; } });
13
15
  Object.defineProperty(exports, "paginate", { enumerable: true, get: function () { return pagination_helpers_1.paginate; } });
14
16
  Object.defineProperty(exports, "paginateIterator", { enumerable: true, get: function () { return pagination_helpers_1.paginateIterator; } });
15
17
  Object.defineProperty(exports, "paginateWithMetadata", { enumerable: true, get: function () { return pagination_helpers_1.paginateWithMetadata; } });
@@ -22,3 +24,5 @@ Object.defineProperty(exports, "RichTextBuilder", { enumerable: true, get: funct
22
24
  Object.defineProperty(exports, "richText", { enumerable: true, get: function () { return richText_helpers_1.richText; } });
23
25
  var sort_helpers_1 = require("./sort.helpers");
24
26
  Object.defineProperty(exports, "sort", { enumerable: true, get: function () { return sort_helpers_1.sort; } });
27
+ var webhook_helpers_1 = require("./webhook.helpers");
28
+ Object.defineProperty(exports, "webhook", { enumerable: true, get: function () { return webhook_helpers_1.webhook; } });
@@ -149,3 +149,59 @@ export declare function paginateWithMetadata<T>(fetchPage: PaginatedFetchFunctio
149
149
  pageCount: number;
150
150
  totalCount: number;
151
151
  }>;
152
+ /**
153
+ * Fetch function for windowed row collection. Same as {@link PaginatedFetchFunction} plus
154
+ * a second parameter carrying the ISO 8601 `created_time` lower bound for the *next*
155
+ * window, present once the previous window was capped at the query result limit
156
+ * (`request_status.type === 'incomplete'`). The caller merges this into their own
157
+ * created_time-ascending query, e.g. `filter.createdTime('Created time').onOrAfter(createdTimeCursor)`.
158
+ */
159
+ export type WindowedFetchFunction<T extends {
160
+ id: string;
161
+ createdTime: Date;
162
+ }> = (cursor: string | undefined, createdTimeCursor: string | undefined) => Promise<PaginatedList<T>>;
163
+ /**
164
+ * Iterates over every row of a data source query, transparently working around the
165
+ * API's 10,000-result-per-query cap.
166
+ *
167
+ * Data source (and view) queries cap at 10,000 results; when capped, the response's
168
+ * `has_more` is still `false`, so following `next_cursor`/`has_more` alone silently
169
+ * truncates the result set. When a window is capped (`request_status.type ===
170
+ * 'incomplete'`), this starts a new query filtered to `created_time >=` the last row
171
+ * seen, de-duplicating by `id` across the window boundary. Requires the query to be
172
+ * sorted by `created_time` ascending.
173
+ *
174
+ * @param fetchWindow - Function that fetches one page, given the current cursor and
175
+ * (once a window has been capped) a `created_time` lower bound for the next window
176
+ * @yields Individual rows across all windows
177
+ *
178
+ * @example
179
+ * ```typescript
180
+ * const rows = collectAllDataSourceRows((cursor, createdTimeCursor) =>
181
+ * notion.dataSources.query(dataSourceId, {
182
+ * start_cursor: cursor,
183
+ * sorts: [{ timestamp: 'created_time', direction: 'ascending' }],
184
+ * filter: createdTimeCursor
185
+ * ? filter.and(baseFilter, filter.createdTime('Created time').onOrAfter(createdTimeCursor))
186
+ * : baseFilter,
187
+ * }),
188
+ * );
189
+ * ```
190
+ */
191
+ export declare function iterateAllDataSourceRows<T extends {
192
+ id: string;
193
+ createdTime: Date;
194
+ }>(fetchWindow: WindowedFetchFunction<T>): AsyncGenerator<T, void, undefined>;
195
+ /**
196
+ * Collects every row of a data source query into an array, transparently working
197
+ * around the API's 10,000-result-per-query cap. See {@link iterateAllDataSourceRows}
198
+ * for the windowing behavior.
199
+ *
200
+ * @param fetchWindow - Function that fetches one page, given the current cursor and
201
+ * (once a window has been capped) a `created_time` lower bound for the next window
202
+ * @returns Array containing all rows across all windows
203
+ */
204
+ export declare function collectAllDataSourceRows<T extends {
205
+ id: string;
206
+ createdTime: Date;
207
+ }>(fetchWindow: WindowedFetchFunction<T>): Promise<T[]>;
@@ -3,6 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.paginate = paginate;
4
4
  exports.paginateIterator = paginateIterator;
5
5
  exports.paginateWithMetadata = paginateWithMetadata;
6
+ exports.iterateAllDataSourceRows = iterateAllDataSourceRows;
7
+ exports.collectAllDataSourceRows = collectAllDataSourceRows;
6
8
  /**
7
9
  * Collects all results from a paginated endpoint by automatically following cursors.
8
10
  *
@@ -143,3 +145,87 @@ async function paginateWithMetadata(fetchPage) {
143
145
  totalCount: items.length,
144
146
  };
145
147
  }
148
+ /**
149
+ * Iterates over every row of a data source query, transparently working around the
150
+ * API's 10,000-result-per-query cap.
151
+ *
152
+ * Data source (and view) queries cap at 10,000 results; when capped, the response's
153
+ * `has_more` is still `false`, so following `next_cursor`/`has_more` alone silently
154
+ * truncates the result set. When a window is capped (`request_status.type ===
155
+ * 'incomplete'`), this starts a new query filtered to `created_time >=` the last row
156
+ * seen, de-duplicating by `id` across the window boundary. Requires the query to be
157
+ * sorted by `created_time` ascending.
158
+ *
159
+ * @param fetchWindow - Function that fetches one page, given the current cursor and
160
+ * (once a window has been capped) a `created_time` lower bound for the next window
161
+ * @yields Individual rows across all windows
162
+ *
163
+ * @example
164
+ * ```typescript
165
+ * const rows = collectAllDataSourceRows((cursor, createdTimeCursor) =>
166
+ * notion.dataSources.query(dataSourceId, {
167
+ * start_cursor: cursor,
168
+ * sorts: [{ timestamp: 'created_time', direction: 'ascending' }],
169
+ * filter: createdTimeCursor
170
+ * ? filter.and(baseFilter, filter.createdTime('Created time').onOrAfter(createdTimeCursor))
171
+ * : baseFilter,
172
+ * }),
173
+ * );
174
+ * ```
175
+ */
176
+ async function* iterateAllDataSourceRows(fetchWindow) {
177
+ const seenIds = new Set();
178
+ let cursor;
179
+ let createdTimeCursor;
180
+ let lastCreatedTime;
181
+ let hasMore = true;
182
+ while (hasMore) {
183
+ const response = await fetchWindow(cursor, createdTimeCursor);
184
+ let yieldedThisWindow = false;
185
+ for (const item of response.results) {
186
+ if (seenIds.has(item.id)) {
187
+ continue;
188
+ }
189
+ seenIds.add(item.id);
190
+ lastCreatedTime = item.createdTime;
191
+ yieldedThisWindow = true;
192
+ yield item;
193
+ }
194
+ if (response.request_status?.type === 'incomplete' && lastCreatedTime) {
195
+ // has_more is false here, so normal cursor pagination would silently stop short.
196
+ // Start a fresh window scoped to created_time >= the last row seen, de-duping by
197
+ // id across the boundary (created_time, not last_edited_time -- the latter shifts
198
+ // rows between windows).
199
+ if (!yieldedThisWindow) {
200
+ // Every row in this capped window was already seen, meaning more rows share
201
+ // this exact created_time than fit in one window -- the created_time >= filter
202
+ // can't disambiguate further, so re-querying would return the same page forever.
203
+ throw new Error(`More than one window's worth of rows share the same created_time (${lastCreatedTime.toISOString()}); ` +
204
+ 'iterateAllDataSourceRows cannot make forward progress past this timestamp.');
205
+ }
206
+ cursor = undefined;
207
+ createdTimeCursor = lastCreatedTime.toISOString();
208
+ hasMore = true;
209
+ }
210
+ else {
211
+ cursor = response.next_cursor ?? undefined;
212
+ hasMore = cursor !== undefined;
213
+ }
214
+ }
215
+ }
216
+ /**
217
+ * Collects every row of a data source query into an array, transparently working
218
+ * around the API's 10,000-result-per-query cap. See {@link iterateAllDataSourceRows}
219
+ * for the windowing behavior.
220
+ *
221
+ * @param fetchWindow - Function that fetches one page, given the current cursor and
222
+ * (once a window has been capped) a `created_time` lower bound for the next window
223
+ * @returns Array containing all rows across all windows
224
+ */
225
+ async function collectAllDataSourceRows(fetchWindow) {
226
+ const all = [];
227
+ for await (const item of iterateAllDataSourceRows(fetchWindow)) {
228
+ all.push(item);
229
+ }
230
+ return all;
231
+ }
@@ -181,6 +181,34 @@ declare function people(userIds: string[]): {
181
181
  id: string;
182
182
  }>;
183
183
  };
184
+ /** Options for the verification property's optional date. */
185
+ interface VerificationDateOptions {
186
+ start: string;
187
+ end?: string | null;
188
+ timeZone?: string | null;
189
+ }
190
+ /** The shape of a verification property value. */
191
+ interface VerificationValue {
192
+ verification: {
193
+ state: 'verified' | 'unverified';
194
+ date?: {
195
+ start: string;
196
+ end: string | null;
197
+ time_zone: string | null;
198
+ } | null;
199
+ };
200
+ }
201
+ /**
202
+ * Create a verification property value.
203
+ *
204
+ * @example
205
+ * ```ts
206
+ * prop.verification('verified')
207
+ * prop.verification('verified', { start: '2025-01-15' })
208
+ * prop.verification('unverified')
209
+ * ```
210
+ */
211
+ declare function verification(state: 'verified' | 'unverified', date?: VerificationDateOptions): VerificationValue;
184
212
  /** A single file entry for the files' property. */
185
213
  interface FileEntry {
186
214
  name: string;
@@ -243,5 +271,6 @@ export declare const prop: {
243
271
  relation: typeof relation;
244
272
  people: typeof people;
245
273
  files: typeof files;
274
+ verification: typeof verification;
246
275
  };
247
276
  export {};
@@ -210,6 +210,32 @@ function people(userIds) {
210
210
  (0, validation_1.validateArrayLength)(userIds, validation_1.LIMITS.PEOPLE, 'People');
211
211
  return { people: userIds.map((id) => ({ object: 'user', id })) };
212
212
  }
213
+ /**
214
+ * Create a verification property value.
215
+ *
216
+ * @example
217
+ * ```ts
218
+ * prop.verification('verified')
219
+ * prop.verification('verified', { start: '2025-01-15' })
220
+ * prop.verification('unverified')
221
+ * ```
222
+ */
223
+ function verification(state, date) {
224
+ return {
225
+ verification: {
226
+ state,
227
+ ...(date
228
+ ? {
229
+ date: {
230
+ start: date.start,
231
+ end: date.end ?? null,
232
+ time_zone: date.timeZone ?? null,
233
+ },
234
+ }
235
+ : {}),
236
+ },
237
+ };
238
+ }
213
239
  /**
214
240
  * Create a files property value.
215
241
  *
@@ -268,4 +294,5 @@ exports.prop = {
268
294
  relation,
269
295
  people,
270
296
  files,
297
+ verification,
271
298
  };
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Sign a payload, for generating test webhook signatures.
3
+ *
4
+ * @param body - The payload to sign. A string is signed as-is; anything else is
5
+ * `JSON.stringify`'d first -- safe here because the caller controls the exact
6
+ * serialization of a payload they're constructing themselves.
7
+ * @param verificationToken - The webhook subscription's verification token
8
+ * @returns The `sha256=<hex digest>` signature
9
+ *
10
+ * @example
11
+ * ```ts
12
+ * const signature = webhook.sign({ event: 'page.updated' }, verificationToken);
13
+ * ```
14
+ */
15
+ declare function sign(body: unknown, verificationToken: string): string;
16
+ /**
17
+ * Verify an incoming webhook's `X-Notion-Signature` header via constant-time comparison.
18
+ *
19
+ * IMPORTANT: `rawBody` must be the exact raw request body Notion sent, not
20
+ * `JSON.stringify(parsedBody)` -- re-serializing a parsed object can produce a
21
+ * different byte sequence (key order, whitespace) than what was actually signed,
22
+ * silently breaking verification. Use your framework's raw-body access (e.g.
23
+ * Express's `express.raw()` / `req.rawBody`), not `req.body` after JSON middleware
24
+ * has already parsed it.
25
+ *
26
+ * @param rawBody - The exact raw request body string Notion sent
27
+ * @param signatureHeader - The value of the `X-Notion-Signature` request header
28
+ * @param verificationToken - The webhook subscription's verification token
29
+ * @returns `true` if the signature is valid; `false` for any mismatch or malformed
30
+ * input (never throws)
31
+ *
32
+ * @example
33
+ * ```ts
34
+ * const isValid = webhook.verifySignature(rawBody, req.headers['x-notion-signature'], verificationToken);
35
+ * ```
36
+ */
37
+ declare function verifySignature(rawBody: string, signatureHeader: string | null | undefined, verificationToken: string): boolean;
38
+ /**
39
+ * Helpers for signing and verifying Notion webhook payloads.
40
+ *
41
+ * @example
42
+ * ```ts
43
+ * import { webhook } from '@visus-io/notion-sdk-ts';
44
+ *
45
+ * const isValid = webhook.verifySignature(rawBody, req.headers['x-notion-signature'], verificationToken);
46
+ * ```
47
+ */
48
+ export declare const webhook: {
49
+ sign: typeof sign;
50
+ verifySignature: typeof verifySignature;
51
+ };
52
+ export {};
@@ -0,0 +1,81 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.webhook = void 0;
4
+ const node_crypto_1 = require("node:crypto");
5
+ /**
6
+ * Webhook signature helpers.
7
+ *
8
+ * Notion signs webhook payloads with HMAC-SHA256, keyed by the subscription's
9
+ * verification token, over the raw JSON request body. The signature is sent in the
10
+ * `X-Notion-Signature` header as `sha256=<hex digest>`.
11
+ *
12
+ * Notion API reference:
13
+ * https://developers.notion.com/reference/webhooks
14
+ */
15
+ function computeSignature(rawBody, verificationToken) {
16
+ return `sha256=${(0, node_crypto_1.createHmac)('sha256', verificationToken).update(rawBody).digest('hex')}`;
17
+ }
18
+ /**
19
+ * Sign a payload, for generating test webhook signatures.
20
+ *
21
+ * @param body - The payload to sign. A string is signed as-is; anything else is
22
+ * `JSON.stringify`'d first -- safe here because the caller controls the exact
23
+ * serialization of a payload they're constructing themselves.
24
+ * @param verificationToken - The webhook subscription's verification token
25
+ * @returns The `sha256=<hex digest>` signature
26
+ *
27
+ * @example
28
+ * ```ts
29
+ * const signature = webhook.sign({ event: 'page.updated' }, verificationToken);
30
+ * ```
31
+ */
32
+ function sign(body, verificationToken) {
33
+ return computeSignature(typeof body === 'string' ? body : JSON.stringify(body), verificationToken);
34
+ }
35
+ /**
36
+ * Verify an incoming webhook's `X-Notion-Signature` header via constant-time comparison.
37
+ *
38
+ * IMPORTANT: `rawBody` must be the exact raw request body Notion sent, not
39
+ * `JSON.stringify(parsedBody)` -- re-serializing a parsed object can produce a
40
+ * different byte sequence (key order, whitespace) than what was actually signed,
41
+ * silently breaking verification. Use your framework's raw-body access (e.g.
42
+ * Express's `express.raw()` / `req.rawBody`), not `req.body` after JSON middleware
43
+ * has already parsed it.
44
+ *
45
+ * @param rawBody - The exact raw request body string Notion sent
46
+ * @param signatureHeader - The value of the `X-Notion-Signature` request header
47
+ * @param verificationToken - The webhook subscription's verification token
48
+ * @returns `true` if the signature is valid; `false` for any mismatch or malformed
49
+ * input (never throws)
50
+ *
51
+ * @example
52
+ * ```ts
53
+ * const isValid = webhook.verifySignature(rawBody, req.headers['x-notion-signature'], verificationToken);
54
+ * ```
55
+ */
56
+ function verifySignature(rawBody, signatureHeader, verificationToken) {
57
+ if (!signatureHeader) {
58
+ return false;
59
+ }
60
+ const expected = Buffer.from(computeSignature(rawBody, verificationToken));
61
+ const actual = Buffer.from(signatureHeader);
62
+ // timingSafeEqual throws on mismatched buffer lengths, so guard explicitly.
63
+ if (expected.length !== actual.length) {
64
+ return false;
65
+ }
66
+ return (0, node_crypto_1.timingSafeEqual)(expected, actual);
67
+ }
68
+ /**
69
+ * Helpers for signing and verifying Notion webhook payloads.
70
+ *
71
+ * @example
72
+ * ```ts
73
+ * import { webhook } from '@visus-io/notion-sdk-ts';
74
+ *
75
+ * const isValid = webhook.verifySignature(rawBody, req.headers['x-notion-signature'], verificationToken);
76
+ * ```
77
+ */
78
+ exports.webhook = {
79
+ sign,
80
+ verifySignature,
81
+ };