@notionhq/custom-blocks 0.1.32 → 0.1.34

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 (38) hide show
  1. package/dist/bridge/SandboxBridge.d.ts +4 -1
  2. package/dist/bridge/SandboxBridge.d.ts.map +1 -1
  3. package/dist/bridge/SandboxBridge.js +95 -57
  4. package/dist/bridge/dataSources/query.d.ts +26 -0
  5. package/dist/bridge/dataSources/query.d.ts.map +1 -0
  6. package/dist/bridge/dataSources/query.js +377 -0
  7. package/dist/bridge/hostState.d.ts +4 -3
  8. package/dist/bridge/hostState.d.ts.map +1 -1
  9. package/dist/bridge/hostState.js +7 -3
  10. package/dist/bridge/sandboxClient.d.ts +4 -2
  11. package/dist/bridge/sandboxClient.d.ts.map +1 -1
  12. package/dist/bridge/sandboxClient.js +10 -4
  13. package/dist/protocol/messages/init.d.ts +1 -1
  14. package/dist/protocol/messages/init.d.ts.map +1 -1
  15. package/dist/protocol/messages/initResult.d.ts +8 -1
  16. package/dist/protocol/messages/initResult.d.ts.map +1 -1
  17. package/dist/protocol/messages/queryDataSource.d.ts +955 -0
  18. package/dist/protocol/messages/queryDataSource.d.ts.map +1 -1
  19. package/dist/protocol/messages/queryDataSource.js +100 -0
  20. package/dist/protocol/messages/queryDataSourceResult.d.ts +1 -1
  21. package/dist/protocol/messages/queryDataSourceResult.d.ts.map +1 -1
  22. package/dist/protocol/messages/sandboxToHost.d.ts +349 -0
  23. package/dist/protocol/messages/sandboxToHost.d.ts.map +1 -1
  24. package/dist/react/useDataSource.d.ts.map +1 -1
  25. package/dist/react/useDataSource.js +10 -5
  26. package/dist/types.d.ts +45 -0
  27. package/dist/types.d.ts.map +1 -1
  28. package/dist/version.js +1 -1
  29. package/docs/data-sources.md +39 -3
  30. package/docs/errors.md +16 -0
  31. package/docs/lifecycle.md +74 -20
  32. package/package.json +1 -1
  33. package/src/bridge/SandboxBridge.ts +112 -64
  34. package/src/bridge/dataSources/query.ts +470 -0
  35. package/src/bridge/hostState.ts +11 -3
  36. package/src/bridge/sandboxClient.ts +20 -4
  37. package/src/react/useDataSource.ts +16 -5
  38. package/src/types.ts +47 -0
