@notionhq/custom-blocks 0.1.33 → 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 (32) hide show
  1. package/dist/bridge/SandboxBridge.d.ts +3 -1
  2. package/dist/bridge/SandboxBridge.d.ts.map +1 -1
  3. package/dist/bridge/SandboxBridge.js +54 -47
  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/queryDataSource.d.ts +955 -0
  14. package/dist/protocol/messages/queryDataSource.d.ts.map +1 -1
  15. package/dist/protocol/messages/queryDataSource.js +100 -0
  16. package/dist/protocol/messages/queryDataSourceResult.d.ts +1 -1
  17. package/dist/protocol/messages/queryDataSourceResult.d.ts.map +1 -1
  18. package/dist/protocol/messages/sandboxToHost.d.ts +349 -0
  19. package/dist/protocol/messages/sandboxToHost.d.ts.map +1 -1
  20. package/dist/react/useDataSource.d.ts.map +1 -1
  21. package/dist/react/useDataSource.js +10 -5
  22. package/dist/types.d.ts +45 -0
  23. package/dist/types.d.ts.map +1 -1
  24. package/dist/version.js +1 -1
  25. package/docs/data-sources.md +39 -3
  26. package/package.json +1 -1
  27. package/src/bridge/SandboxBridge.ts +65 -57
  28. package/src/bridge/dataSources/query.ts +470 -0
  29. package/src/bridge/hostState.ts +11 -3
  30. package/src/bridge/sandboxClient.ts +20 -4
  31. package/src/react/useDataSource.ts +16 -5
  32. package/src/types.ts +47 -0
@@ -41,7 +41,10 @@ Use `row.update(...)` whenever you already have a row in hand. For pages you don
41
41
  ```ts
42
42
  function useDataSource(
43
43
  key: string,
44
- options?: { limit?: number },
44
+ options?: {
45
+ limit?: number;
46
+ filter?: NotionDataSourceFilter;
47
+ },
45
48
  ): UseDataSourceResult;
46
49
 
47
50
  type UseDataSourceResult = {
@@ -56,7 +59,36 @@ type UseDataSourceResult = {
56
59
  };
57
60
  ```
58
61
 
59
- Reads the data source mapped to `key`. `limit` defaults to 20 and is capped at 999. To show more rows, keep the desired limit in your own component state and pass the larger value back into `useDataSource(key, { limit })`. `propertyIdsByKey` maps manifest property keys to raw Notion property IDs; unbound keys map to `undefined`. `propertySchemasByKey` exposes the corresponding schemas and is likewise `undefined` for declared-but-unbound slots.
62
+ Reads the data source mapped to `key`. The SDK uses a default `limit` of 20 and caps it at 999. To request more rows, store the limit in component state and pass it to `useDataSource` again. Each request is bounded. The hook does not provide cursor pagination.
63
+
64
+ `filter` accepts one property condition or one shallow `and` group. Use `key` for a semantic property key. Use `propertyId` for a raw Notion property ID. The SDK resolves semantic keys before it sends the bridge request. The SDK reports local errors for unknown or unbound keys, property-type mismatches, unsupported filter fields, invalid operators, and invalid values.
65
+
66
+ ```tsx
67
+ const query = useDataSource("tasks", {
68
+ filter: {
69
+ and: [
70
+ { key: "title", title: { contains: "launch" } },
71
+ { key: "done", checkbox: { equals: false } },
72
+ ],
73
+ },
74
+ limit: 50,
75
+ });
76
+ ```
77
+
78
+ Filters support these property types:
79
+
80
+ - Text properties: `title`, `rich_text`, `url`, `email`, and `phone_number`. They support `equals`, `does_not_equal`, `contains`, `does_not_contain`, `starts_with`, `ends_with`, `is_empty`, and `is_not_empty`.
81
+ - `number`: `equals`, `does_not_equal`, `greater_than`, `less_than`, `greater_than_or_equal_to`, `less_than_or_equal_to`, `is_empty`, and `is_not_empty`.
82
+ - `checkbox`: `equals` and `does_not_equal`.
83
+ - `select` and `status`: `equals`, `does_not_equal`, `is_empty`, and `is_not_empty`.
84
+ - `multi_select`: `contains`, `contains_all`, `does_not_contain`, `is_empty`, and `is_not_empty`.
85
+ - `date`: `equals`, `before`, `after`, `on_or_before`, `on_or_after`, `is_empty`, and `is_not_empty`.
86
+
87
+ Select, multi-select, and status values can be one option name or an array of option names. For multi-select properties, `contains` matches any supplied option and `contains_all` requires every supplied option. Empty checks use `{ is_empty: true }` or `{ is_not_empty: true }`. Date comparisons accept ISO dates or ISO timestamps.
88
+
89
+ The `and` group can contain up to 25 conditions. An empty group is valid. Nested groups and `or` groups are not supported.
90
+
91
+ `propertyIdsByKey` maps manifest property keys to raw Notion property IDs; unbound keys map to `undefined`. `propertySchemasByKey` exposes the corresponding schemas and is likewise `undefined` for declared-but-unbound slots.
60
92
 
