@visus-io/notion-sdk-ts 3.2.0 → 3.2.1

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 (39) hide show
  1. package/dist/api/blocks.api.d.ts +1083 -224
  2. package/dist/api/blocks.api.js +4 -0
  3. package/dist/api/comments.api.d.ts +19 -1
  4. package/dist/api/comments.api.js +1 -1
  5. package/dist/api/dataSources.api.d.ts +36 -0
  6. package/dist/api/databases.api.d.ts +36 -0
  7. package/dist/api/databases.api.js +1 -0
  8. package/dist/api/fileUploads.api.d.ts +17 -7
  9. package/dist/api/fileUploads.api.js +30 -16
  10. package/dist/api/pages.api.d.ts +46 -0
  11. package/dist/client.d.ts +33 -2
  12. package/dist/client.js +88 -21
  13. package/dist/helpers/filter.helpers.d.ts +11 -4
  14. package/dist/helpers/filter.helpers.js +73 -30
  15. package/dist/helpers/pagination.helpers.d.ts +7 -2
  16. package/dist/helpers/pagination.helpers.js +26 -5
  17. package/dist/models/page.model.d.ts +20 -1
  18. package/dist/models/page.model.js +33 -2
  19. package/dist/schemas/block.schema.d.ts +1082 -224
  20. package/dist/schemas/block.schema.js +40 -25
  21. package/dist/schemas/comment.schema.d.ts +18 -0
  22. package/dist/schemas/dataSource.schema.d.ts +36 -0
  23. package/dist/schemas/database.schema.d.ts +36 -0
  24. package/dist/schemas/meetingNotesQuery.schema.d.ts +1082 -224
  25. package/dist/schemas/page.schema.d.ts +46 -0
  26. package/dist/schemas/page.schema.js +2 -1
  27. package/dist/schemas/pageMarkdown.schema.js +1 -1
  28. package/dist/schemas/pageProperties.schema.d.ts +99 -1
  29. package/dist/schemas/pageProperties.schema.js +16 -1
  30. package/dist/schemas/pagination.schema.d.ts +1 -1
  31. package/dist/schemas/propertyObjects.schema.js +2 -1
  32. package/dist/schemas/richText.schema.d.ts +36 -0
  33. package/dist/schemas/richText.schema.js +21 -0
  34. package/dist/schemas/shared.schema.d.ts +3 -7
  35. package/dist/schemas/shared.schema.js +5 -9
  36. package/dist/schemas/view.schema.d.ts +46 -0
  37. package/dist/validation.d.ts +8 -0
  38. package/dist/validation.js +16 -0
  39. package/package.json +1 -1
package/dist/client.js CHANGED
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.NotionClient = exports.NOTION_VERSION = void 0;
4
4
  const errors_1 = require("./errors");
5
+ const validation_1 = require("./validation");
5
6
  /**
6
7
  * The Notion API version this SDK uses.
7
8
  * All schemas, request bodies, helpers, and models depend on this version.
@@ -9,6 +10,8 @@ const errors_1 = require("./errors");
9
10
  * @category Client & Core
10
11
  */
11
12
  exports.NOTION_VERSION = '2026-03-11';
13
+ /** Upper bound for a single retry wait, in milliseconds. */
14
+ const MAX_RETRY_DELAY_MS = 60_000;
12
15
  /**
13
16
  * Base HTTP client for Notion API requests.
14
17
  */
@@ -36,10 +39,10 @@ class NotionClient {
36
39
  return await this.makeRequest(options);
37
40
  }
