@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
|
@@ -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
|
-
|
|
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":"AAEA,OAAO,KAAK,EACX,gBAAgB,EAEhB,MAAM,4DAA4D,CAAA;AACnE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,yCAAyC,CAAA;AAW3E,OAAO,EAEN,KAAK,WAAW,EAEhB,MAAM,mDAAmD,CAAA;AAc1D,OAAO,KAAK,EACX,cAAc,EACd,gBAAgB,EAChB,aAAa,EACb,aAAa,EACb,aAAa,EACb,eAAe,EACf,8BAA8B,EAC9B,YAAY,EACZ,cAAc,EACd,gBAAgB,EAChB,oBAAoB,EACpB,MAAM,aAAa,CAAA;
|
|
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
|
|
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 [
|
|
176
|
-
if (nextKeys.has(
|
|
177
|
-
prunedState[
|
|
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
|
|
235
|
-
if (
|
|
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
|
-
[
|
|
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
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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
|
|
538
|
-
|
|
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.
|
|
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
|
-
[
|
|
566
|
+
[subscriptionId]: {
|
|
551
567
|
...currentState,
|
|
568
|
+
dataSourceKey: key,
|
|
552
569
|
isLoading: true,
|
|
553
570
|
error: undefined,
|
|
554
|
-
|
|
555
|
-
|
|
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:
|
|
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(
|
|
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
|
-
[
|
|
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
|