@@ -0,0 +1,470 @@
1
+ import type { NotionDataSource } from "@notionhq/custom-blocks-protocol/dataSources/dataSource.js"
2
+ import type { NotionDataSourceId } from "@notionhq/custom-blocks-protocol/ids.js"
3
+ import {
4
+ customBlockCheckboxFilterOperatorSchema,
5
+ customBlockContainsFilterOperatorSchema,
6
+ customBlockDateFilterOperatorSchema,
7
+ customBlockNumberFilterOperatorSchema,
8
+ customBlockOptionFilterOperatorSchema,
9
+ customBlockTextFilterOperatorSchema,
10
+ type ResolvedCustomBlockDataSourceFilter,
11
+ type ResolvedCustomBlockDataSourcePropertyFilter,
12
+ } from "@notionhq/custom-blocks-protocol/messages/queryDataSource.js"
13
+ import * as v from "valibot"
14
+ import type { UseDataSourceOptions } from "../../types.js"
15
+
16
+ const DEFAULT_DATA_SOURCE_QUERY_LIMIT = 20
17
+ const MAX_DATA_SOURCE_QUERY_LIMIT = 999
18
+ const MAX_DATA_SOURCE_QUERY_FILTER_CHILDREN = 25
19
+
20
+ export type ResolvedDataSourceQuery = {
21
+ dataSourceId: NotionDataSourceId
22
+ limit: number
23
+ filter?: ResolvedCustomBlockDataSourceFilter
24
+ identity: string
25
+ }
26
+
27
+ export type ResolveDataSourceQueryResult =
28
+ | { status: "error"; error: string }
29
+ | {
30
+ status: "ok"
31
+ dataSource: NotionDataSource
32
+ query: ResolvedDataSourceQuery
33
+ }
34
+
35
+ export function resolveDataSourceQuery(args: {
36
+ dataSources: NotionDataSource[]
37
+ key: string
38
+ options?: unknown
39
+ warn?: (message: string) => void
40
+ }): ResolveDataSourceQueryResult {
41
+ const { dataSources, key, options, warn } = args
42
+ if (options !== undefined && !isPlainRecord(options)) {
43
+ return {
44
+ status: "error",
45
+ error: "Data source query options must be an object.",
46
+ }
47
+ }
48
+ if (
49
+ options !== undefined &&
50
+ Object.keys(options).some(key => !["limit", "filter"].includes(key))
51
+ ) {
52
+ return {
53
+ status: "error",
54
+ error: "Data source query options contain unsupported fields.",
55
+ }
56
+ }
57
+ const dataSource = dataSources.find(entry => entry.key === key)
58
+ if (dataSource === undefined) {
59
+ return {
60
+ status: "error",
61
+ error: `Unknown data source key "${key}". Known keys: [${dataSources.map(entry => entry.key).join(", ")}].`,
62
+ }
63
+ }
64
+ if (dataSource.collectionPointer === undefined) {
65
+ return {
66
+ status: "error",
67
+ error: `Data source "${key}" has not been mapped to a database yet.`,
68
+ }
69
+ }
70
+
71
+ const limit = resolveDataSourceQueryLimit(options?.limit, warn)
72
+ if (limit.status === "error") {
73
+ return limit
74
+ }
75
+ const filter = resolveFilter(options?.filter, dataSource)
76
+ if (filter.status === "error") {
77
+ return filter
78
+ }
79
+
80
+ const identity = stableJsonStringify({
81
+ dataSourceId: dataSource.collectionPointer.id,
82
+ limit: limit.limit,
83
+ filter: filter.filter ?? null,
84
+ })
85
+ return {
86
+ status: "ok",
87
+ dataSource,
88
+ query: {
89
+ dataSourceId: dataSource.collectionPointer.id,
90
+ limit: limit.limit,
91
+ filter: filter.filter,
92
+ identity,
93
+ },
94
+ }
95
+ }
96
+
97
+ function resolveDataSourceQueryLimit(
98
+ limit: unknown,
99
+ warn: ((message: string) => void) | undefined = console.warn,
100
+ ): { status: "ok"; limit: number } | { status: "error"; error: string } {
101
+ if (limit === undefined) {
102
+ return { status: "ok", limit: DEFAULT_DATA_SOURCE_QUERY_LIMIT }
103
+ }
104
+ if (
105
+ typeof limit !== "number" ||
106
+ !Number.isFinite(limit) ||
107
+ !Number.isInteger(limit) ||
108
+ limit < 1
109
+ ) {
110
+ return {
111
+ status: "error",
112
+ error: `Data source query limit must be a positive integer between 1 and ${MAX_DATA_SOURCE_QUERY_LIMIT}.`,
113
+ }
114
+ }
115
+ if (limit > MAX_DATA_SOURCE_QUERY_LIMIT) {
116
+ warn?.(
117
+ `Data source query limit ${limit} exceeds the maximum of ${MAX_DATA_SOURCE_QUERY_LIMIT}; clamping to ${MAX_DATA_SOURCE_QUERY_LIMIT}.`,
118
+ )
119
+ return { status: "ok", limit: MAX_DATA_SOURCE_QUERY_LIMIT }
120
+ }
121
+ return { status: "ok", limit }
122
+ }
123
+
124
+ export function getDataSourceQueryOptionsIdentity(
125
+ options: UseDataSourceOptions | undefined,
126
+ ): string {
127
+ try {
128
+ return stableJsonStringify(options ?? {})
129
+ } catch {
130
+ return "invalid-data-source-query-options"
131
+ }
132
+ }
133
+
134
+ function stableJsonStringify(value: unknown): string {
135
+ return JSON.stringify(normalizeJsonValue(value))
136
+ }
137
+
138
+ function resolveFilter(
139
+ filter: unknown,
140
+ dataSource: NotionDataSource,
141
+ ):
142
+ | { status: "ok"; filter?: ResolvedCustomBlockDataSourceFilter }
143
+ | { status: "error"; error: string } {
144
+ if (filter === undefined) {
145
+ return { status: "ok" }
146
+ }
147
+ if (!isPlainRecord(filter)) {
148
+ return {
149
+ status: "error",
150
+ error: "Data source query filter must be an object.",
151
+ }
152
+ }
153
+ if ("and" in filter) {
154
+ if (Object.keys(filter).length !== 1 || !Array.isArray(filter.and)) {
155
+ return {
156
+ status: "error",
157
+ error: 'Data source query filter "and" must be an array.',
158
+ }
159
+ }
160
+ if (filter.and.length > MAX_DATA_SOURCE_QUERY_FILTER_CHILDREN) {
161
+ return {
162
+ status: "error",
163
+ error: `Data source query filter "and" may contain at most ${MAX_DATA_SOURCE_QUERY_FILTER_CHILDREN} children.`,
164
+ }
165
+ }
166
+ const and: ResolvedCustomBlockDataSourcePropertyFilter[] = []
167
+ for (const child of filter.and) {
168
+ const resolved = resolvePropertyFilter(child, dataSource)
169
+ if (resolved.status === "error") {
170
+ return resolved
171
+ }
172
+ and.push(resolved.filter)
173
+ }
174
+ return { status: "ok", filter: { and } }
175
+ }
176
+ return resolvePropertyFilter(filter, dataSource)
177
+ }
178
+
179
+ function resolvePropertyFilter(
180
+ filter: unknown,
181
+ dataSource: NotionDataSource,
182
+ ):
183
+ | { status: "ok"; filter: ResolvedCustomBlockDataSourcePropertyFilter }
184
+ | { status: "error"; error: string } {
185
+ if (!isPlainRecord(filter)) {
186
+ return {
187
+ status: "error",
188
+ error: "Data source query property filter must be an object.",
189
+ }
190
+ }
191
+ const property = resolvePropertyAddress(filter, dataSource)
192
+ if (property.status === "error") {
193
+ return property
194
+ }
195
+ const branchKeys = Object.keys(filter).filter(
196
+ key => key !== "key" && key !== "propertyId",
197
+ )
198
+ if (branchKeys.length !== 1) {
199
+ return {
200
+ status: "error",
201
+ error:
202
+ "Data source query property filter must contain exactly one property branch.",
203
+ }
204
+ }
205
+ const branch = branchKeys[0]
206
+ const value = filter[branch]
207
+ if (!isPlainRecord(value) || Object.keys(value).length !== 1) {
208
+ return invalidOperator(branch)
209
+ }
210
+ if (property.propertyType !== branch) {
211
+ return {
212
+ status: "error",
213
+ error: `Data source query filter branch "${branch}" does not match property type "${property.propertyType}".`,
214
+ }
215
+ }
216
+
217
+ switch (branch) {
218
+ case "title": {
219
+ const parsed = v.safeParse(customBlockTextFilterOperatorSchema, value)
220
+ return parsed.success
221
+ ? {
222
+ status: "ok",
223
+ filter: { propertyId: property.propertyId, title: parsed.output },
224
+ }
225
+ : invalidOperator(branch)
226
+ }
227
+ case "rich_text": {
228
+ const parsed = v.safeParse(customBlockTextFilterOperatorSchema, value)
229
+ return parsed.success
230
+ ? {
231
+ status: "ok",
232
+ filter: {
233
+ propertyId: property.propertyId,
234
+ rich_text: parsed.output,
235
+ },
236
+ }
237
+ : invalidOperator(branch)
238
+ }
239
+ case "url":
240
+ case "email":
241
+ case "phone_number": {
242
+ const parsed = v.safeParse(customBlockTextFilterOperatorSchema, value)
243
+ if (!parsed.success) {
244
+ return invalidOperator(branch)
245
+ }
246
+ if (branch === "url") {
247
+ return {
248
+ status: "ok",
249
+ filter: { propertyId: property.propertyId, url: parsed.output },
250
+ }
251
+ }
252
+ if (branch === "email") {
253
+ return {
254
+ status: "ok",
255
+ filter: { propertyId: property.propertyId, email: parsed.output },
256
+ }
257
+ }
258
+ return {
259
+ status: "ok",
260
+ filter: {
261
+ propertyId: property.propertyId,
262
+ phone_number: parsed.output,
263
+ },
264
+ }
265
+ }
266
+ case "number": {
267
+ const parsed = v.safeParse(customBlockNumberFilterOperatorSchema, value)
268
+ if (
269
+ parsed.success &&
270
+ Object.values(parsed.output).every(
271
+ entry => typeof entry !== "number" || Number.isFinite(entry),
272
+ )
273
+ ) {
274
+ return {
275
+ status: "ok",
276
+ filter: { propertyId: property.propertyId, number: parsed.output },
277
+ }
278
+ }
279
+ return invalidOperator(branch)
280
+ }
281
+ case "checkbox": {
282
+ const parsed = v.safeParse(customBlockCheckboxFilterOperatorSchema, value)
283
+ return parsed.success
284
+ ? {
285
+ status: "ok",
286
+ filter: {
287
+ propertyId: property.propertyId,
288
+ checkbox: parsed.output,
289
+ },
290
+ }
291
+ : invalidOperator(branch)
292
+ }
293
+ case "select": {
294
+ const parsed = v.safeParse(customBlockOptionFilterOperatorSchema, value)
295
+ return parsed.success
296
+ ? {
297
+ status: "ok",
298
+ filter: { propertyId: property.propertyId, select: parsed.output },
299
+ }
300
+ : invalidOperator(branch)
301
+ }
302
+ case "multi_select": {
303
+ const parsed = v.safeParse(customBlockContainsFilterOperatorSchema, value)
304
+ return parsed.success
305
+ ? {
306
+ status: "ok",
307
+ filter: {
308
+ propertyId: property.propertyId,
309
+ multi_select: parsed.output,
310
+ },
311
+ }
312
+ : invalidOperator(branch)
313
+ }
314
+ case "status": {
315
+ const parsed = v.safeParse(customBlockOptionFilterOperatorSchema, value)
316
+ return parsed.success
317
+ ? {
318
+ status: "ok",
319
+ filter: { propertyId: property.propertyId, status: parsed.output },
320
+ }
321
+ : invalidOperator(branch)
322
+ }
323
+ case "date": {
324
+ const parsed = v.safeParse(customBlockDateFilterOperatorSchema, value)
325
+ if (
326
+ parsed.success &&
327
+ Object.values(parsed.output).every(
328
+ entry => entry === true || isValidIsoDate(entry),
329
+ )
330
+ ) {
331
+ return {
332
+ status: "ok",
333
+ filter: { propertyId: property.propertyId, date: parsed.output },
334
+ }
335
+ }
336
+ return invalidOperator(branch)
337
+ }
338
+ default:
339
+ return {
340
+ status: "error",
341
+ error: `Data source query filter branch "${branch}" is not supported.`,
342
+ }
343
+ }
344
+ }
345
+
346
+ function resolvePropertyAddress(
347
+ value: Record<string, unknown>,
348
+ dataSource: NotionDataSource,
349
+ ):
350
+ | { status: "ok"; propertyId: string; propertyType: string }
351
+ | { status: "error"; error: string } {
352
+ const hasKey = "key" in value
353
+ const hasPropertyId = "propertyId" in value
354
+ if (hasKey === hasPropertyId) {
355
+ return {
356
+ status: "error",
357
+ error:
358
+ "Data source query filters must use exactly one of key or propertyId.",
359
+ }
360
+ }
361
+ let propertyId: string
362
+ if (hasKey) {
363
+ if (typeof value.key !== "string") {
364
+ return {
365
+ status: "error",
366
+ error: "Data source query property key must be a string.",
367
+ }
368
+ }
369
+ if (!(value.key in dataSource.propertyIdsByKey)) {
370
+ return {
371
+ status: "error",
372
+ error: `Unknown property key "${value.key}" for data source "${dataSource.key}".`,
373
+ }
374
+ }
375
+ const resolvedPropertyId = dataSource.propertyIdsByKey[value.key]
376
+ if (resolvedPropertyId === undefined) {
377
+ return {
378
+ status: "error",
379
+ error: `Property key "${value.key}" for data source "${dataSource.key}" is not bound.`,
380
+ }
381
+ }
382
+ propertyId = resolvedPropertyId
383
+ } else {
384
+ if (typeof value.propertyId !== "string") {
385
+ return {
386
+ status: "error",
387
+ error: "Data source query propertyId must be a string.",
388
+ }
389
+ }
390
+ propertyId = value.propertyId
391
+ }
392
+ const propertySchema = dataSource.propertySchemasById[propertyId]
393
+ if (propertySchema === undefined) {
394
+ return {
395
+ status: "error",
396
+ error: `Unknown property ID "${propertyId}" for data source "${dataSource.key}".`,
397
+ }
398
+ }
399
+ return {
400
+ status: "ok",
401
+ propertyId,
402
+ propertyType: propertySchema.type,
403
+ }
404
+ }
405
+
406
+ function invalidOperator(branch: string): { status: "error"; error: string } {
407
+ return {
408
+ status: "error",
409
+ error: `Data source query filter branch "${branch}" has an invalid operator or value.`,
410
+ }
411
+ }
412
+
413
+ function normalizeJsonValue(value: unknown): unknown {
414
+ if (
415
+ value === null ||
416
+ typeof value === "string" ||
417
+ typeof value === "boolean"
418
+ ) {
419
+ return value
420
+ }
421
+ if (typeof value === "number") {
422
+ if (!Number.isFinite(value)) {
423
+ throw new Error("Data source query values must be finite JSON numbers.")
424
+ }
425
+ return value
426
+ }
427
+ if (Array.isArray(value)) {
428
+ return value.map(entry => normalizeJsonValue(entry))
429
+ }
430
+ if (isPlainRecord(value)) {
431
+ const normalized: Record<string, unknown> = {}
432
+ for (const key of Object.keys(value).sort()) {
433
+ const child = value[key]
434
+ if (child !== undefined) {
435
+ normalized[key] = normalizeJsonValue(child)
436
+ }
437
+ }
438
+ return normalized
439
+ }
440
+ throw new Error("Data source query values must be JSON-compatible.")
441
+ }
442
+
443
+ function isPlainRecord(value: unknown): value is Record<string, unknown> {
444
+ return (
445
+ typeof value === "object" &&
446
+ value !== null &&
447
+ !Array.isArray(value) &&
448
+ (Object.getPrototypeOf(value) === Object.prototype ||
449
+ Object.getPrototypeOf(value) === null)
450
+ )
451
+ }
452
+
453
+ function isValidIsoDate(value: unknown): boolean {
454
+ if (typeof value !== "string") {
455
+ return false
456
+ }
457
+ const dateOnly = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value)
458
+ if (dateOnly !== null) {
459
+ const [, year, month, day] = dateOnly
460
+ const date = new Date(
461
+ Date.UTC(Number(year), Number(month) - 1, Number(day)),
462
+ )
463
+ return date.toISOString().slice(0, 10) === value
464
+ }
465
+ return (
466
+ /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d{1,3})?)?(?:Z|[+-]\d{2}:\d{2})$/.test(
467
+ value,
468
+ ) && Number.isFinite(Date.parse(value))
469
+ )
470
+ }
@@ -41,17 +41,21 @@ export type InitializedHostState = {
41
41
  }