38
41
  catch (error) {
39
- // Retry rate-limited requests (if enabled) and service-overload (529)
40
- // responses, which the API recommends always retrying.
42
+ // Retry per isRetryable(); retryOnRateLimit:false suppresses only rate_limited.
41
43
  if (error instanceof errors_1.NotionAPIError &&
42
- ((error.isRateLimited() && this.retryOnRateLimit) || error.isServiceOverloaded()) &&
44
+ error.isRetryable() &&
45
+ !(error.isRateLimited() && !this.retryOnRateLimit) &&
43
46
  attempt < this.maxRetries) {
44
47
  // Prefer the server-supplied Retry-After value; fall back to
45
48
  // exponential backoff when the header is absent.
@@ -55,21 +58,46 @@ class NotionClient {
55
58
  // If we exhausted all retries, throw the last error
56
59
  throw lastError ?? new Error('Request failed after all retries');
57
60
  }
61
+ /**
62
+ * Send a `multipart/form-data` `POST` to a Notion file-upload URL. Reuses the
63
+ * configured `fetch` and the request timeout. Does not retry. Does not set
64
+ * `Content-Type`, so `fetch` adds the multipart boundary.
65
+ *
66
+ * @param uploadUrl - The absolute `upload_url` from `fileUploads.initiate()`.
67
+ * @param form - A `FormData` body with the file bytes under the `file` key.
68
+ * @throws {NotionValidationError} If `uploadUrl` is not a valid `https` URL.
69
+ * @throws {NotionAPIError} If the endpoint returns an error response.
70
+ * @throws {NotionRequestTimeoutError} If the request exceeds the timeout.
71
+ * @throws {NotionNetworkError} If a network problem blocks the request.
72
+ */
73
+ async sendFileUpload(uploadUrl, form) {
74
+ this.assertHttpsUrl(uploadUrl);
75
+ const { 'Content-Type': _contentType, ...headers } = this.requestHeaders;
76
+ try {
77
+ const response = await this.fetchWithTimeout(uploadUrl, {
78
+ method: 'POST',
79
+ headers,
80
+ body: form,
81
+ });
82
+ if (!response.ok) {
83
+ await this.handleErrorResponse(response);
84
+ }
85
+ }
86
+ catch (error) {
87
+ this.mapTransportError(error);
88
+ }
89
+ }
58
90
  /**
59
91
  * Send one HTTP request to the Notion API.
60
92
  */
61
93
  async makeRequest(options) {
62
94
  const url = this.buildUrl(options.path, options.query);
63
- const controller = new AbortController();
64
- const timeoutId = setTimeout(() => controller.abort(), this.timeoutMs);
65
95
  try {
66
- const response = await this.fetchImpl(url, {
96
+ const response = await this.fetchWithTimeout(url, {
67
97
  method: options.method,
68
98
  headers: this.requestHeaders,
69
99
  body: options.body ? JSON.stringify(options.body) : undefined,
70
- signal: controller.signal,
71
100
  });
72
- clearTimeout(timeoutId);
73
101
  if (!response.ok) {
74
102
  await this.handleErrorResponse(response);
75
103
  }
@@ -81,18 +109,56 @@ class NotionClient {
81
109
  return data;
82
110
  }
83
111
  catch (error) {
112
+ this.mapTransportError(error);
113
+ }
114
+ }
115
+ /**
116
+ * Call the configured `fetch` with an abort timeout of `timeoutMs`.
117
+ * Clear the timer as soon as the response headers arrive or the request fails.
118
+ */
119
+ async fetchWithTimeout(url, init) {
120
+ const controller = new AbortController();
121
+ const timeoutId = setTimeout(() => controller.abort(), this.timeoutMs);
122
+ try {
123
+ return await this.fetchImpl(url, { ...init, signal: controller.signal });
124
+ }
125
+ finally {
84
126
  clearTimeout(timeoutId);
85
- if (error instanceof errors_1.NotionAPIError) {
86
- throw error;
87
- }
88
- if (error instanceof Error) {
89
- if (error.name === 'AbortError') {
90
- throw new errors_1.NotionRequestTimeoutError(`Request timed out after ${this.timeoutMs}ms`);
91
- }
92
- throw new errors_1.NotionNetworkError('Network request failed', error);
93
- }
127
+ }
128
+ }
129
+ /**
130
+ * Map a transport-layer failure to the matching SDK error and throw it.
131
+ * Rethrow a `NotionAPIError` unchanged. Map an `AbortError` to
132
+ * `NotionRequestTimeoutError`. Map any other `Error` to `NotionNetworkError`.
133
+ */
134
+ mapTransportError(error) {
135
+ if (error instanceof errors_1.NotionAPIError) {
94
136
  throw error;
95
137
  }
138
+ if (error instanceof Error) {
139
+ if (error.name === 'AbortError') {
140
+ throw new errors_1.NotionRequestTimeoutError(`Request timed out after ${this.timeoutMs}ms`);
141
+ }
142
+ throw new errors_1.NotionNetworkError('Network request failed', error);
143
+ }
144
+ throw error;
145
+ }
146
+ /**
147
+ * Assert that `url` is a valid `https` URL. The SDK sends the API token in the
148
+ * `Authorization` header of a file upload. Reject a plaintext `http` URL so the
149
+ * token never travels unencrypted.
150
+ */
151
+ assertHttpsUrl(url) {
152
+ let protocol;
153
+ try {
154
+ protocol = new URL(url).protocol;
155
+ }
156
+ catch {
157
+ throw new validation_1.NotionValidationError(`Invalid file upload URL: ${url}`);
158
+ }
159
+ if (protocol !== 'https:') {
160
+ throw new validation_1.NotionValidationError(`File upload URL must use https, got "${protocol}"`);
161
+ }
96
162
  }
97
163
  /**
98
164
  * Calculate a fallback retry delay with exponential backoff.
@@ -101,7 +167,7 @@ class NotionClient {
101
167
  */
102
168
  getRetryAfter(attempt) {
103
169
  const backoffMs = Math.pow(2, attempt) * 1000;
104
- return Math.min(backoffMs, 60000); // Cap at 60 seconds
170
+ return Math.min(backoffMs, MAX_RETRY_DELAY_MS);
105
171
  }
106
172
  /**
107
173
  * Pause for the given duration, in milliseconds.
@@ -130,8 +196,9 @@ class NotionClient {
130
196
  return url.toString();
131
197
  }
132
198
  /**
133
- * Parse the `Retry-After` response header into milliseconds.
134
- * Return `undefined` if the header is missing or not a valid non-negative number.
199
+ * Parse the `Retry-After` response header into milliseconds, clamped to
200
+ * {@link MAX_RETRY_DELAY_MS}. Return `undefined` if the header is missing or
201
+ * not a valid non-negative number.
135
202
  */
136
203
  parseRetryAfterHeader(response) {
137
204
  const header = response.headers.get('Retry-After');
@@ -142,7 +209,7 @@ class NotionClient {
142
209
  if (!Number.isFinite(seconds) || seconds < 0) {
143
210
  return undefined;
144
211
  }
145
- return Math.ceil(seconds) * 1000;
212
+ return Math.min(Math.ceil(seconds) * 1000, MAX_RETRY_DELAY_MS);
146
213
  }
147
214
  /**
148
215
  * Handle an error response from the API.
@@ -4,7 +4,9 @@ type FilterCondition = Record<string, unknown>;
4
4
  declare class TextFilter {
5
5
  private readonly property;
6
6
  private readonly propertyType;
7
- constructor(property: string, propertyType: string);
7
+ private readonly isFormula;
8
+ constructor(property: string, propertyType: string, isFormula?: boolean);
9
+ private build;
8
10
  equals(value: string): FilterCondition;
9
11
  doesNotEqual(value: string): FilterCondition;
10
12
  contains(value: string): FilterCondition;
@@ -17,7 +19,9 @@ declare class TextFilter {
17
19
  /** Operators for number properties. */
18
20
  declare class NumberFilter {
19
21
  private readonly property;
20
- constructor(property: string);
22
+ private readonly isFormula;
23
+ constructor(property: string, isFormula?: boolean);
24
+ private build;
21
25
  equals(value: number): FilterCondition;
22
26
  doesNotEqual(value: number): FilterCondition;
23
27
  greaterThan(value: number): FilterCondition;
@@ -30,7 +34,9 @@ declare class NumberFilter {
30
34
  /** Operators for checkbox properties. */
31
35
  declare class CheckboxFilter {
32
36
  private readonly property;
33
- constructor(property: string);
37
+ private readonly isFormula;
38
+ constructor(property: string, isFormula?: boolean);
39
+ private build;
34
40
  equals(value: boolean): FilterCondition;
35
41
  doesNotEqual(value: boolean): FilterCondition;
36
42
  }
@@ -65,7 +71,8 @@ declare class StatusFilter {
65
71
  declare class DateFilter {
66
72
  private readonly key;
67
73
  private readonly isTimestamp;
68
- constructor(key: string, isTimestamp: boolean);
74
+ private readonly isFormula;
75
+ constructor(key: string, isTimestamp: boolean, isFormula?: boolean);
69
76
  private wrap;
70
77
  equals(value: string): FilterCondition;
71
78
  before(value: string): FilterCondition;
@@ -1,83 +1,117 @@
1
1
  "use strict";
2
- // ---------------------------------------------------------------------------
3
- // Filter value types
4
- // ---------------------------------------------------------------------------
5
2
  Object.defineProperty(exports, "__esModule", { value: true });
6
3
  exports.filter = void 0;
4
+ const validation_1 = require("../validation");
5
+ /**
6
+ * Assert that a select-like filter value is a single string. Notion rejects an
7
+ * array for the `equals`, `does_not_equal`, `contains`, and `does_not_contain`
8
+ * operators on `select`, `status`, and `multi_select` properties.
9
+ *
10
+ * The parameter type still accepts `string[]` for backward compatibility. An
11
+ * array now fails fast with a clear error instead of a generic API 400.
12
+ *
13
+ * @throws {NotionValidationError} If `value` is an array.
14
+ */
15
+ function assertScalarFilterValue(value, label) {
16
+ if (Array.isArray(value)) {
17
+ throw new validation_1.NotionValidationError(`${label} accepts a single string, not an array. Compose an OR filter with filter.or(...) to match multiple values.`);
18
+ }
19
+ }
7
20
  // ---------------------------------------------------------------------------
8
21
  // Property filter builders
9
22
  // ---------------------------------------------------------------------------
10
23
  /** Operators common to text-like properties (title, rich_text, url, email, phone_number). */
11
24
  class TextFilter {
12
- constructor(property, propertyType) {
25
+ constructor(property, propertyType, isFormula = false) {
13
26
  this.property = property;
14
27
  this.propertyType = propertyType;
28
+ this.isFormula = isFormula;
29
+ }
30
+ build(condition) {
31
+ if (this.isFormula) {
32
+ return { property: this.property, formula: { string: condition } };
33
+ }
34
+ return { property: this.property, [this.propertyType]: condition };
15
35
  }
16
36
  equals(value) {
17
- return { property: this.property, [this.propertyType]: { equals: value } };
37
+ return this.build({ equals: value });
18
38
  }
19
39
  doesNotEqual(value) {
20
- return { property: this.property, [this.propertyType]: { does_not_equal: value } };
40
+ return this.build({ does_not_equal: value });
21
41
  }
22
42
  contains(value) {
23
- return { property: this.property, [this.propertyType]: { contains: value } };
43
+ return this.build({ contains: value });
24
44
  }
25
45
  doesNotContain(value) {
26
- return { property: this.property, [this.propertyType]: { does_not_contain: value } };
46
+ return this.build({ does_not_contain: value });
27
47
  }
28
48
  startsWith(value) {
29
- return { property: this.property, [this.propertyType]: { starts_with: value } };
49
+ return this.build({ starts_with: value });
30
50
  }
31
51
  endsWith(value) {
32
- return { property: this.property, [this.propertyType]: { ends_with: value } };
52
+ return this.build({ ends_with: value });
33
53
  }
34
54
  isEmpty() {
35
- return { property: this.property, [this.propertyType]: { is_empty: true } };
55
+ return this.build({ is_empty: true });
36
56
  }
37
57
  isNotEmpty() {
38
- return { property: this.property, [this.propertyType]: { is_not_empty: true } };
58
+ return this.build({ is_not_empty: true });
39
59
  }
40
60
  }
41
61
  /** Operators for number properties. */
42
62
  class NumberFilter {
43
- constructor(property) {
63
+ constructor(property, isFormula = false) {
44
64
  this.property = property;
65
+ this.isFormula = isFormula;
66
+ }
67
+ build(condition) {
68
+ if (this.isFormula) {
69
+ return { property: this.property, formula: { number: condition } };
70
+ }
71
+ return { property: this.property, number: condition };
45
72
  }
46
73
  equals(value) {
47
- return { property: this.property, number: { equals: value } };
74
+ return this.build({ equals: value });
48
75
  }
49
76
  doesNotEqual(value) {
50
- return { property: this.property, number: { does_not_equal: value } };
77
+ return this.build({ does_not_equal: value });
51
78
  }
52
79
  greaterThan(value) {
53
- return { property: this.property, number: { greater_than: value } };
80
+ return this.build({ greater_than: value });
54
81
  }
55
82
  greaterThanOrEqualTo(value) {
56
- return { property: this.property, number: { greater_than_or_equal_to: value } };
83
+ return this.build({ greater_than_or_equal_to: value });
57
84
  }
58
85
  lessThan(value) {
59
- return { property: this.property, number: { less_than: value } };
86
+ return this.build({ less_than: value });
60
87
  }
61
88
  lessThanOrEqualTo(value) {
62
- return { property: this.property, number: { less_than_or_equal_to: value } };
89
+ return this.build({ less_than_or_equal_to: value });
63
90
  }
64
91
  isEmpty() {
65
- return { property: this.property, number: { is_empty: true } };
92
+ return this.build({ is_empty: true });
66
93
  }
67
94
  isNotEmpty() {
68
- return { property: this.property, number: { is_not_empty: true } };
95
+ return this.build({ is_not_empty: true });
69
96
  }
70
97
  }
71
98
  /** Operators for checkbox properties. */
72
99
  class CheckboxFilter {
73
- constructor(property) {
100
+ constructor(property, isFormula = false) {
74
101
  this.property = property;
102
+ this.isFormula = isFormula;
103
+ }
104
+ build(condition) {
105
+ if (this.isFormula) {
106
+ return { property: this.property, formula: { checkbox: condition } };
107
+ }
108
+ return { property: this.property, checkbox: condition };
75
109
  }
76
110
  equals(value) {
77
- return { property: this.property, checkbox: { equals: value } };
111
+ return this.build({ equals: value });
78
112
  }
79
113
  doesNotEqual(value) {
80
- return { property: this.property, checkbox: { does_not_equal: value } };
114
+ return this.build({ does_not_equal: value });
81
115
  }
82
116
  }
83
117
  /** Operators for select properties. */
@@ -86,9 +120,11 @@ class SelectFilter {
86
120
  this.property = property;
87
121
  }
88
122
  equals(value) {
123
+ assertScalarFilterValue(value, 'select().equals');
89
124
  return { property: this.property, select: { equals: value } };
90
125
  }
91
126
  doesNotEqual(value) {
127
+ assertScalarFilterValue(value, 'select().doesNotEqual');
92
128
  return { property: this.property, select: { does_not_equal: value } };
93
129
  }
94
130
  isEmpty() {
@@ -104,9 +140,11 @@ class MultiSelectFilter {
104
140
  this.property = property;
105
141
  }
106
142
  contains(value) {
143
+ assertScalarFilterValue(value, 'multiSelect().contains');
107
144
  return { property: this.property, multi_select: { contains: value } };
108
145
  }
109
146
  doesNotContain(value) {
147
+ assertScalarFilterValue(value, 'multiSelect().doesNotContain');
110
148
  return { property: this.property, multi_select: { does_not_contain: value } };
111
149
  }
112
150
  isEmpty() {
@@ -122,9 +160,11 @@ class StatusFilter {
122
160
  this.property = property;
123
161
  }
124
162
  equals(value) {
163
+ assertScalarFilterValue(value, 'status().equals');
125
164
  return { property: this.property, status: { equals: value } };
126
165
  }
127
166
  doesNotEqual(value) {
167
+ assertScalarFilterValue(value, 'status().doesNotEqual');
128
168
  return { property: this.property, status: { does_not_equal: value } };
129
169
  }
130
170
  isEmpty() {
@@ -136,11 +176,15 @@ class StatusFilter {
136
176
  }
137
177
  /** Operators for date properties and timestamp filters. */
138
178
  class DateFilter {
139
- constructor(key, isTimestamp) {
179
+ constructor(key, isTimestamp, isFormula = false) {
140
180
  this.key = key;
141
181
  this.isTimestamp = isTimestamp;
182
+ this.isFormula = isFormula;
142
183
  }
143
184
  wrap(condition) {
185
+ if (this.isFormula) {
186
+ return { property: this.key, formula: { date: condition } };
187
+ }
144
188
  if (this.isTimestamp) {
145
189
  return { timestamp: this.key, [this.key]: condition };
146
190
  }
@@ -240,17 +284,16 @@ class FormulaFilter {
240
284
  this.property = property;
241
285
  }
242
286
  text() {
243
- return new TextFilter(this.property, 'formula');
287
+ return new TextFilter(this.property, 'formula', true);
244
288
  }
245
289
  number() {
246
- // Return a NumberFilter-like but under "formula" key
247
- return new NumberFilter(this.property);
290
+ return new NumberFilter(this.property, true);
248
291
  }
249
292
  checkbox() {
250
- return new CheckboxFilter(this.property);
293
+ return new CheckboxFilter(this.property, true);
251
294
  }
252
295
  date() {
253
- return new DateFilter(this.property, false);
296
+ return new DateFilter(this.property, false, true);
254
297
  }
255
298
  }
256
299
  // ---------------------------------------------------------------------------
@@ -43,8 +43,13 @@ export type PaginatedFetchFunction<T> = (cursor?: string) => Promise<PaginatedLi
43
43
  /**
44
44
  * Collects all results from a paginated endpoint by automatically following cursors.
45
45
  *
46
- * This function fetches pages until `has_more` is `false`. It collects all results
47
- * into one array. Use this function when you need all results at once.
46
+ * This function fetches pages until `has_more` is `false` or `next_cursor` is `null`.
47
+ * It collects all results into one array. Use this function when you need all results
48
+ * at once.
49
+ *
50
+ * This helper does not work around the 10,000-result cap on data source, view, and
51
+ * meeting-notes queries. When a query hits that cap, the result set is truncated and
52
+ * this helper writes a warning. Use {@link collectAllDataSourceRows} for those queries.
48
53
  *
49
54
  * @param fetchPage - Function that fetches a single page of results
50
55
  * @returns Array containing all results from all pages
@@ -5,11 +5,32 @@ exports.paginateIterator = paginateIterator;
5
5
  exports.paginateWithMetadata = paginateWithMetadata;
6
6
  exports.iterateAllDataSourceRows = iterateAllDataSourceRows;
7
7
  exports.collectAllDataSourceRows = collectAllDataSourceRows;
8
+ /**
9
+ * Return the cursor for the next page, or `undefined` when iteration must stop
10
+ * (`has_more` is `false`, or `next_cursor` is `null`). Warn when the result is a
11
+ * truncated query (`request_status.type === 'incomplete'`).
12
+ */
13
+ function nextCursor(response) {
14
+ if (response.request_status?.type === 'incomplete') {
15
+ console.warn('Notion returned a truncated query result (the 10,000-result cap was reached). ' +
16
+ 'This paginate helper stops here and the result set is incomplete. ' +
17
+ 'Use collectAllDataSourceRows() or iterateAllDataSourceRows() to read every row.');
18
+ }
19
+ if (!response.has_more || response.next_cursor === null) {
20
+ return undefined;
21
+ }
22
+ return response.next_cursor ?? undefined;
23
+ }
8
24
  /**
9
25
  * Collects all results from a paginated endpoint by automatically following cursors.
10
26
  *
11
- * This function fetches pages until `has_more` is `false`. It collects all results
12
- * into one array. Use this function when you need all results at once.
27
+ * This function fetches pages until `has_more` is `false` or `next_cursor` is `null`.
28
+ * It collects all results into one array. Use this function when you need all results
29
+ * at once.
30
+ *
31
+ * This helper does not work around the 10,000-result cap on data source, view, and
32
+ * meeting-notes queries. When a query hits that cap, the result set is truncated and
33
+ * this helper writes a warning. Use {@link collectAllDataSourceRows} for those queries.
13
34
  *
14
35
  * @param fetchPage - Function that fetches a single page of results
15
36
  * @returns Array containing all results from all pages
@@ -58,7 +79,7 @@ async function paginate(fetchPage) {
58
79
  do {
59
80
  const response = await fetchPage(cursor);
60
81
  all.push(...response.results);
61
- cursor = response.next_cursor ?? undefined;
82
+ cursor = nextCursor(response);
62
83
  } while (cursor);
63
84
  return all;
64
85
  }
@@ -112,7 +133,7 @@ async function* paginateIterator(fetchPage) {
112
133
  for (const item of response.results) {
113
134
  yield item;
114
135
  }
115
- cursor = response.next_cursor ?? undefined;
136
+ cursor = nextCursor(response);
116
137
  } while (cursor);
117
138
  }
118
139
  /**
@@ -142,7 +163,7 @@ async function paginateWithMetadata(fetchPage) {
142
163
  do {
143
164
  const response = await fetchPage(cursor);
144
165
  items.push(...response.results);
145
- cursor = response.next_cursor ?? undefined;
166
+ cursor = nextCursor(response);
146
167
  pageCount++;
147
168
  } while (cursor);
148
169
  return {
@@ -28,11 +28,30 @@ export declare class Page extends BaseModel<NotionPage> {
28
28
  */
29
29
  getTitle(): string | null;
30
30
  /**
31
- * Check if the page is a child of a database.
31
+ * Check if the page is a row in a database.
32
+ *
33
+ * On API version 2025-09-03 and later, a database row has a `data_source_id`
34
+ * parent. Older responses use a `database_id` parent. This method returns `true`
35
+ * for both.
32
36
  */
33
37
  isInDatabase(): boolean;
38
+ /**
39
+ * Check if the page is a row in a data source.
40
+ */
41
+ isInDataSource(): boolean;
34
42
  /**
35
43
  * Check if the page is a child of another page.
36
44
  */
37
45
  isSubpage(): boolean;
46
+ /**
47
+ * Get the parent data source ID.
48
+ * Return `null` when the parent is not a data source.
49
+ */
50
+ getParentDataSourceId(): string | null;
51
+ /**
52
+ * Get the parent database ID.
53
+ * A `data_source_id` parent also carries its database ID. Return `null` when the
54
+ * parent is neither a database nor a data source.
55
+ */
56
+ getParentDatabaseId(): string | null;
38
57
  }
@@ -62,10 +62,20 @@ class Page extends base_model_1.BaseModel {
62
62
  return null;
63
63
  }
64
64
  /**
65
- * Check if the page is a child of a database.
65
+ * Check if the page is a row in a database.
66
+ *
67
+ * On API version 2025-09-03 and later, a database row has a `data_source_id`
68
+ * parent. Older responses use a `database_id` parent. This method returns `true`
69
+ * for both.
66
70
  */
67
71
  isInDatabase() {
68
- return this.data.parent.type === 'database_id';
72
+ return this.data.parent.type === 'data_source_id' || this.data.parent.type === 'database_id';
73
+ }
74
+ /**
75
+ * Check if the page is a row in a data source.
76
+ */
77
+ isInDataSource() {
78
+ return this.data.parent.type === 'data_source_id';
69
79
  }
70
80
  /**
71
81
  * Check if the page is a child of another page.
@@ -73,5 +83,26 @@ class Page extends base_model_1.BaseModel {
73
83
  isSubpage() {
74
84
  return this.data.parent.type === 'page_id';
75
85
  }
86
+ /**
87
+ * Get the parent data source ID.
88
+ * Return `null` when the parent is not a data source.
89
+ */
90
+ getParentDataSourceId() {
91
+ return this.data.parent.type === 'data_source_id' ? this.data.parent.data_source_id : null;
92
+ }
93
+ /**
94
+ * Get the parent database ID.
95
+ * A `data_source_id` parent also carries its database ID. Return `null` when the
96
+ * parent is neither a database nor a data source.
97
+ */
98
+ getParentDatabaseId() {
99
+ if (this.data.parent.type === 'database_id') {
100
+ return this.data.parent.database_id;
101
+ }
102
+ if (this.data.parent.type === 'data_source_id') {
103
+ return this.data.parent.database_id;
104
+ }
105
+ return null;
106
+ }
76
107
  }
77
108
  exports.Page = Page;