@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.
- package/dist/bridge/SandboxBridge.d.ts +3 -1
- package/dist/bridge/SandboxBridge.d.ts.map +1 -1
- package/dist/bridge/SandboxBridge.js +54 -47
- package/dist/bridge/dataSources/query.d.ts +26 -0
- package/dist/bridge/dataSources/query.d.ts.map +1 -0
- package/dist/bridge/dataSources/query.js +377 -0
- package/dist/bridge/hostState.d.ts +4 -3
- package/dist/bridge/hostState.d.ts.map +1 -1
- package/dist/bridge/hostState.js +7 -3
- package/dist/bridge/sandboxClient.d.ts +4 -2
- package/dist/bridge/sandboxClient.d.ts.map +1 -1
- package/dist/bridge/sandboxClient.js +10 -4
- package/dist/protocol/messages/queryDataSource.d.ts +955 -0
- package/dist/protocol/messages/queryDataSource.d.ts.map +1 -1
- package/dist/protocol/messages/queryDataSource.js +100 -0
- package/dist/protocol/messages/queryDataSourceResult.d.ts +1 -1
- package/dist/protocol/messages/queryDataSourceResult.d.ts.map +1 -1
- package/dist/protocol/messages/sandboxToHost.d.ts +349 -0
- package/dist/protocol/messages/sandboxToHost.d.ts.map +1 -1
- package/dist/react/useDataSource.d.ts.map +1 -1
- package/dist/react/useDataSource.js +10 -5
- package/dist/types.d.ts +45 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/version.js +1 -1
- package/docs/data-sources.md +39 -3
- package/package.json +1 -1
- package/src/bridge/SandboxBridge.ts +65 -57
- package/src/bridge/dataSources/query.ts +470 -0
- package/src/bridge/hostState.ts +11 -3
- package/src/bridge/sandboxClient.ts +20 -4
- package/src/react/useDataSource.ts +16 -5
- package/src/types.ts +47 -0
package/docs/data-sources.md
CHANGED
|
@@ -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?: {
|
|
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`
|
|
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
|
|
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
|
@@ -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
|
|
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 [
|
|
400
|
-
|
|
401
|
-
|
|
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
|
|
480
|
-
|
|
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
|
-
[
|
|
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
|
-
|
|
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
|
-
|
|
694
|
+
subscriptionState?.dataSourceKey === key
|
|
695
|
+
? subscriptionState
|
|
696
|
+
: createEmptyDataSourceQueryState(key)
|
|
685
697
|
if (dataSource === undefined) {
|
|
686
|
-
this.setDataSourceQueryError(
|
|
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(
|
|
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
|
|
703
|
-
|
|
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.
|
|
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
|
-
[
|
|
740
|
+
[subscriptionId]: {
|
|
720
741
|
...currentState,
|
|
742
|
+
dataSourceKey: key,
|
|
721
743
|
isLoading: true,
|
|
722
744
|
error: undefined,
|
|
723
|
-
|
|
724
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
[
|
|
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 {
|