@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
@@ -65,8 +65,10 @@ export declare class SandboxBridge {
65
65
  * iframe covered throughout this frame, so it can apply the measured height before first paint.
66
66
  */
67
67
  private scheduleInitResult;
68
- queryDataSource(key: string, options?: UseDataSourceOptions): void;
68
+ createDataSourceSubscriptionId(): string;
69
+ queryDataSource(subscriptionId: string, key: string, options?: UseDataSourceOptions): void;
69
70
  private setDataSourceQueryError;
71
+ releaseDataSourceSubscription(subscriptionId: string): void;
70
72
  postResize(height: number): void;
71
73
  createPage(args: CreatePageArgs): Promise<CreatePageResult>;
72
74
  getPage(pageId: NotionPageId): Promise<GetPageResult>;
@@ -1 +1 @@
1
- {"version":3,"file":"SandboxBridge.d.ts","sourceRoot":"","sources":["../../../../home/runner/work/custom-blocks/custom-blocks/sdk/src/bridge/SandboxBridge.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"SandboxBridge.d.ts","sourceRoot":"","sources":["../../../../home/runner/work/custom-blocks/custom-blocks/sdk/src/bridge/SandboxBridge.ts"],"names":[],"mappings":"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"}
@@ -7,6 +7,7 @@ import * as v from "valibot";
7
7
  import { unreachable } from "../utils.js";
8
8
  import { CUSTOM_BLOCKS_SDK_VERSION } from "../version.js";
9
9
  import { NOTION_DARK_BACKGROUND_BASE, NOTION_LIGHT_BACKGROUND_BASE, } from "./appearance.js";
10
+ import { resolveDataSourceQuery } from "./dataSources/query.js";
10
11
  import { resolveDataSources } from "./dataSources/resolve.js";
11
12
  import { resolvePropertyWriteMapForDataSource } from "./dataSources/resolveProperty.js";
12
13
  import { createEmptyDataSourceQueryState, } from "./hostState.js";
@@ -169,12 +170,12 @@ export class SandboxBridge {
169
170
  nextBindings,
170
171
  });
171
172
  this.latestDataSourceBindings = nextBindings;
172
- // Drop cached query state for keys that no longer exist in the mapping.
173
+ // Drop cached query state for subscriptions whose key no longer exists.
173
174
  const nextKeys = new Set(dataSources.map(s => s.key));
174
175
  const prunedState = {};
175
- for (const [key, state] of Object.entries(hostState.dataSourceState)) {
176
- if (nextKeys.has(key)) {
177
- prunedState[key] = state;
176
+ for (const [subscriptionId, state] of Object.entries(hostState.dataSourceState)) {
177
+ if (nextKeys.has(state.dataSourceKey)) {
178
+ prunedState[subscriptionId] = state;
178
179
  }
179
180
  }
180
181
  this.hostState = {
@@ -231,11 +232,10 @@ export class SandboxBridge {
231
232
  return;
232
233
  }
233
234
  case "queryDataSourceResult": {
234
- const queryEntry = Object.entries(hostState.dataSourceState).find(([, state]) => state.subscriptionId === message.subscriptionId);
235
- if (queryEntry === undefined) {
235
+ const currentState = hostState.dataSourceState[message.subscriptionId];
236
+ if (currentState === undefined) {
236
237
  return;
237
238
  }
238
- const [key, currentState] = queryEntry;
239
239
  const queryResult = message.status === "error"
240
240
  ? {
241
241
  items: [],
@@ -251,13 +251,14 @@ export class SandboxBridge {
251
251
  ...hostState,
252
252
  dataSourceState: {
253
253
  ...hostState.dataSourceState,
254
- [key]: {
254
+ [message.subscriptionId]: {
255
+ dataSourceKey: currentState.dataSourceKey,
255
256
  items: queryResult.items,
256
257
  isLoading: false,
257
258
  hasMore: queryResult.hasMore,
258
259
  error: queryResult.error,
259
- subscriptionId: message.subscriptionId,
260
260
  latestLimit: currentState.latestLimit,
261
+ latestQueryIdentity: currentState.latestQueryIdentity,
261
262
  },
262
263
  },
263
264
  };
@@ -512,14 +513,20 @@ export class SandboxBridge {
512
513
  requestAnimationFrame(acknowledge);
513
514
  }
514
515
  }
515
- queryDataSource(key, options = {}) {
516
+ createDataSourceSubscriptionId() {
517
+ return `data-source:${globalThis.crypto.randomUUID()}`;
518
+ }
519
+ queryDataSource(subscriptionId, key, options = {}) {
516
520
  if (this.hostState.status !== "initialized") {
517
521
  return;
518
522
  }
519
523
  const dataSource = this.hostState.dataSources.find(entry => entry.key === key);
520
- const currentState = this.hostState.dataSourceState[key] ?? createEmptyDataSourceQueryState();
524
+ const subscriptionState = this.hostState.dataSourceState[subscriptionId];
525
+ const currentState = subscriptionState?.dataSourceKey === key
526
+ ? subscriptionState
527
+ : createEmptyDataSourceQueryState(key);
521
528
  if (dataSource === undefined) {
522
- this.setDataSourceQueryError(key, currentState, {
529
+ this.setDataSourceQueryError(subscriptionId, currentState, {
523
530
  code: "unknown_data_source_key",
524
531
  message: `Unknown data source key "${key}". Known keys: [${this.hostState.dataSources.map(entry => entry.key).join(", ")}].`,
525
532
  isRetryable: false,
@@ -527,32 +534,42 @@ export class SandboxBridge {
527
534
  return;
528
535
  }
529
536
  if (dataSource.collectionPointer === undefined) {
530
- this.setDataSourceQueryError(key, currentState, {
537
+ this.setDataSourceQueryError(subscriptionId, currentState, {
531
538
  code: "unmapped_data_source",
532
539
  message: `Data source "${key}" has not been mapped to a database yet.`,
533
540
  isRetryable: false,
534
541
  });
535
542
  return;
536
543
  }
537
- const limit = resolveDataSourceQueryLimit(options.limit);
538
- const subscriptionId = makeDataSourceSubscriptionId({
544
+ const resolvedQuery = resolveDataSourceQuery({
545
+ dataSources: this.hostState.dataSources,
539
546
  key,
547
+ options,
540
548
  });
549
+ if (resolvedQuery.status === "error") {
550
+ this.setDataSourceQueryError(subscriptionId, currentState, {
551
+ code: "invalid_data_source_query",
552
+ message: resolvedQuery.error,
553
+ isRetryable: false,
554
+ });
555
+ return;
556
+ }
557
+ const query = resolvedQuery.query;
541
558
  if (currentState.isLoading &&
542
- currentState.subscriptionId === subscriptionId &&
543
- currentState.latestLimit === limit) {
559
+ currentState.latestQueryIdentity === query.identity) {
544
560
  return;
545
561
  }
546
562
  this.hostState = {
547
563
  ...this.hostState,
548
564
  dataSourceState: {
549
565
  ...this.hostState.dataSourceState,
550
- [key]: {
566
+ [subscriptionId]: {
551
567
  ...currentState,
568
+ dataSourceKey: key,
552
569
  isLoading: true,
553
570
  error: undefined,
554
- subscriptionId,
555
- latestLimit: limit,
571
+ latestLimit: query.limit,
572
+ latestQueryIdentity: query.identity,
556
573
  },
557
574
  },
558
575
  };
@@ -560,12 +577,13 @@ export class SandboxBridge {
560
577
  const outbound = {
561
578
  type: "queryDataSource",
562
579
  subscriptionId,
563
- dataSourceId: dataSource.collectionPointer.id,
564
- limit,
580
+ dataSourceId: query.dataSourceId,
581
+ limit: query.limit,
582
+ ...(query.filter !== undefined ? { filter: query.filter } : {}),
565
583
  };
566
584
  this.postToHost(outbound);
567
585
  }
568
- setDataSourceQueryError(key, currentState, error) {
586
+ setDataSourceQueryError(subscriptionId, currentState, error) {
569
587
  if (this.hostState.status !== "initialized") {
570
588
  return;
571
589
  }
@@ -573,17 +591,28 @@ export class SandboxBridge {
573
591
  ...this.hostState,
574
592
  dataSourceState: {
575
593
  ...this.hostState.dataSourceState,
576
- [key]: {
594
+ [subscriptionId]: {
577
595
  ...currentState,
578
596
  isLoading: false,
579
597
  error,
580
- subscriptionId: undefined,
581
598
  latestLimit: undefined,
599
+ latestQueryIdentity: undefined,
582
600
  },
583
601
  },
584
602
  };
585
603
  this.notify();
586
604
  }
605
+ releaseDataSourceSubscription(subscriptionId) {
606
+ if (this.hostState.status !== "initialized" ||
607
+ this.hostState.dataSourceState[subscriptionId] === undefined) {
608
+ return;
609
+ }
610
+ const { [subscriptionId]: _, ...dataSourceState } = this.hostState.dataSourceState;
611
+ this.hostState = {
612
+ ...this.hostState,
613
+ dataSourceState,
614
+ };
615
+ }
587
616
  postResize(height) {
588
617
  if (typeof window === "undefined") {
589
618
  return;
@@ -833,28 +862,6 @@ function reuseDataSourcesForUnchangedBindings(args) {
833
862
  return dataSource;
834
863
  });
835
864
  }
836
- // The default number of items to return in a live snapshot response if no limit is provided.
837
- const DEFAULT_DATA_SOURCE_QUERY_LIMIT = 20;
838
- // The maximum number of items to return in a single live snapshot response.
839
- const MAX_DATA_SOURCE_QUERY_LIMIT = 999;
840
- function resolveDataSourceQueryLimit(limit) {
841
- if (limit === undefined) {
842
- return DEFAULT_DATA_SOURCE_QUERY_LIMIT;
843
- }
844
- if (!Number.isFinite(limit) || !Number.isInteger(limit) || limit < 1) {
845
- console.warn(`[custom-blocks-sdk] useDataSource limit must be a positive integer; using ${DEFAULT_DATA_SOURCE_QUERY_LIMIT}.`);
846
- return DEFAULT_DATA_SOURCE_QUERY_LIMIT;
847
- }
848
- if (limit > MAX_DATA_SOURCE_QUERY_LIMIT) {
849
- console.warn(`[custom-blocks-sdk] useDataSource limit is capped at ${MAX_DATA_SOURCE_QUERY_LIMIT}.`);
850
- return MAX_DATA_SOURCE_QUERY_LIMIT;
851
- }
852
- return limit;
853
- }
854
- function makeDataSourceSubscriptionId(args) {
855
- const { key } = args;
856
- return `data-source:${encodeURIComponent(key)}`;
857
- }
858
865
  function invalidInitPayloadError(issues) {
859
866
  return {
860
867
  code: "invalid_init_payload",
@@ -0,0 +1,26 @@
1
+ import type { NotionDataSource } from "../../protocol/dataSources/dataSource.js";
2
+ import type { NotionDataSourceId } from "../../protocol/ids.js";
3
+ import { type ResolvedCustomBlockDataSourceFilter } from "../../protocol/messages/queryDataSource.js";
4
+ import type { UseDataSourceOptions } from "../../types.js";
5
+ export type ResolvedDataSourceQuery = {
6
+ dataSourceId: NotionDataSourceId;
7
+ limit: number;
8
+ filter?: ResolvedCustomBlockDataSourceFilter;
9
+ identity: string;
10
+ };
11
+ export type ResolveDataSourceQueryResult = {
12
+ status: "error";
13
+ error: string;
14
+ } | {
15
+ status: "ok";
16
+ dataSource: NotionDataSource;
17
+ query: ResolvedDataSourceQuery;
18
+ };
19
+ export declare function resolveDataSourceQuery(args: {
20
+ dataSources: NotionDataSource[];
21
+ key: string;
22
+ options?: unknown;
23
+ warn?: (message: string) => void;
24
+ }): ResolveDataSourceQueryResult;
25
+ export declare function getDataSourceQueryOptionsIdentity(options: UseDataSourceOptions | undefined): string;
26
+ //# sourceMappingURL=query.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"query.d.ts","sourceRoot":"","sources":["../../../../../home/runner/work/custom-blocks/custom-blocks/sdk/src/bridge/dataSources/query.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,4DAA4D,CAAA;AAClG,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yCAAyC,CAAA;AACjF,OAAO,EAON,KAAK,mCAAmC,EAExC,MAAM,8DAA8D,CAAA;AAErE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAA;AAM1D,MAAM,MAAM,uBAAuB,GAAG;IACrC,YAAY,EAAE,kBAAkB,CAAA;IAChC,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,CAAC,EAAE,mCAAmC,CAAA;IAC5C,QAAQ,EAAE,MAAM,CAAA;CAChB,CAAA;AAED,MAAM,MAAM,4BAA4B,GACrC;IAAE,MAAM,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAClC;IACA,MAAM,EAAE,IAAI,CAAA;IACZ,UAAU,EAAE,gBAAgB,CAAA;IAC5B,KAAK,EAAE,uBAAuB,CAAA;CAC7B,CAAA;AAEJ,wBAAgB,sBAAsB,CAAC,IAAI,EAAE;IAC5C,WAAW,EAAE,gBAAgB,EAAE,CAAA;IAC/B,GAAG,EAAE,MAAM,CAAA;IACX,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;CAChC,GAAG,4BAA4B,CAuD/B;AA6BD,wBAAgB,iCAAiC,CAChD,OAAO,EAAE,oBAAoB,GAAG,SAAS,GACvC,MAAM,CAMR"}
@@ -0,0 +1,377 @@
1
+ import { customBlockCheckboxFilterOperatorSchema, customBlockContainsFilterOperatorSchema, customBlockDateFilterOperatorSchema, customBlockNumberFilterOperatorSchema, customBlockOptionFilterOperatorSchema, customBlockTextFilterOperatorSchema, } from "../../protocol/messages/queryDataSource.js";
2
+ import * as v from "valibot";
3
+ const DEFAULT_DATA_SOURCE_QUERY_LIMIT = 20;
4
+ const MAX_DATA_SOURCE_QUERY_LIMIT = 999;
5
+ const MAX_DATA_SOURCE_QUERY_FILTER_CHILDREN = 25;
6
+ export function resolveDataSourceQuery(args) {
7
+ const { dataSources, key, options, warn } = args;
8
+ if (options !== undefined && !isPlainRecord(options)) {
9
+ return {
10
+ status: "error",
11
+ error: "Data source query options must be an object.",
12
+ };
13
+ }
14
+ if (options !== undefined &&
15
+ Object.keys(options).some(key => !["limit", "filter"].includes(key))) {
16
+ return {
17
+ status: "error",
18
+ error: "Data source query options contain unsupported fields.",
19
+ };
20
+ }
21
+ const dataSource = dataSources.find(entry => entry.key === key);
22
+ if (dataSource === undefined) {
23
+ return {
24
+ status: "error",
25
+ error: `Unknown data source key "${key}". Known keys: [${dataSources.map(entry => entry.key).join(", ")}].`,
26
+ };
27
+ }
28
+ if (dataSource.collectionPointer === undefined) {
29
+ return {
30
+ status: "error",
31
+ error: `Data source "${key}" has not been mapped to a database yet.`,
32
+ };
33
+ }
34
+ const limit = resolveDataSourceQueryLimit(options?.limit, warn);
35
+ if (limit.status === "error") {
36
+ return limit;
37
+ }
38
+ const filter = resolveFilter(options?.filter, dataSource);
39
+ if (filter.status === "error") {
40
+ return filter;
41
+ }
42
+ const identity = stableJsonStringify({
43
+ dataSourceId: dataSource.collectionPointer.id,
44
+ limit: limit.limit,
45
+ filter: filter.filter ?? null,
46
+ });
47
+ return {
48
+ status: "ok",
49
+ dataSource,
50
+ query: {
51
+ dataSourceId: dataSource.collectionPointer.id,
52
+ limit: limit.limit,
53
+ filter: filter.filter,
54
+ identity,
55
+ },
56
+ };
57
+ }
58
+ function resolveDataSourceQueryLimit(limit, warn = console.warn) {
59
+ if (limit === undefined) {
60
+ return { status: "ok", limit: DEFAULT_DATA_SOURCE_QUERY_LIMIT };
61
+ }
62
+ if (typeof limit !== "number" ||
63
+ !Number.isFinite(limit) ||
64
+ !Number.isInteger(limit) ||
65
+ limit < 1) {
66
+ return {
67
+ status: "error",
68
+ error: `Data source query limit must be a positive integer between 1 and ${MAX_DATA_SOURCE_QUERY_LIMIT}.`,
69
+ };
70
+ }
71
+ if (limit > MAX_DATA_SOURCE_QUERY_LIMIT) {
72
+ warn?.(`Data source query limit ${limit} exceeds the maximum of ${MAX_DATA_SOURCE_QUERY_LIMIT}; clamping to ${MAX_DATA_SOURCE_QUERY_LIMIT}.`);
73
+ return { status: "ok", limit: MAX_DATA_SOURCE_QUERY_LIMIT };
74
+ }
75
+ return { status: "ok", limit };
76
+ }
77
+ export function getDataSourceQueryOptionsIdentity(options) {
78
+ try {
79
+ return stableJsonStringify(options ?? {});
80
+ }
81
+ catch {
82
+ return "invalid-data-source-query-options";
83
+ }
84
+ }
85
+ function stableJsonStringify(value) {
86
+ return JSON.stringify(normalizeJsonValue(value));
87
+ }
88
+ function resolveFilter(filter, dataSource) {
89
+ if (filter === undefined) {
90
+ return { status: "ok" };
91
+ }
92
+ if (!isPlainRecord(filter)) {
93
+ return {
94
+ status: "error",
95
+ error: "Data source query filter must be an object.",
96
+ };
97
+ }
98
+ if ("and" in filter) {
99
+ if (Object.keys(filter).length !== 1 || !Array.isArray(filter.and)) {
100
+ return {
101
+ status: "error",
102
+ error: 'Data source query filter "and" must be an array.',
103
+ };
104
+ }
105
+ if (filter.and.length > MAX_DATA_SOURCE_QUERY_FILTER_CHILDREN) {
106
+ return {
107
+ status: "error",
108
+ error: `Data source query filter "and" may contain at most ${MAX_DATA_SOURCE_QUERY_FILTER_CHILDREN} children.`,
109
+ };
110
+ }
111
+ const and = [];
112
+ for (const child of filter.and) {
113
+ const resolved = resolvePropertyFilter(child, dataSource);
114
+ if (resolved.status === "error") {
115
+ return resolved;
116
+ }
117
+ and.push(resolved.filter);
118
+ }
119
+ return { status: "ok", filter: { and } };
120
+ }
121
+ return resolvePropertyFilter(filter, dataSource);
122
+ }
123
+ function resolvePropertyFilter(filter, dataSource) {
124
+ if (!isPlainRecord(filter)) {
125
+ return {
126
+ status: "error",
127
+ error: "Data source query property filter must be an object.",
128
+ };
129
+ }
130
+ const property = resolvePropertyAddress(filter, dataSource);
131
+ if (property.status === "error") {
132
+ return property;
133
+ }
134
+ const branchKeys = Object.keys(filter).filter(key => key !== "key" && key !== "propertyId");
135
+ if (branchKeys.length !== 1) {
136
+ return {
137
+ status: "error",
138
+ error: "Data source query property filter must contain exactly one property branch.",
139
+ };
140
+ }
141
+ const branch = branchKeys[0];
142
+ const value = filter[branch];
143
+ if (!isPlainRecord(value) || Object.keys(value).length !== 1) {
144
+ return invalidOperator(branch);
145
+ }
146
+ if (property.propertyType !== branch) {
147
+ return {
148
+ status: "error",
149
+ error: `Data source query filter branch "${branch}" does not match property type "${property.propertyType}".`,
150
+ };
151
+ }
152
+ switch (branch) {
153
+ case "title": {
154
+ const parsed = v.safeParse(customBlockTextFilterOperatorSchema, value);
155
+ return parsed.success
156
+ ? {
157
+ status: "ok",
158
+ filter: { propertyId: property.propertyId, title: parsed.output },
159
+ }
160
+ : invalidOperator(branch);
161
+ }
162
+ case "rich_text": {
163
+ const parsed = v.safeParse(customBlockTextFilterOperatorSchema, value);
164
+ return parsed.success
165
+ ? {
166
+ status: "ok",
167
+ filter: {
168
+ propertyId: property.propertyId,
169
+ rich_text: parsed.output,
170
+ },
171
+ }
172
+ : invalidOperator(branch);
173
+ }
174
+ case "url":
175
+ case "email":
176
+ case "phone_number": {
177
+ const parsed = v.safeParse(customBlockTextFilterOperatorSchema, value);
178
+ if (!parsed.success) {
179
+ return invalidOperator(branch);
180
+ }
181
+ if (branch === "url") {
182
+ return {
183
+ status: "ok",
184
+ filter: { propertyId: property.propertyId, url: parsed.output },
185
+ };
186
+ }
187
+ if (branch === "email") {
188
+ return {
189
+ status: "ok",
190
+ filter: { propertyId: property.propertyId, email: parsed.output },
191
+ };
192
+ }
193
+ return {
194
+ status: "ok",
195
+ filter: {
196
+ propertyId: property.propertyId,
197
+ phone_number: parsed.output,
198
+ },
199
+ };
200
+ }
201
+ case "number": {
202
+ const parsed = v.safeParse(customBlockNumberFilterOperatorSchema, value);
203
+ if (parsed.success &&
204
+ Object.values(parsed.output).every(entry => typeof entry !== "number" || Number.isFinite(entry))) {
205
+ return {
206
+ status: "ok",
207
+ filter: { propertyId: property.propertyId, number: parsed.output },
208
+ };
209
+ }
210
+ return invalidOperator(branch);
211
+ }
212
+ case "checkbox": {
213
+ const parsed = v.safeParse(customBlockCheckboxFilterOperatorSchema, value);
214
+ return parsed.success
215
+ ? {
216
+ status: "ok",
217
+ filter: {
218
+ propertyId: property.propertyId,
219
+ checkbox: parsed.output,
220
+ },
221
+ }
222
+ : invalidOperator(branch);
223
+ }
224
+ case "select": {
225
+ const parsed = v.safeParse(customBlockOptionFilterOperatorSchema, value);
226
+ return parsed.success
227
+ ? {
228
+ status: "ok",
229
+ filter: { propertyId: property.propertyId, select: parsed.output },
230
+ }
231
+ : invalidOperator(branch);
232
+ }
233
+ case "multi_select": {
234
+ const parsed = v.safeParse(customBlockContainsFilterOperatorSchema, value);
235
+ return parsed.success
236
+ ? {
237
+ status: "ok",
238
+ filter: {
239
+ propertyId: property.propertyId,
240
+ multi_select: parsed.output,
241
+ },
242
+ }
243
+ : invalidOperator(branch);
244
+ }
245
+ case "status": {
246
+ const parsed = v.safeParse(customBlockOptionFilterOperatorSchema, value);
247
+ return parsed.success
248
+ ? {
249
+ status: "ok",
250
+ filter: { propertyId: property.propertyId, status: parsed.output },
251
+ }
252
+ : invalidOperator(branch);
253
+ }
254
+ case "date": {
255
+ const parsed = v.safeParse(customBlockDateFilterOperatorSchema, value);
256
+ if (parsed.success &&
257
+ Object.values(parsed.output).every(entry => entry === true || isValidIsoDate(entry))) {
258
+ return {
259
+ status: "ok",
260
+ filter: { propertyId: property.propertyId, date: parsed.output },
261
+ };
262
+ }
263
+ return invalidOperator(branch);
264
+ }
265
+ default:
266
+ return {
267
+ status: "error",
268
+ error: `Data source query filter branch "${branch}" is not supported.`,
269
+ };
270
+ }
271
+ }
272
+ function resolvePropertyAddress(value, dataSource) {
273
+ const hasKey = "key" in value;
274
+ const hasPropertyId = "propertyId" in value;
275
+ if (hasKey === hasPropertyId) {
276
+ return {
277
+ status: "error",
278
+ error: "Data source query filters must use exactly one of key or propertyId.",
279
+ };
280
+ }
281
+ let propertyId;
282
+ if (hasKey) {
283
+ if (typeof value.key !== "string") {
284
+ return {
285
+ status: "error",
286
+ error: "Data source query property key must be a string.",
287
+ };
288
+ }
289
+ if (!(value.key in dataSource.propertyIdsByKey)) {
290
+ return {
291
+ status: "error",
292
+ error: `Unknown property key "${value.key}" for data source "${dataSource.key}".`,
293
+ };
294
+ }
295
+ const resolvedPropertyId = dataSource.propertyIdsByKey[value.key];
296
+ if (resolvedPropertyId === undefined) {
297
+ return {
298
+ status: "error",
299
+ error: `Property key "${value.key}" for data source "${dataSource.key}" is not bound.`,
300
+ };
301
+ }
302
+ propertyId = resolvedPropertyId;
303
+ }
304
+ else {
305
+ if (typeof value.propertyId !== "string") {
306
+ return {
307
+ status: "error",
308
+ error: "Data source query propertyId must be a string.",
309
+ };
310
+ }
311
+ propertyId = value.propertyId;
312
+ }
313
+ const propertySchema = dataSource.propertySchemasById[propertyId];
314
+ if (propertySchema === undefined) {
315
+ return {
316
+ status: "error",
317
+ error: `Unknown property ID "${propertyId}" for data source "${dataSource.key}".`,
318
+ };
319
+ }
320
+ return {
321
+ status: "ok",
322
+ propertyId,
323
+ propertyType: propertySchema.type,
324
+ };
325
+ }
326
+ function invalidOperator(branch) {
327
+ return {
328
+ status: "error",
329
+ error: `Data source query filter branch "${branch}" has an invalid operator or value.`,
330
+ };
331
+ }
332
+ function normalizeJsonValue(value) {
333
+ if (value === null ||
334
+ typeof value === "string" ||
335
+ typeof value === "boolean") {
336
+ return value;
337
+ }
338
+ if (typeof value === "number") {
339
+ if (!Number.isFinite(value)) {
340
+ throw new Error("Data source query values must be finite JSON numbers.");
341
+ }
342
+ return value;
343
+ }
344
+ if (Array.isArray(value)) {
345
+ return value.map(entry => normalizeJsonValue(entry));
346
+ }
347
+ if (isPlainRecord(value)) {
348
+ const normalized = {};
349
+ for (const key of Object.keys(value).sort()) {
350
+ const child = value[key];
351
+ if (child !== undefined) {
352
+ normalized[key] = normalizeJsonValue(child);
353
+ }
354
+ }
355
+ return normalized;
356
+ }
357
+ throw new Error("Data source query values must be JSON-compatible.");
358
+ }
359
+ function isPlainRecord(value) {
360
+ return (typeof value === "object" &&
361
+ value !== null &&
362
+ !Array.isArray(value) &&
363
+ (Object.getPrototypeOf(value) === Object.prototype ||
364
+ Object.getPrototypeOf(value) === null));
365
+ }
366
+ function isValidIsoDate(value) {
367
+ if (typeof value !== "string") {
368
+ return false;
369
+ }
370
+ const dateOnly = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
371
+ if (dateOnly !== null) {
372
+ const [, year, month, day] = dateOnly;
373
+ const date = new Date(Date.UTC(Number(year), Number(month) - 1, Number(day)));
374
+ return date.toISOString().slice(0, 10) === value;
375
+ }
376
+ return (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d{1,3})?)?(?:Z|[+-]\d{2}:\d{2})$/.test(value) && Number.isFinite(Date.parse(value)));
377
+ }
@@ -29,15 +29,16 @@ export type InitializedHostState = {
29
29
  dataSourceState: Record<string, DataSourceQueryState>;
30
30
  };
31
31
  export type DataSourceQueryState = {
32
+ dataSourceKey: string;
32
33
  /** Latest pages from the host as parsed from the bridge. `propertiesByKey` is derived lazily. */
33
34
  items: NotionDataSourcePageBridge[];
34
35
  isLoading: boolean;
35
36
  hasMore: boolean;
36
37
  error?: CustomBlockQueryDataSourceErrorInfo;
37
- subscriptionId?: string;
38
38
  latestLimit?: number;
39
+ latestQueryIdentity?: string;
39
40
  };
40
- export declare function createEmptyDataSourceQueryState(): DataSourceQueryState;
41
+ export declare function createEmptyDataSourceQueryState(dataSourceKey: string): DataSourceQueryState;
41
42
  /**
42
43
  * Resolved page + schema views for a single data source, keyed by raw property
43
44
  * ID and by user-defined key. Re-derived from `propertyIdsByKey` on every read so
@@ -69,5 +70,5 @@ export type UpdateDataSourcePageFn = (args: {
69
70
  pageId: NotionPageId;
70
71
  pageUpdateArgs: NotionDataSourcePageUpdateArgs;
71
72
  }) => Promise<UpdatePageResult>;
72
- export declare function getDataSourceQueryView(hostState: CustomBlockHostState, key: string, updateDataSourcePage: UpdateDataSourcePageFn): DataSourceQueryView;
73
+ export declare function getDataSourceQueryView(hostState: CustomBlockHostState, key: string, subscriptionId: string, updateDataSourcePage: UpdateDataSourcePageFn): DataSourceQueryView;
73
74
  //# sourceMappingURL=hostState.d.ts.map