42
42
 
43
43
  export type DataSourceQueryState = {
44
+ dataSourceKey: string
44
45
  /** Latest pages from the host as parsed from the bridge. `propertiesByKey` is derived lazily. */
45
46
  items: NotionDataSourcePageBridge[]
46
47
  isLoading: boolean
47
48
  hasMore: boolean
48
49
  error?: CustomBlockQueryDataSourceErrorInfo
49
- subscriptionId?: string
50
50
  latestLimit?: number
51
+ latestQueryIdentity?: string
51
52
  }
52
53
 
53
- export function createEmptyDataSourceQueryState(): DataSourceQueryState {
54
+ export function createEmptyDataSourceQueryState(
55
+ dataSourceKey: string,
56
+ ): DataSourceQueryState {
54
57
  return {
58
+ dataSourceKey,
55
59
  items: [],
56
60
  isLoading: false,
57
61
  hasMore: false,
@@ -97,6 +101,7 @@ export type UpdateDataSourcePageFn = (args: {
97
101
  export function getDataSourceQueryView(
98
102
  hostState: CustomBlockHostState,
99
103
  key: string,
104
+ subscriptionId: string,
100
105
  updateDataSourcePage: UpdateDataSourcePageFn,
101
106
  ): DataSourceQueryView {
102
107
  if (hostState.status !== "initialized") {
@@ -104,8 +109,11 @@ export function getDataSourceQueryView(
104
109
  }
105
110
 
106
111
  const dataSource = hostState.dataSources.find(entry => entry.key === key)
112
+ const subscriptionState = hostState.dataSourceState[subscriptionId]
107
113
  const queryState =
108
- hostState.dataSourceState[key] ?? createEmptyDataSourceQueryState()
114
+ subscriptionState?.dataSourceKey === key
115
+ ? subscriptionState
116
+ : createEmptyDataSourceQueryState(key)
109
117
 
110
118
  if (dataSource === undefined) {
111
119
  return {
@@ -68,16 +68,32 @@ export const customBlockHost = {
68
68
  }
69
69
 
70
70
  export const customBlockDataSources = {
71
- query: (key: string, options?: UseDataSourceOptions) => {
72
- getBridge().queryDataSource(key, options)
71
+ createSubscriptionId: () => {
72
+ return getBridge().createDataSourceSubscriptionId()
73
+ },
74
+
75
+ query: (
76
+ subscriptionId: string,
77
+ key: string,
78
+ options?: UseDataSourceOptions,
79
+ ) => {
80
+ getBridge().queryDataSource(subscriptionId, key, options)
81
+ },
82
+
83
+ release: (subscriptionId: string) => {
84
+ getBridge().releaseDataSourceSubscription(subscriptionId)
73
85
  },
74
86
 
75
87
  getView: (
76
88
  hostState: CustomBlockHostState,
77
89
  key: string,
90
+ subscriptionId: string,
78
91
  ): DataSourceQueryView => {
79
- return getDataSourceQueryViewWithBridge(hostState, key, args =>
80
- getBridge().updateDataSourcePage(args),
92
+ return getDataSourceQueryViewWithBridge(
93
+ hostState,
94
+ key,
95
+ subscriptionId,
96
+ args => getBridge().updateDataSourcePage(args),
81
97
  )
82
98
  },
83
99
  }
@@ -1,4 +1,5 @@
1
- import { useEffect } from "react"
1
+ import { useEffect, useState } from "react"
2
+ import { getDataSourceQueryOptionsIdentity } from "../bridge/dataSources/query.js"
2
3
  import { customBlockDataSources } from "../bridge/sandboxClient.js"
3
4
  import type { UseDataSourceOptions, UseDataSourceResult } from "../types.js"
4
5
  import { useCustomBlockHost } from "./useHostState.js"
@@ -18,7 +19,10 @@ export function useDataSource(
18
19
  options?: UseDataSourceOptions,
19
20
  ): UseDataSourceResult {
20
21
  const host = useCustomBlockHost()
21
- const limit = options?.limit
22
+ const [subscriptionId] = useState(() =>
23
+ customBlockDataSources.createSubscriptionId(),
24
+ )
25
+ const optionsIdentity = getDataSourceQueryOptionsIdentity(options)
22
26
  const matchingDataSource =
23
27
  host.status === "initialized"
24
28
  ? host.dataSources.find(dataSource => dataSource.key === key)
@@ -30,10 +34,17 @@ export function useDataSource(
30
34
  return
31
35
  }
32
36
 
33
- customBlockDataSources.query(key, { limit })
34
- }, [matchingDataSource, isInitialized, key, limit])
37
+ customBlockDataSources.query(subscriptionId, key, options)
38
+ }, [matchingDataSource, isInitialized, key, optionsIdentity, subscriptionId])
35
39
 
36
- const view = customBlockDataSources.getView(host, key)
40
+ useEffect(
41
+ () => () => {
42
+ customBlockDataSources.release(subscriptionId)
43
+ },
44
+ [subscriptionId],
45
+ )
46
+
47
+ const view = customBlockDataSources.getView(host, key, subscriptionId)
37
48
 
38
49
  return {
39
50
  items: view.items,
package/src/types.ts CHANGED
@@ -24,6 +24,14 @@ import type {
24
24
  ListUsersMessage,
25
25
  ListUsersResultMessage,
26
26
  } from "@notionhq/custom-blocks-protocol/messages/listUsers.js"
27
+ import type {
28
+ CustomBlockCheckboxFilterOperator,
29
+ CustomBlockContainsFilterOperator,
30
+ CustomBlockDateFilterOperator,
31
+ CustomBlockNumberFilterOperator,
32
+ CustomBlockOptionFilterOperator,
33
+ CustomBlockTextFilterOperator,
34
+ } from "@notionhq/custom-blocks-protocol/messages/queryDataSource.js"
27
35
  import type { CustomBlockQueryDataSourceErrorInfo } from "@notionhq/custom-blocks-protocol/messages/queryDataSourceResult.js"
28
36
  import type { UpdatePageMessage } from "@notionhq/custom-blocks-protocol/messages/updatePage.js"
29
37
  import type {
@@ -116,6 +124,40 @@ export type NotionDataSourcePageUpdateInput = NotionDataSourcePageUpdateArgs
116
124
  */
117
125
  export type NotionDataSourcePageUpdateResult = UpdatePageResult
118
126
 
127
+ export type NotionDataSourceTextFilterOperator = CustomBlockTextFilterOperator
128
+ export type NotionDataSourceNumberFilterOperator =
129
+ CustomBlockNumberFilterOperator
130
+ export type NotionDataSourceCheckboxFilterOperator =
131
+ CustomBlockCheckboxFilterOperator
132
+ export type NotionDataSourceOptionFilterOperator =
133
+ CustomBlockOptionFilterOperator
134
+ export type NotionDataSourceContainsFilterOperator =
135
+ CustomBlockContainsFilterOperator
136
+ export type NotionDataSourceDateFilterOperator = CustomBlockDateFilterOperator
137
+
138
+ export type NotionDataSourcePropertyAddress =
139
+ | { key: string; propertyId?: never }
140
+ | { propertyId: string; key?: never }
141
+
142
+ export type NotionDataSourcePropertyFilter = NotionDataSourcePropertyAddress &
143
+ (
144
+ | { title: NotionDataSourceTextFilterOperator }
145
+ | { rich_text: NotionDataSourceTextFilterOperator }
146
+ | { url: NotionDataSourceTextFilterOperator }
147
+ | { email: NotionDataSourceTextFilterOperator }
148
+ | { phone_number: NotionDataSourceTextFilterOperator }
149
+ | { number: NotionDataSourceNumberFilterOperator }
150
+ | { checkbox: NotionDataSourceCheckboxFilterOperator }
151
+ | { select: NotionDataSourceOptionFilterOperator }
152
+ | { multi_select: NotionDataSourceContainsFilterOperator }
153
+ | { status: NotionDataSourceOptionFilterOperator }
154
+ | { date: NotionDataSourceDateFilterOperator }
155
+ )
156
+
157
+ export type NotionDataSourceFilter =
158
+ | NotionDataSourcePropertyFilter
159
+ | { and: NotionDataSourcePropertyFilter[] }
160
+
119
161
  /**
120
162
  * Return shape of `useDataSource`.
121
163
  *
@@ -157,6 +199,11 @@ export type UseDataSourceOptions = {
157
199
  * Maximum number of rows to request from the host. Defaults to 20.
158
200
  */
159
201
  limit?: number
202
+ /**
203
+ * Optional property filter. The SDK resolves semantic property keys before
204
+ * it sends the query to the host.
205
+ */
206
+ filter?: NotionDataSourceFilter
160
207
  }
161
208
 
162
209
  /**