61
93
  Query failures follow the SDK's [error-handling contract](./errors.md).
62
94
 
@@ -161,7 +193,11 @@ export function ScoreList() {
161
193
  - `NotionDataSourcePageUpdateArgs` / `UpdatePageResult` — arguments and result for the per-page `update` helper.
162
194
  - `NotionDataSourcePageUpdateInput` — deprecated alias for `NotionDataSourcePageUpdateArgs`.
163
195
  - `NotionDataSourcePageUpdateResult` — deprecated alias for `UpdatePageResult`.
164
- - `UseDataSourceOptions` — options accepted by `useDataSource`, currently `{ limit?: number }`.
196
+ - `UseDataSourceOptions` — options accepted by `useDataSource`: `limit?: number` and `filter?: NotionDataSourceFilter`.
197
+ - `NotionDataSourceFilter` — one property condition or one shallow `and` group.
198
+ - `NotionDataSourcePropertyFilter` — one property address combined with a type-specific operator.
199
+ - `NotionDataSourcePropertyAddress` — either a semantic property `key` or a raw `propertyId`.
200
+ - `NotionDataSourceTextFilterOperator`, `NotionDataSourceNumberFilterOperator`, `NotionDataSourceCheckboxFilterOperator`, `NotionDataSourceOptionFilterOperator`, `NotionDataSourceContainsFilterOperator`, and `NotionDataSourceDateFilterOperator` — the operators accepted for each property type.
165
201
 
166
202
  ### Property schemas
167
203
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@notionhq/custom-blocks",
3
- "version": "0.1.33",
3
+ "version": "0.1.34",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -52,6 +52,7 @@ import {
52
52
  NOTION_DARK_BACKGROUND_BASE,
53
53
  NOTION_LIGHT_BACKGROUND_BASE,
54
54
  } from "./appearance.js"
55
+ import { resolveDataSourceQuery } from "./dataSources/query.js"
55
56
  import { resolveDataSources } from "./dataSources/resolve.js"
56
57
  import { resolvePropertyWriteMapForDataSource } from "./dataSources/resolveProperty.js"
57
58
  import {
@@ -393,12 +394,14 @@ export class SandboxBridge {
393
394
  nextBindings,
394
395
  })
395
396
  this.latestDataSourceBindings = nextBindings
396
- // Drop cached query state for keys that no longer exist in the mapping.
397
+ // Drop cached query state for subscriptions whose key no longer exists.
397
398
  const nextKeys = new Set(dataSources.map(s => s.key))
398
399
  const prunedState: Record<string, DataSourceQueryState> = {}
399
- for (const [key, state] of Object.entries(hostState.dataSourceState)) {
400
- if (nextKeys.has(key)) {
401
- prunedState[key] = state
400
+ for (const [subscriptionId, state] of Object.entries(
401
+ hostState.dataSourceState,
402
+ )) {
403
+ if (nextKeys.has(state.dataSourceKey)) {
404
+ prunedState[subscriptionId] = state
402
405
  }
403
406
  }
404
407
  this.hostState = {
@@ -476,13 +479,10 @@ export class SandboxBridge {
476
479
  }
477
480
 
478
481
  case "queryDataSourceResult": {
479
- const queryEntry = Object.entries(hostState.dataSourceState).find(
480
- ([, state]) => state.subscriptionId === message.subscriptionId,
481
- )
482
- if (queryEntry === undefined) {
482
+ const currentState = hostState.dataSourceState[message.subscriptionId]
483
+ if (currentState === undefined) {
483
484
  return
484
485
  }
485
- const [key, currentState] = queryEntry
486
486
  const queryResult =
487
487
  message.status === "error"
488
488
  ? {
@@ -499,13 +499,14 @@ export class SandboxBridge {
499
499
  ...hostState,
500
500
  dataSourceState: {
501
501
  ...hostState.dataSourceState,
502
- [key]: {
502
+ [message.subscriptionId]: {
503
+ dataSourceKey: currentState.dataSourceKey,
503
504
  items: queryResult.items,
504
505
  isLoading: false,
505
506
  hasMore: queryResult.hasMore,
506
507
  error: queryResult.error,
507
- subscriptionId: message.subscriptionId,
508
508
  latestLimit: currentState.latestLimit,
509
+ latestQueryIdentity: currentState.latestQueryIdentity,
509
510
  },
510
511
  },
511
512
  }
@@ -672,7 +673,15 @@ export class SandboxBridge {
672
673
  }
673
674
  }
674
675
 
675
- queryDataSource(key: string, options: UseDataSourceOptions = {}) {
676
+ createDataSourceSubscriptionId(): string {
677
+ return `data-source:${globalThis.crypto.randomUUID()}`
678
+ }
679
+
680
+ queryDataSource(
681
+ subscriptionId: string,
682
+ key: string,
683
+ options: UseDataSourceOptions = {},
684
+ ) {
676
685
  if (this.hostState.status !== "initialized") {
677
686
  return
678
687
  }
@@ -680,10 +689,13 @@ export class SandboxBridge {
680
689
  const dataSource = this.hostState.dataSources.find(
681
690
  entry => entry.key === key,
682
691
  )
692
+ const subscriptionState = this.hostState.dataSourceState[subscriptionId]
683
693
  const currentState =
684
- this.hostState.dataSourceState[key] ?? createEmptyDataSourceQueryState()
694
+ subscriptionState?.dataSourceKey === key
695
+ ? subscriptionState
696
+ : createEmptyDataSourceQueryState(key)
685
697
  if (dataSource === undefined) {
686
- this.setDataSourceQueryError(key, currentState, {
698
+ this.setDataSourceQueryError(subscriptionId, currentState, {
687
699
  code: "unknown_data_source_key",
688
700
  message: `Unknown data source key "${key}". Known keys: [${this.hostState.dataSources.map(entry => entry.key).join(", ")}].`,
689
701
  isRetryable: false,
@@ -691,7 +703,7 @@ export class SandboxBridge {
691
703
  return
692
704
  }
693
705
  if (dataSource.collectionPointer === undefined) {
694
- this.setDataSourceQueryError(key, currentState, {
706
+ this.setDataSourceQueryError(subscriptionId, currentState, {
695
707
  code: "unmapped_data_source",
696
708
  message: `Data source "${key}" has not been mapped to a database yet.`,
697
709
  isRetryable: false,
@@ -699,15 +711,24 @@ export class SandboxBridge {
699
711
  return
700
712
  }
701
713
 
702
- const limit = resolveDataSourceQueryLimit(options.limit)
703
- const subscriptionId = makeDataSourceSubscriptionId({
714
+ const resolvedQuery = resolveDataSourceQuery({
715
+ dataSources: this.hostState.dataSources,
704
716
  key,
717
+ options,
705
718
  })
719
+ if (resolvedQuery.status === "error") {
720
+ this.setDataSourceQueryError(subscriptionId, currentState, {
721
+ code: "invalid_data_source_query",
722
+ message: resolvedQuery.error,
723
+ isRetryable: false,
724
+ })
725
+ return
726
+ }
727
+ const query = resolvedQuery.query
706
728
 
707
729
  if (
708
730
  currentState.isLoading &&
709
- currentState.subscriptionId === subscriptionId &&
710
- currentState.latestLimit === limit
731
+ currentState.latestQueryIdentity === query.identity
711
732
  ) {
712
733
  return
713
734
  }
@@ -716,12 +737,13 @@ export class SandboxBridge {
716
737
  ...this.hostState,
717
738
  dataSourceState: {
718
739
  ...this.hostState.dataSourceState,
719
- [key]: {
740
+ [subscriptionId]: {
720
741
  ...currentState,
742
+ dataSourceKey: key,
721
743
  isLoading: true,
722
744
  error: undefined,
723
- subscriptionId,
724
- latestLimit: limit,
745
+ latestLimit: query.limit,
746
+ latestQueryIdentity: query.identity,
725
747
  },
726
748
  },
727
749
  }
@@ -730,14 +752,15 @@ export class SandboxBridge {
730
752
  const outbound: QueryDataSourceMessage = {
731
753
  type: "queryDataSource",
732
754
  subscriptionId,
733
- dataSourceId: dataSource.collectionPointer.id,
734
- limit,
755
+ dataSourceId: query.dataSourceId,
756
+ limit: query.limit,
757
+ ...(query.filter !== undefined ? { filter: query.filter } : {}),
735
758
  }
736
759
  this.postToHost(outbound)
737
760
  }
738
761
 
739
762
  private setDataSourceQueryError(
740
- key: string,
763
+ subscriptionId: string,
741
764
  currentState: DataSourceQueryState,
742
765
  error: CustomBlockQueryDataSourceErrorInfo,
743
766
  ) {
@@ -748,18 +771,33 @@ export class SandboxBridge {
748
771
  ...this.hostState,
749
772
  dataSourceState: {
750
773
  ...this.hostState.dataSourceState,
751
- [key]: {
774
+ [subscriptionId]: {
752
775
  ...currentState,
753
776
  isLoading: false,
754
777
  error,
755
- subscriptionId: undefined,
756
778
  latestLimit: undefined,
779
+ latestQueryIdentity: undefined,
757
780
  },
758
781
  },
759
782
  }
760
783
  this.notify()
761
784
  }
762
785
 
786
+ releaseDataSourceSubscription(subscriptionId: string) {
787
+ if (
788
+ this.hostState.status !== "initialized" ||
789
+ this.hostState.dataSourceState[subscriptionId] === undefined
790
+ ) {
791
+ return
792
+ }
793
+ const { [subscriptionId]: _, ...dataSourceState } =
794
+ this.hostState.dataSourceState
795
+ this.hostState = {
796
+ ...this.hostState,
797
+ dataSourceState,
798
+ }
799
+ }
800
+
763
801
  postResize(height: number) {
764
802
  if (typeof window === "undefined") {
765
803
  return
@@ -1054,36 +1092,6 @@ function reuseDataSourcesForUnchangedBindings(args: {
1054
1092
  })
1055
1093
  }
1056
1094
 
1057
- // The default number of items to return in a live snapshot response if no limit is provided.
1058
- const DEFAULT_DATA_SOURCE_QUERY_LIMIT = 20
1059
-
1060
- // The maximum number of items to return in a single live snapshot response.
1061
- const MAX_DATA_SOURCE_QUERY_LIMIT = 999
1062
-
1063
- function resolveDataSourceQueryLimit(limit: number | undefined): number {
1064
- if (limit === undefined) {
1065
- return DEFAULT_DATA_SOURCE_QUERY_LIMIT
1066
- }
1067
- if (!Number.isFinite(limit) || !Number.isInteger(limit) || limit < 1) {
1068
- console.warn(
1069
- `[custom-blocks-sdk] useDataSource limit must be a positive integer; using ${DEFAULT_DATA_SOURCE_QUERY_LIMIT}.`,
1070
- )
1071
- return DEFAULT_DATA_SOURCE_QUERY_LIMIT
1072
- }
1073
- if (limit > MAX_DATA_SOURCE_QUERY_LIMIT) {
1074
- console.warn(
1075
- `[custom-blocks-sdk] useDataSource limit is capped at ${MAX_DATA_SOURCE_QUERY_LIMIT}.`,
1076
- )
1077
- return MAX_DATA_SOURCE_QUERY_LIMIT
1078
- }
1079
- return limit
1080
- }
1081
-
1082
- function makeDataSourceSubscriptionId(args: { key: string }): string {
1083
- const { key } = args
1084
- return `data-source:${encodeURIComponent(key)}`
1085
- }
1086
-
1087
1095
  function invalidInitPayloadError(
1088
1096
  issues: readonly v.BaseIssue<unknown>[],
1089
1097
  ): CustomBlockInitResultErrorInfo {