@notionhq/custom-blocks 0.0.58
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/HOST.md +171 -0
- package/README.md +57 -0
- package/bin/cli/attach.js +180 -0
- package/bin/cli/cli.js +248 -0
- package/bin/cli/create.js +106 -0
- package/bin/cli/datasources.js +255 -0
- package/bin/cli/deploy.js +109 -0
- package/bin/cli/ids.js +13 -0
- package/bin/cli/ntn.js +95 -0
- package/bin/cli/pullData.js +76 -0
- package/bin/cli/pullManifest.js +158 -0
- package/bin/cli/target.js +95 -0
- package/bin/src/bridge/dataSources/propertySchema.js +148 -0
- package/bin/src/bridge/manifest.js +40 -0
- package/dist/autoResize.d.ts +4 -0
- package/dist/autoResize.d.ts.map +1 -0
- package/dist/autoResize.js +71 -0
- package/dist/bridge/SandboxBridge.d.ts +92 -0
- package/dist/bridge/SandboxBridge.d.ts.map +1 -0
- package/dist/bridge/SandboxBridge.js +718 -0
- package/dist/bridge/dataSources/dataSource.d.ts +732 -0
- package/dist/bridge/dataSources/dataSource.d.ts.map +1 -0
- package/dist/bridge/dataSources/dataSource.js +37 -0
- package/dist/bridge/dataSources/dataSourcePage.d.ts +118 -0
- package/dist/bridge/dataSources/dataSourcePage.d.ts.map +1 -0
- package/dist/bridge/dataSources/dataSourcePage.js +14 -0
- package/dist/bridge/dataSources/dataSourceValue.d.ts +67 -0
- package/dist/bridge/dataSources/dataSourceValue.d.ts.map +1 -0
- package/dist/bridge/dataSources/dataSourceValue.js +14 -0
- package/dist/bridge/dataSources/dateValue.d.ts +158 -0
- package/dist/bridge/dataSources/dateValue.d.ts.map +1 -0
- package/dist/bridge/dataSources/dateValue.js +59 -0
- package/dist/bridge/dataSources/errors.d.ts +6 -0
- package/dist/bridge/dataSources/errors.d.ts.map +1 -0
- package/dist/bridge/dataSources/errors.js +4 -0
- package/dist/bridge/dataSources/propertySchema.d.ts +191 -0
- package/dist/bridge/dataSources/propertySchema.d.ts.map +1 -0
- package/dist/bridge/dataSources/propertySchema.js +148 -0
- package/dist/bridge/dataSources/recordPointer.d.ts +10 -0
- package/dist/bridge/dataSources/recordPointer.d.ts.map +1 -0
- package/dist/bridge/dataSources/recordPointer.js +8 -0
- package/dist/bridge/dataSources/resolve.d.ts +22 -0
- package/dist/bridge/dataSources/resolve.d.ts.map +1 -0
- package/dist/bridge/dataSources/resolve.js +65 -0
- package/dist/bridge/dataSources/resolveProperty.d.ts +41 -0
- package/dist/bridge/dataSources/resolveProperty.d.ts.map +1 -0
- package/dist/bridge/dataSources/resolveProperty.js +73 -0
- package/dist/bridge/errors.d.ts +12 -0
- package/dist/bridge/errors.d.ts.map +1 -0
- package/dist/bridge/errors.js +6 -0
- package/dist/bridge/hostState.d.ts +65 -0
- package/dist/bridge/hostState.d.ts.map +1 -0
- package/dist/bridge/hostState.js +65 -0
- package/dist/bridge/ids.d.ts +39 -0
- package/dist/bridge/ids.d.ts.map +1 -0
- package/dist/bridge/ids.js +5 -0
- package/dist/bridge/incomingType.d.ts +10 -0
- package/dist/bridge/incomingType.d.ts.map +1 -0
- package/dist/bridge/incomingType.js +17 -0
- package/dist/bridge/loadManifest.d.ts +25 -0
- package/dist/bridge/loadManifest.d.ts.map +1 -0
- package/dist/bridge/loadManifest.js +68 -0
- package/dist/bridge/manifest.d.ts +66 -0
- package/dist/bridge/manifest.d.ts.map +1 -0
- package/dist/bridge/manifest.js +40 -0
- package/dist/bridge/messages/createPage.d.ts +225 -0
- package/dist/bridge/messages/createPage.d.ts.map +1 -0
- package/dist/bridge/messages/createPage.js +46 -0
- package/dist/bridge/messages/createPageResult.d.ts +211 -0
- package/dist/bridge/messages/createPageResult.d.ts.map +1 -0
- package/dist/bridge/messages/createPageResult.js +28 -0
- package/dist/bridge/messages/currentUserChanged.d.ts +19 -0
- package/dist/bridge/messages/currentUserChanged.d.ts.map +1 -0
- package/dist/bridge/messages/currentUserChanged.js +9 -0
- package/dist/bridge/messages/dataSourcesChanged.d.ts +158 -0
- package/dist/bridge/messages/dataSourcesChanged.d.ts.map +1 -0
- package/dist/bridge/messages/dataSourcesChanged.js +13 -0
- package/dist/bridge/messages/getPage.d.ts +216 -0
- package/dist/bridge/messages/getPage.d.ts.map +1 -0
- package/dist/bridge/messages/getPage.js +33 -0
- package/dist/bridge/messages/getUser.d.ts +45 -0
- package/dist/bridge/messages/getUser.d.ts.map +1 -0
- package/dist/bridge/messages/getUser.js +32 -0
- package/dist/bridge/messages/hostToSandbox.d.ts +1089 -0
- package/dist/bridge/messages/hostToSandbox.d.ts.map +1 -0
- package/dist/bridge/messages/hostToSandbox.js +32 -0
- package/dist/bridge/messages/init.d.ts +209 -0
- package/dist/bridge/messages/init.d.ts.map +1 -0
- package/dist/bridge/messages/init.js +47 -0
- package/dist/bridge/messages/invalidHostMessage.d.ts +15 -0
- package/dist/bridge/messages/invalidHostMessage.d.ts.map +1 -0
- package/dist/bridge/messages/invalidHostMessage.js +13 -0
- package/dist/bridge/messages/invalidSandboxMessage.d.ts +15 -0
- package/dist/bridge/messages/invalidSandboxMessage.d.ts.map +1 -0
- package/dist/bridge/messages/invalidSandboxMessage.js +13 -0
- package/dist/bridge/messages/listUsers.d.ts +53 -0
- package/dist/bridge/messages/listUsers.d.ts.map +1 -0
- package/dist/bridge/messages/listUsers.js +33 -0
- package/dist/bridge/messages/pageChanged.d.ts +12 -0
- package/dist/bridge/messages/pageChanged.d.ts.map +1 -0
- package/dist/bridge/messages/pageChanged.js +9 -0
- package/dist/bridge/messages/parentChanged.d.ts +22 -0
- package/dist/bridge/messages/parentChanged.d.ts.map +1 -0
- package/dist/bridge/messages/parentChanged.js +9 -0
- package/dist/bridge/messages/queryDataSource.d.ts +20 -0
- package/dist/bridge/messages/queryDataSource.d.ts.map +1 -0
- package/dist/bridge/messages/queryDataSource.js +19 -0
- package/dist/bridge/messages/queryDataSourceResult.d.ts +90 -0
- package/dist/bridge/messages/queryDataSourceResult.d.ts.map +1 -0
- package/dist/bridge/messages/queryDataSourceResult.js +21 -0
- package/dist/bridge/messages/ready.d.ts +45 -0
- package/dist/bridge/messages/ready.d.ts.map +1 -0
- package/dist/bridge/messages/ready.js +37 -0
- package/dist/bridge/messages/resize.d.ts +12 -0
- package/dist/bridge/messages/resize.d.ts.map +1 -0
- package/dist/bridge/messages/resize.js +10 -0
- package/dist/bridge/messages/sandboxToHost.d.ts +410 -0
- package/dist/bridge/messages/sandboxToHost.d.ts.map +1 -0
- package/dist/bridge/messages/sandboxToHost.js +25 -0
- package/dist/bridge/messages/themeChanged.d.ts +11 -0
- package/dist/bridge/messages/themeChanged.d.ts.map +1 -0
- package/dist/bridge/messages/themeChanged.js +10 -0
- package/dist/bridge/messages/updatePage.d.ts +171 -0
- package/dist/bridge/messages/updatePage.d.ts.map +1 -0
- package/dist/bridge/messages/updatePage.js +15 -0
- package/dist/bridge/messages/updatePageResult.d.ts +210 -0
- package/dist/bridge/messages/updatePageResult.d.ts.map +1 -0
- package/dist/bridge/messages/updatePageResult.js +27 -0
- package/dist/bridge/pages/page.d.ts +600 -0
- package/dist/bridge/pages/page.d.ts.map +1 -0
- package/dist/bridge/pages/page.js +217 -0
- package/dist/bridge/parent.d.ts +25 -0
- package/dist/bridge/parent.d.ts.map +1 -0
- package/dist/bridge/parent.js +11 -0
- package/dist/bridge/pendingRequests.d.ts +14 -0
- package/dist/bridge/pendingRequests.d.ts.map +1 -0
- package/dist/bridge/pendingRequests.js +27 -0
- package/dist/bridge/sandboxClient.d.ts +63 -0
- package/dist/bridge/sandboxClient.d.ts.map +1 -0
- package/dist/bridge/sandboxClient.js +92 -0
- package/dist/bridge/theme.d.ts +4 -0
- package/dist/bridge/theme.d.ts.map +1 -0
- package/dist/bridge/theme.js +2 -0
- package/dist/bridge/users/user.d.ts +36 -0
- package/dist/bridge/users/user.d.ts.map +1 -0
- package/dist/bridge/users/user.js +19 -0
- package/dist/customBlock.d.ts +36 -0
- package/dist/customBlock.d.ts.map +1 -0
- package/dist/customBlock.js +65 -0
- package/dist/host/createCustomBlockHost.d.ts +34 -0
- package/dist/host/createCustomBlockHost.d.ts.map +1 -0
- package/dist/host/createCustomBlockHost.js +278 -0
- package/dist/host/lifecycle/initErrors.d.ts +10 -0
- package/dist/host/lifecycle/initErrors.d.ts.map +1 -0
- package/dist/host/lifecycle/initErrors.js +123 -0
- package/dist/host/lifecycle/ready.d.ts +8 -0
- package/dist/host/lifecycle/ready.d.ts.map +1 -0
- package/dist/host/lifecycle/ready.js +24 -0
- package/dist/host/lifecycle/types.d.ts +24 -0
- package/dist/host/lifecycle/types.d.ts.map +1 -0
- package/dist/host/lifecycle/types.js +1 -0
- package/dist/host/messages/invalidSandboxMessage.d.ts +7 -0
- package/dist/host/messages/invalidSandboxMessage.d.ts.map +1 -0
- package/dist/host/messages/invalidSandboxMessage.js +24 -0
- package/dist/host/messages/types.d.ts +60 -0
- package/dist/host/messages/types.d.ts.map +1 -0
- package/dist/host/messages/types.js +1 -0
- package/dist/host/queries/querySubscriptions.d.ts +20 -0
- package/dist/host/queries/querySubscriptions.d.ts.map +1 -0
- package/dist/host/queries/querySubscriptions.js +29 -0
- package/dist/host/queries/types.d.ts +9 -0
- package/dist/host/queries/types.d.ts.map +1 -0
- package/dist/host/queries/types.js +1 -0
- package/dist/host.d.ts +73 -0
- package/dist/host.d.ts.map +1 -0
- package/dist/host.js +42 -0
- package/dist/index.d.ts +37 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/init.d.ts +53 -0
- package/dist/init.d.ts.map +1 -0
- package/dist/init.js +87 -0
- package/dist/react/DebugMessageLog.d.ts +2 -0
- package/dist/react/DebugMessageLog.d.ts.map +1 -0
- package/dist/react/DebugMessageLog.js +22 -0
- package/dist/react/NotionCustomBlock.css +110 -0
- package/dist/react/NotionCustomBlock.d.ts +50 -0
- package/dist/react/NotionCustomBlock.d.ts.map +1 -0
- package/dist/react/NotionCustomBlock.js +81 -0
- package/dist/react/index.d.ts +14 -0
- package/dist/react/index.d.ts.map +1 -0
- package/dist/react/index.js +13 -0
- package/dist/react/standalonePreview.d.ts +2 -0
- package/dist/react/standalonePreview.d.ts.map +1 -0
- package/dist/react/standalonePreview.js +26 -0
- package/dist/react/useCustomBlockAutoResize.d.ts +30 -0
- package/dist/react/useCustomBlockAutoResize.d.ts.map +1 -0
- package/dist/react/useCustomBlockAutoResize.js +35 -0
- package/dist/react/useCustomBlockInit.d.ts +43 -0
- package/dist/react/useCustomBlockInit.d.ts.map +1 -0
- package/dist/react/useCustomBlockInit.js +60 -0
- package/dist/react/useDataSource.d.ts +13 -0
- package/dist/react/useDataSource.d.ts.map +1 -0
- package/dist/react/useDataSource.js +37 -0
- package/dist/react/useHostState.d.ts +2 -0
- package/dist/react/useHostState.d.ts.map +1 -0
- package/dist/react/useHostState.js +5 -0
- package/dist/react/useRuntimeState.d.ts +66 -0
- package/dist/react/useRuntimeState.d.ts.map +1 -0
- package/dist/react/useRuntimeState.js +73 -0
- package/dist/types.d.ts +154 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/dist/utils.d.ts +9 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +10 -0
- package/dist/version.d.ts +9 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +7 -0
- package/docs/block-location.md +83 -0
- package/docs/data-sources.md +186 -0
- package/docs/lifecycle.md +172 -0
- package/docs/manifest.md +42 -0
- package/docs/pages.md +143 -0
- package/docs/users.md +92 -0
- package/package.json +69 -0
- package/src/autoResize.ts +84 -0
- package/src/bridge/SandboxBridge.ts +921 -0
- package/src/bridge/dataSources/dataSource.ts +63 -0
- package/src/bridge/dataSources/dataSourcePage.ts +71 -0
- package/src/bridge/dataSources/dataSourceValue.ts +19 -0
- package/src/bridge/dataSources/dateValue.ts +96 -0
- package/src/bridge/dataSources/errors.ts +14 -0
- package/src/bridge/dataSources/propertySchema.ts +186 -0
- package/src/bridge/dataSources/recordPointer.ts +13 -0
- package/src/bridge/dataSources/resolve.ts +99 -0
- package/src/bridge/dataSources/resolveProperty.ts +130 -0
- package/src/bridge/errors.ts +13 -0
- package/src/bridge/hostState.ts +154 -0
- package/src/bridge/ids.ts +58 -0
- package/src/bridge/incomingType.ts +19 -0
- package/src/bridge/loadManifest.ts +89 -0
- package/src/bridge/manifest.ts +53 -0
- package/src/bridge/messages/createPage.ts +67 -0
- package/src/bridge/messages/createPageResult.ts +52 -0
- package/src/bridge/messages/currentUserChanged.ts +14 -0
- package/src/bridge/messages/dataSourcesChanged.ts +18 -0
- package/src/bridge/messages/getPage.ts +52 -0
- package/src/bridge/messages/getUser.ts +48 -0
- package/src/bridge/messages/hostToSandbox.ts +37 -0
- package/src/bridge/messages/init.ts +81 -0
- package/src/bridge/messages/invalidHostMessage.ts +16 -0
- package/src/bridge/messages/invalidSandboxMessage.ts +18 -0
- package/src/bridge/messages/listUsers.ts +49 -0
- package/src/bridge/messages/pageChanged.ts +12 -0
- package/src/bridge/messages/parentChanged.ts +14 -0
- package/src/bridge/messages/queryDataSource.ts +24 -0
- package/src/bridge/messages/queryDataSourceResult.ts +38 -0
- package/src/bridge/messages/ready.ts +41 -0
- package/src/bridge/messages/resize.ts +13 -0
- package/src/bridge/messages/sandboxToHost.ts +30 -0
- package/src/bridge/messages/themeChanged.ts +15 -0
- package/src/bridge/messages/updatePage.ts +22 -0
- package/src/bridge/messages/updatePageResult.ts +49 -0
- package/src/bridge/pages/page.ts +281 -0
- package/src/bridge/parent.ts +27 -0
- package/src/bridge/pendingRequests.ts +28 -0
- package/src/bridge/sandboxClient.ts +137 -0
- package/src/bridge/theme.ts +5 -0
- package/src/bridge/users/user.ts +31 -0
- package/src/css.d.ts +1 -0
- package/src/customBlock.ts +102 -0
- package/src/host/createCustomBlockHost.ts +392 -0
- package/src/host/lifecycle/initErrors.ts +132 -0
- package/src/host/lifecycle/ready.ts +36 -0
- package/src/host/lifecycle/types.ts +28 -0
- package/src/host/messages/invalidSandboxMessage.ts +38 -0
- package/src/host/messages/types.ts +67 -0
- package/src/host/queries/querySubscriptions.ts +52 -0
- package/src/host/queries/types.ts +16 -0
- package/src/host.ts +167 -0
- package/src/index.ts +115 -0
- package/src/init.ts +143 -0
- package/src/react/DebugMessageLog.tsx +30 -0
- package/src/react/NotionCustomBlock.css +110 -0
- package/src/react/NotionCustomBlock.tsx +151 -0
- package/src/react/index.ts +28 -0
- package/src/react/standalonePreview.ts +28 -0
- package/src/react/useCustomBlockAutoResize.ts +46 -0
- package/src/react/useCustomBlockInit.ts +91 -0
- package/src/react/useDataSource.ts +47 -0
- package/src/react/useHostState.ts +9 -0
- package/src/react/useRuntimeState.ts +85 -0
- package/src/types.ts +175 -0
- package/src/utils.ts +13 -0
- package/src/version.ts +8 -0
- package/vite-plugin/index.d.ts +46 -0
- package/vite-plugin/index.js +115 -0
|
@@ -0,0 +1,921 @@
|
|
|
1
|
+
import * as v from "valibot"
|
|
2
|
+
import type {
|
|
3
|
+
CreatePageInput,
|
|
4
|
+
CreatePageResult,
|
|
5
|
+
GetPageResult,
|
|
6
|
+
GetUserResult,
|
|
7
|
+
ListUsersInput,
|
|
8
|
+
ListUsersResult,
|
|
9
|
+
NotionPageId,
|
|
10
|
+
NotionUserId,
|
|
11
|
+
UpdatePageInput,
|
|
12
|
+
UpdatePageResult,
|
|
13
|
+
UseDataSourceOptions,
|
|
14
|
+
} from "../types.js"
|
|
15
|
+
import { unreachable } from "../utils.js"
|
|
16
|
+
import { NCBLOCK_SDK_VERSION } from "../version.js"
|
|
17
|
+
import type {
|
|
18
|
+
NotionDataSource,
|
|
19
|
+
NotionDataSourceBindings,
|
|
20
|
+
} from "./dataSources/dataSource.js"
|
|
21
|
+
import type {
|
|
22
|
+
NotionDataSourcePageUpdateInput,
|
|
23
|
+
NotionDataSourcePageUpdateResult,
|
|
24
|
+
} from "./dataSources/dataSourcePage.js"
|
|
25
|
+
import { resolveDataSources } from "./dataSources/resolve.js"
|
|
26
|
+
import { resolvePropertyWriteMapForDataSource } from "./dataSources/resolveProperty.js"
|
|
27
|
+
import {
|
|
28
|
+
type CustomBlockHostState,
|
|
29
|
+
createEmptyDataSourceQueryState,
|
|
30
|
+
type DataSourceQueryState,
|
|
31
|
+
} from "./hostState.js"
|
|
32
|
+
import { readIncomingType } from "./incomingType.js"
|
|
33
|
+
import type { ManifestLoadResult } from "./loadManifest.js"
|
|
34
|
+
import type { CustomBlockManifest } from "./manifest.js"
|
|
35
|
+
import type {
|
|
36
|
+
CreatePageMessage,
|
|
37
|
+
CreatePageMessageParent,
|
|
38
|
+
} from "./messages/createPage.js"
|
|
39
|
+
import type { CustomBlockCreatePageErrorInfo } from "./messages/createPageResult.js"
|
|
40
|
+
import type { GetUserMessage } from "./messages/getUser.js"
|
|
41
|
+
import { hostToSandboxMessageSchema } from "./messages/hostToSandbox.js"
|
|
42
|
+
import { CustomBlockInitError, type InitMessage } from "./messages/init.js"
|
|
43
|
+
import type { InvalidHostMessage } from "./messages/invalidHostMessage.js"
|
|
44
|
+
import type { ListUsersMessage } from "./messages/listUsers.js"
|
|
45
|
+
import type { QueryDataSourceMessage } from "./messages/queryDataSource.js"
|
|
46
|
+
import type { CustomBlockQueryDataSourceErrorInfo } from "./messages/queryDataSourceResult.js"
|
|
47
|
+
import type { ReadyMessage } from "./messages/ready.js"
|
|
48
|
+
import type { ResizeMessage } from "./messages/resize.js"
|
|
49
|
+
import type { UpdatePageMessage } from "./messages/updatePage.js"
|
|
50
|
+
import { PendingRequests } from "./pendingRequests.js"
|
|
51
|
+
import type { NotionUser } from "./users/user.js"
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Used to ensure that the host and client are using the same version of the bridge protocol. A
|
|
55
|
+
* single host needs to support multiple custom blocks built with different versions of the bridge
|
|
56
|
+
* protocol. Increment this number any time a breaking change is made to the bridge protocol.
|
|
57
|
+
*/
|
|
58
|
+
export const CUSTOM_BLOCK_BRIDGE_PROTOCOL_VERSION = 2
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* A single entry in the bridge message log. Kept intentionally plain so the log
|
|
62
|
+
* is copy-pasteable to a local coding agent without needing extra context.
|
|
63
|
+
*/
|
|
64
|
+
export type MessageLogEntry = {
|
|
65
|
+
timestamp: string
|
|
66
|
+
direction: "sent" | "received"
|
|
67
|
+
data: unknown
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export class SandboxBridge {
|
|
71
|
+
private hostState: CustomBlockHostState = {
|
|
72
|
+
status: "uninitialized",
|
|
73
|
+
theme: "light",
|
|
74
|
+
}
|
|
75
|
+
private listeners = new Set<() => void>()
|
|
76
|
+
private messageLog: MessageLogEntry[] = []
|
|
77
|
+
private messageLogListeners = new Set<() => void>()
|
|
78
|
+
private nextRequestId = 1
|
|
79
|
+
private readonly pendingCreatePage = new PendingRequests<CreatePageResult>(
|
|
80
|
+
"custom-block-create-page",
|
|
81
|
+
)
|
|
82
|
+
private readonly pendingGetPage = new PendingRequests<GetPageResult>(
|
|
83
|
+
"custom-block-get-page",
|
|
84
|
+
)
|
|
85
|
+
private readonly pendingGetUser = new PendingRequests<GetUserResult>(
|
|
86
|
+
"custom-block-get-user",
|
|
87
|
+
)
|
|
88
|
+
private readonly pendingListUsers = new PendingRequests<ListUsersResult>(
|
|
89
|
+
"custom-block-list-users",
|
|
90
|
+
)
|
|
91
|
+
private readonly pendingUpdatePage = new PendingRequests<UpdatePageResult>(
|
|
92
|
+
"custom-block-update-page",
|
|
93
|
+
)
|
|
94
|
+
private hasSentReady = false
|
|
95
|
+
private latestDataSourceBindings: NotionDataSourceBindings = {}
|
|
96
|
+
private resolveInit: ((message: InitMessage) => void) | undefined
|
|
97
|
+
private rejectInit: ((reason: Error) => void) | undefined
|
|
98
|
+
private readonly initMessage: Promise<InitMessage> = new Promise(
|
|
99
|
+
(resolve, reject) => {
|
|
100
|
+
this.resolveInit = resolve
|
|
101
|
+
this.rejectInit = reject
|
|
102
|
+
},
|
|
103
|
+
)
|
|
104
|
+
private manifest: CustomBlockManifest | null = null
|
|
105
|
+
|
|
106
|
+
constructor() {
|
|
107
|
+
// `ready` is sent later by `initCustomBlock` (after the manifest fetch
|
|
108
|
+
// resolves). Top-level / no-iframe rejection is handled there too, so
|
|
109
|
+
// the constructor just attaches the listener.
|
|
110
|
+
if (typeof window !== "undefined") {
|
|
111
|
+
window.addEventListener("message", this.handleMessage)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
private static MAX_LOG_ENTRIES = 100
|
|
116
|
+
|
|
117
|
+
private logMessage(direction: "sent" | "received", data: unknown) {
|
|
118
|
+
if (this.messageLog.length >= SandboxBridge.MAX_LOG_ENTRIES) {
|
|
119
|
+
this.messageLog.shift()
|
|
120
|
+
}
|
|
121
|
+
this.messageLog.push({
|
|
122
|
+
timestamp: new Date().toISOString(),
|
|
123
|
+
direction,
|
|
124
|
+
data,
|
|
125
|
+
})
|
|
126
|
+
for (const listener of this.messageLogListeners) {
|
|
127
|
+
listener()
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
getMessageLog(): readonly MessageLogEntry[] {
|
|
132
|
+
return this.messageLog
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
subscribeToMessageLog(listener: () => void) {
|
|
136
|
+
this.messageLogListeners.add(listener)
|
|
137
|
+
return () => this.messageLogListeners.delete(listener)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
awaitInit(signal?: AbortSignal): Promise<InitMessage> {
|
|
141
|
+
if (!signal) {
|
|
142
|
+
return this.initMessage
|
|
143
|
+
}
|
|
144
|
+
return new Promise((resolve, reject) => {
|
|
145
|
+
if (signal.aborted) {
|
|
146
|
+
reject(signal.reason)
|
|
147
|
+
return
|
|
148
|
+
}
|
|
149
|
+
const onAbort = () => reject(signal.reason)
|
|
150
|
+
signal.addEventListener("abort", onAbort, { once: true })
|
|
151
|
+
this.initMessage.then(
|
|
152
|
+
message => {
|
|
153
|
+
signal.removeEventListener("abort", onAbort)
|
|
154
|
+
resolve(message)
|
|
155
|
+
},
|
|
156
|
+
err => {
|
|
157
|
+
signal.removeEventListener("abort", onAbort)
|
|
158
|
+
reject(err)
|
|
159
|
+
},
|
|
160
|
+
)
|
|
161
|
+
})
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
sendReady(manifestResult: ManifestLoadResult) {
|
|
165
|
+
if (typeof window === "undefined") {
|
|
166
|
+
return
|
|
167
|
+
}
|
|
168
|
+
if (this.hasSentReady) {
|
|
169
|
+
console.warn("[notion-custom-sdk] ignoring duplicate ready message")
|
|
170
|
+
return
|
|
171
|
+
}
|
|
172
|
+
const { manifest, error } = manifestResult
|
|
173
|
+
this.hasSentReady = true
|
|
174
|
+
this.manifest = manifest
|
|
175
|
+
const readyMessage: ReadyMessage =
|
|
176
|
+
error !== undefined
|
|
177
|
+
? {
|
|
178
|
+
type: "ready",
|
|
179
|
+
status: "error",
|
|
180
|
+
bridgeProtocolVersion: CUSTOM_BLOCK_BRIDGE_PROTOCOL_VERSION,
|
|
181
|
+
sdkVersion: NCBLOCK_SDK_VERSION,
|
|
182
|
+
error,
|
|
183
|
+
}
|
|
184
|
+
: {
|
|
185
|
+
type: "ready",
|
|
186
|
+
status: "success",
|
|
187
|
+
bridgeProtocolVersion: CUSTOM_BLOCK_BRIDGE_PROTOCOL_VERSION,
|
|
188
|
+
sdkVersion: NCBLOCK_SDK_VERSION,
|
|
189
|
+
manifest,
|
|
190
|
+
}
|
|
191
|
+
this.postToHost(readyMessage)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
private postToHost(message: unknown) {
|
|
195
|
+
console.debug("[notion-custom-sdk] outbound postMessage", message)
|
|
196
|
+
this.logMessage("sent", message)
|
|
197
|
+
window.parent.postMessage(message, "*")
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
private notify = () => {
|
|
201
|
+
for (const listener of this.listeners) {
|
|
202
|
+
listener()
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
private handleMessage = (event: MessageEvent) => {
|
|
207
|
+
console.debug("[notion-custom-sdk] incoming postMessage", {
|
|
208
|
+
data: event.data,
|
|
209
|
+
fromParent: event.source === window.parent,
|
|
210
|
+
})
|
|
211
|
+
if (event.source !== window.parent) {
|
|
212
|
+
return
|
|
213
|
+
}
|
|
214
|
+
this.logMessage("received", event.data)
|
|
215
|
+
|
|
216
|
+
const parsed = v.safeParse(hostToSandboxMessageSchema, event.data)
|
|
217
|
+
if (!parsed.success) {
|
|
218
|
+
console.warn(
|
|
219
|
+
"[notion-custom-sdk] ignoring malformed host message",
|
|
220
|
+
parsed.issues,
|
|
221
|
+
)
|
|
222
|
+
const incomingType = readIncomingType(event.data)
|
|
223
|
+
// Loop guard: never NACK a NACK. Excludes both directions —
|
|
224
|
+
// `invalidSandboxMessage` is what the host normally sends, but
|
|
225
|
+
// a buggy host that echoes our own `invalidHostMessage` back
|
|
226
|
+
// would otherwise spin a NACK feedback loop.
|
|
227
|
+
if (
|
|
228
|
+
incomingType !== "invalidSandboxMessage" &&
|
|
229
|
+
incomingType !== "invalidHostMessage"
|
|
230
|
+
) {
|
|
231
|
+
const nack: InvalidHostMessage = {
|
|
232
|
+
type: "invalidHostMessage",
|
|
233
|
+
reason: formatInvalidHostReason(incomingType, parsed.issues),
|
|
234
|
+
}
|
|
235
|
+
this.postToHost(nack)
|
|
236
|
+
}
|
|
237
|
+
return
|
|
238
|
+
}
|
|
239
|
+
const message = parsed.output
|
|
240
|
+
|
|
241
|
+
// If the host couldn't parse one of our outbound sandbox-to-host messages, it sends back
|
|
242
|
+
// an `invalidSandboxMessage` NACK with a human-readable `reason`. Log it for developer
|
|
243
|
+
// visibility and stop. We don't retry as the sandbox can't recover from a host-side parse
|
|
244
|
+
// failure on its own.
|
|
245
|
+
if (message.type === "invalidSandboxMessage") {
|
|
246
|
+
console.warn(
|
|
247
|
+
"[notion-custom-sdk] host reported invalid sandbox message:",
|
|
248
|
+
message.reason,
|
|
249
|
+
)
|
|
250
|
+
return
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// `init` is the only message valid before initialization. Handle it up
|
|
254
|
+
// front so every later case can assume `status === "initialized"`.
|
|
255
|
+
if (message.type === "init") {
|
|
256
|
+
this.applyInit(message)
|
|
257
|
+
return
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Alias to keep TS's narrowed `InitializedHostState` across the switch
|
|
261
|
+
// below. Reading `this.hostState` repeatedly would re-widen it.
|
|
262
|
+
const hostState = this.hostState
|
|
263
|
+
|
|
264
|
+
if (hostState.status !== "initialized") {
|
|
265
|
+
console.warn(`[notion-custom-sdk] ignoring ${message.type} before init`)
|
|
266
|
+
return
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
switch (message.type) {
|
|
270
|
+
case "themeChanged": {
|
|
271
|
+
this.hostState = {
|
|
272
|
+
...hostState,
|
|
273
|
+
theme: message.theme,
|
|
274
|
+
}
|
|
275
|
+
this.notify()
|
|
276
|
+
return
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
case "parentChanged": {
|
|
280
|
+
this.hostState = {
|
|
281
|
+
...hostState,
|
|
282
|
+
parent: message.parent,
|
|
283
|
+
}
|
|
284
|
+
this.notify()
|
|
285
|
+
return
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
case "pageChanged": {
|
|
289
|
+
this.hostState = {
|
|
290
|
+
...hostState,
|
|
291
|
+
page: message.page,
|
|
292
|
+
}
|
|
293
|
+
this.notify()
|
|
294
|
+
return
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
case "currentUserChanged": {
|
|
298
|
+
this.hostState = {
|
|
299
|
+
...hostState,
|
|
300
|
+
currentUser: message.currentUser,
|
|
301
|
+
}
|
|
302
|
+
this.notify()
|
|
303
|
+
return
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
case "dataSourcesChanged": {
|
|
307
|
+
const nextBindings = message.dataSources.bindings
|
|
308
|
+
const dataSources = reuseDataSourcesForUnchangedBindings({
|
|
309
|
+
previousDataSources: hostState.dataSources,
|
|
310
|
+
previousBindings: this.latestDataSourceBindings,
|
|
311
|
+
nextDataSources: resolveDataSources({
|
|
312
|
+
manifest: this.manifest,
|
|
313
|
+
dataSourceBindings: nextBindings,
|
|
314
|
+
}),
|
|
315
|
+
nextBindings,
|
|
316
|
+
})
|
|
317
|
+
this.latestDataSourceBindings = nextBindings
|
|
318
|
+
// Drop cached query state for keys that no longer exist in the mapping.
|
|
319
|
+
const nextKeys = new Set(dataSources.map(s => s.key))
|
|
320
|
+
const prunedState: Record<string, DataSourceQueryState> = {}
|
|
321
|
+
for (const [key, state] of Object.entries(hostState.dataSourceState)) {
|
|
322
|
+
if (nextKeys.has(key)) {
|
|
323
|
+
prunedState[key] = state
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
this.hostState = {
|
|
327
|
+
...hostState,
|
|
328
|
+
dataSources,
|
|
329
|
+
dataSourceState: prunedState,
|
|
330
|
+
}
|
|
331
|
+
this.notify()
|
|
332
|
+
return
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
case "createPageResult": {
|
|
336
|
+
const result: CreatePageResult =
|
|
337
|
+
message.status === "success"
|
|
338
|
+
? { status: "success", page: message.page }
|
|
339
|
+
: { status: "error", error: message.error }
|
|
340
|
+
if (!this.pendingCreatePage.resolve(message.requestId, result)) {
|
|
341
|
+
console.warn(
|
|
342
|
+
`[notion-custom-sdk] createPageResult for unknown requestId ${message.requestId}`,
|
|
343
|
+
)
|
|
344
|
+
}
|
|
345
|
+
return
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
case "getPageResult": {
|
|
349
|
+
const result: GetPageResult =
|
|
350
|
+
message.status === "success"
|
|
351
|
+
? { status: "success", page: message.page }
|
|
352
|
+
: { status: "error", error: message.error }
|
|
353
|
+
if (!this.pendingGetPage.resolve(message.requestId, result)) {
|
|
354
|
+
console.warn(
|
|
355
|
+
`[notion-custom-sdk] getPageResult for unknown requestId ${message.requestId}`,
|
|
356
|
+
)
|
|
357
|
+
}
|
|
358
|
+
return
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
case "getUserResult": {
|
|
362
|
+
const result: GetUserResult =
|
|
363
|
+
message.status === "success"
|
|
364
|
+
? { status: "success", user: message.user as unknown as NotionUser }
|
|
365
|
+
: { status: "error", error: message.error }
|
|
366
|
+
if (!this.pendingGetUser.resolve(message.requestId, result)) {
|
|
367
|
+
console.warn(
|
|
368
|
+
`[notion-custom-sdk] getUserResult for unknown requestId ${message.requestId}`,
|
|
369
|
+
)
|
|
370
|
+
}
|
|
371
|
+
return
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
case "listUsersResult": {
|
|
375
|
+
const result: ListUsersResult =
|
|
376
|
+
message.status === "success"
|
|
377
|
+
? { status: "success", list: message.list }
|
|
378
|
+
: { status: "error", error: message.error }
|
|
379
|
+
if (!this.pendingListUsers.resolve(message.requestId, result)) {
|
|
380
|
+
console.warn(
|
|
381
|
+
`[notion-custom-sdk] listUsersResult for unknown requestId ${message.requestId}`,
|
|
382
|
+
)
|
|
383
|
+
}
|
|
384
|
+
return
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
case "updatePageResult": {
|
|
388
|
+
const result: UpdatePageResult =
|
|
389
|
+
message.status === "success"
|
|
390
|
+
? { status: "success", page: message.page }
|
|
391
|
+
: { status: "error", error: message.error }
|
|
392
|
+
if (!this.pendingUpdatePage.resolve(message.requestId, result)) {
|
|
393
|
+
console.warn(
|
|
394
|
+
`[notion-custom-sdk] updatePageResult for unknown requestId ${message.requestId}`,
|
|
395
|
+
)
|
|
396
|
+
}
|
|
397
|
+
return
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
case "queryDataSourceResult": {
|
|
401
|
+
const queryEntry = Object.entries(hostState.dataSourceState).find(
|
|
402
|
+
([, state]) => state.latestSnapshotId === message.snapshotId,
|
|
403
|
+
)
|
|
404
|
+
if (queryEntry === undefined) {
|
|
405
|
+
return
|
|
406
|
+
}
|
|
407
|
+
const [key, currentState] = queryEntry
|
|
408
|
+
if (currentState.latestRequestId !== message.requestId) {
|
|
409
|
+
return
|
|
410
|
+
}
|
|
411
|
+
this.hostState = {
|
|
412
|
+
...hostState,
|
|
413
|
+
dataSourceState: {
|
|
414
|
+
...hostState.dataSourceState,
|
|
415
|
+
[key]: {
|
|
416
|
+
items: message.items,
|
|
417
|
+
isLoading: false,
|
|
418
|
+
hasMore: message.hasMore,
|
|
419
|
+
error: message.error,
|
|
420
|
+
// Keep the request ID so later host-pushed refreshes for the
|
|
421
|
+
// same subscription still match.
|
|
422
|
+
latestRequestId: message.requestId,
|
|
423
|
+
latestSnapshotId: message.snapshotId,
|
|
424
|
+
latestLimit: currentState.latestLimit,
|
|
425
|
+
},
|
|
426
|
+
},
|
|
427
|
+
}
|
|
428
|
+
this.notify()
|
|
429
|
+
return
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
default: {
|
|
433
|
+
unreachable(message)
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
subscribe(listener: () => void) {
|
|
439
|
+
this.listeners.add(listener)
|
|
440
|
+
return () => this.listeners.delete(listener)
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
getHostState(): CustomBlockHostState {
|
|
444
|
+
return this.hostState
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* The author-declared manifest loaded from `custom_blocks.json` and forwarded
|
|
449
|
+
* to the host in `ready`. `null` when it failed to load/parse and the host
|
|
450
|
+
* should reject init via `ready.status: "error"`. Static for the lifetime
|
|
451
|
+
* of the sandbox.
|
|
452
|
+
*/
|
|
453
|
+
getManifest(): CustomBlockManifest | null {
|
|
454
|
+
return this.manifest
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Apply an `init` payload as if it had arrived from the host. Lets callers
|
|
459
|
+
* seed the bridge directly (e.g. the React provider's standalone preview
|
|
460
|
+
* fallback) without going through `postMessage`. The bridge stays unaware
|
|
461
|
+
* of why it's being seeded.
|
|
462
|
+
*/
|
|
463
|
+
setMockState(message: InitMessage) {
|
|
464
|
+
this.applyInit(message)
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
private applyInit(message: InitMessage) {
|
|
468
|
+
if (message.status === "error") {
|
|
469
|
+
// The host couldn't construct block location for this block (most commonly the parent record
|
|
470
|
+
// failed to resolve). Surface the failure through the `awaitInit` promise so callers see
|
|
471
|
+
// it through `useCustomBlockInit().error` instead of timing out. We also log here
|
|
472
|
+
// because not every consumer renders the error UI, and we want the failure visible in
|
|
473
|
+
// the browser console either way. We deliberately don't NACK the host or post anything
|
|
474
|
+
// back over the bridge since the host has already given up.
|
|
475
|
+
console.error(
|
|
476
|
+
`[notion-custom-sdk] host reported init error (${message.error.code}): ${message.error.message}`,
|
|
477
|
+
)
|
|
478
|
+
if (this.rejectInit) {
|
|
479
|
+
this.rejectInit(new CustomBlockInitError(message.error))
|
|
480
|
+
this.resolveInit = undefined
|
|
481
|
+
this.rejectInit = undefined
|
|
482
|
+
}
|
|
483
|
+
return
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const { blockId, parent, page } = message
|
|
487
|
+
|
|
488
|
+
this.latestDataSourceBindings = message.dataSources.bindings
|
|
489
|
+
const dataSources = resolveDataSources({
|
|
490
|
+
manifest: this.manifest,
|
|
491
|
+
dataSourceBindings: this.latestDataSourceBindings,
|
|
492
|
+
})
|
|
493
|
+
this.hostState = {
|
|
494
|
+
status: "initialized",
|
|
495
|
+
theme: message.theme,
|
|
496
|
+
blockId,
|
|
497
|
+
parent,
|
|
498
|
+
page,
|
|
499
|
+
currentUser: message.currentUser,
|
|
500
|
+
dataSources,
|
|
501
|
+
dataSourceState: {},
|
|
502
|
+
}
|
|
503
|
+
this.notify()
|
|
504
|
+
// Resolve the awaitInit promise once. Subsequent `init` messages
|
|
505
|
+
// (the host shouldn't send these, but be tolerant) update state but
|
|
506
|
+
// don't re-resolve.
|
|
507
|
+
if (this.resolveInit) {
|
|
508
|
+
this.resolveInit(message)
|
|
509
|
+
this.resolveInit = undefined
|
|
510
|
+
this.rejectInit = undefined
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
queryDataSource(key: string, options: UseDataSourceOptions = {}) {
|
|
515
|
+
if (this.hostState.status !== "initialized") {
|
|
516
|
+
return
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
const dataSource = this.hostState.dataSources.find(
|
|
520
|
+
entry => entry.key === key,
|
|
521
|
+
)
|
|
522
|
+
const currentState =
|
|
523
|
+
this.hostState.dataSourceState[key] ?? createEmptyDataSourceQueryState()
|
|
524
|
+
if (dataSource === undefined) {
|
|
525
|
+
this.setDataSourceQueryError(key, currentState, {
|
|
526
|
+
code: "unknown_data_source_key",
|
|
527
|
+
message: `Unknown data source key "${key}". Known keys: [${this.hostState.dataSources.map(entry => entry.key).join(", ")}].`,
|
|
528
|
+
isRetryable: false,
|
|
529
|
+
})
|
|
530
|
+
return
|
|
531
|
+
}
|
|
532
|
+
if (dataSource.collectionPointer === undefined) {
|
|
533
|
+
this.setDataSourceQueryError(key, currentState, {
|
|
534
|
+
code: "unmapped_data_source",
|
|
535
|
+
message: `Data source "${key}" has not been mapped to a database yet.`,
|
|
536
|
+
isRetryable: false,
|
|
537
|
+
})
|
|
538
|
+
return
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
const limit = resolveDataSourceQueryLimit(options.limit)
|
|
542
|
+
const snapshotId = makeDataSourceSnapshotId({
|
|
543
|
+
key,
|
|
544
|
+
})
|
|
545
|
+
|
|
546
|
+
if (
|
|
547
|
+
currentState.isLoading &&
|
|
548
|
+
currentState.latestSnapshotId === snapshotId &&
|
|
549
|
+
currentState.latestLimit === limit
|
|
550
|
+
) {
|
|
551
|
+
return
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
const requestId = `custom-block-query-${this.nextRequestId}`
|
|
555
|
+
this.nextRequestId += 1
|
|
556
|
+
|
|
557
|
+
this.hostState = {
|
|
558
|
+
...this.hostState,
|
|
559
|
+
dataSourceState: {
|
|
560
|
+
...this.hostState.dataSourceState,
|
|
561
|
+
[key]: {
|
|
562
|
+
...currentState,
|
|
563
|
+
isLoading: true,
|
|
564
|
+
error: undefined,
|
|
565
|
+
latestRequestId: requestId,
|
|
566
|
+
latestSnapshotId: snapshotId,
|
|
567
|
+
latestLimit: limit,
|
|
568
|
+
},
|
|
569
|
+
},
|
|
570
|
+
}
|
|
571
|
+
this.notify()
|
|
572
|
+
|
|
573
|
+
const outbound: QueryDataSourceMessage = {
|
|
574
|
+
type: "queryDataSource",
|
|
575
|
+
requestId,
|
|
576
|
+
snapshotId,
|
|
577
|
+
dataSourceId: dataSource.collectionPointer.id,
|
|
578
|
+
limit,
|
|
579
|
+
}
|
|
580
|
+
this.postToHost(outbound)
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
private setDataSourceQueryError(
|
|
584
|
+
key: string,
|
|
585
|
+
currentState: DataSourceQueryState,
|
|
586
|
+
error: CustomBlockQueryDataSourceErrorInfo,
|
|
587
|
+
) {
|
|
588
|
+
if (this.hostState.status !== "initialized") {
|
|
589
|
+
return
|
|
590
|
+
}
|
|
591
|
+
this.hostState = {
|
|
592
|
+
...this.hostState,
|
|
593
|
+
dataSourceState: {
|
|
594
|
+
...this.hostState.dataSourceState,
|
|
595
|
+
[key]: {
|
|
596
|
+
...currentState,
|
|
597
|
+
isLoading: false,
|
|
598
|
+
error,
|
|
599
|
+
latestRequestId: undefined,
|
|
600
|
+
latestSnapshotId: undefined,
|
|
601
|
+
latestLimit: undefined,
|
|
602
|
+
},
|
|
603
|
+
},
|
|
604
|
+
}
|
|
605
|
+
this.notify()
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
postResize(height: number) {
|
|
609
|
+
if (typeof window === "undefined") {
|
|
610
|
+
return
|
|
611
|
+
}
|
|
612
|
+
const safeHeight =
|
|
613
|
+
Number.isFinite(height) && height >= 0 ? Math.ceil(height) : 0
|
|
614
|
+
const outbound: ResizeMessage = {
|
|
615
|
+
type: "resize",
|
|
616
|
+
height: safeHeight,
|
|
617
|
+
}
|
|
618
|
+
this.postToHost(outbound)
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
createPage(input: CreatePageInput): Promise<CreatePageResult> {
|
|
622
|
+
return new Promise(resolve => {
|
|
623
|
+
const resolvedParent = this.resolveCreatePageParent(input.parent)
|
|
624
|
+
if (resolvedParent.status === "error") {
|
|
625
|
+
resolve(resolvedParent)
|
|
626
|
+
return
|
|
627
|
+
}
|
|
628
|
+
const resolvedProperties = resolvePropertyWriteMapForDataSource({
|
|
629
|
+
dataSource: resolvedParent.dataSource,
|
|
630
|
+
properties: input.properties,
|
|
631
|
+
operationName: "createPage",
|
|
632
|
+
})
|
|
633
|
+
if (resolvedProperties.status === "error") {
|
|
634
|
+
resolve(resolvedProperties)
|
|
635
|
+
return
|
|
636
|
+
}
|
|
637
|
+
const requestId = this.pendingCreatePage.allocate(resolve)
|
|
638
|
+
const outbound: CreatePageMessage = {
|
|
639
|
+
type: "createPage",
|
|
640
|
+
requestId,
|
|
641
|
+
parent: resolvedParent.parent,
|
|
642
|
+
properties: resolvedProperties.properties,
|
|
643
|
+
}
|
|
644
|
+
if (input.icon !== undefined) {
|
|
645
|
+
outbound.icon = input.icon
|
|
646
|
+
}
|
|
647
|
+
if (input.cover !== undefined) {
|
|
648
|
+
outbound.cover = input.cover
|
|
649
|
+
}
|
|
650
|
+
if (input.position !== undefined) {
|
|
651
|
+
outbound.position = input.position
|
|
652
|
+
}
|
|
653
|
+
this.postToHost(outbound)
|
|
654
|
+
})
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
getPage(pageId: NotionPageId): Promise<GetPageResult> {
|
|
658
|
+
return new Promise(resolve => {
|
|
659
|
+
const requestId = this.pendingGetPage.allocate(resolve)
|
|
660
|
+
const outbound = {
|
|
661
|
+
type: "getPage",
|
|
662
|
+
requestId,
|
|
663
|
+
pageId,
|
|
664
|
+
}
|
|
665
|
+
this.postToHost(outbound)
|
|
666
|
+
})
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
getUser(userId: NotionUserId): Promise<GetUserResult> {
|
|
670
|
+
return new Promise(resolve => {
|
|
671
|
+
const requestId = this.pendingGetUser.allocate(resolve)
|
|
672
|
+
const outbound: GetUserMessage = {
|
|
673
|
+
type: "getUser",
|
|
674
|
+
requestId,
|
|
675
|
+
userId,
|
|
676
|
+
}
|
|
677
|
+
this.postToHost(outbound)
|
|
678
|
+
})
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
listUsers(input: ListUsersInput = {}): Promise<ListUsersResult> {
|
|
682
|
+
return new Promise(resolve => {
|
|
683
|
+
const requestId = this.pendingListUsers.allocate(resolve)
|
|
684
|
+
const outbound: ListUsersMessage = {
|
|
685
|
+
type: "listUsers",
|
|
686
|
+
requestId,
|
|
687
|
+
startCursor: input.startCursor,
|
|
688
|
+
pageSize: input.pageSize,
|
|
689
|
+
}
|
|
690
|
+
this.postToHost(outbound)
|
|
691
|
+
})
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
updatePage(input: UpdatePageInput): Promise<UpdatePageResult> {
|
|
695
|
+
return new Promise(resolve => {
|
|
696
|
+
if (
|
|
697
|
+
(input.properties === undefined ||
|
|
698
|
+
Object.keys(input.properties).length === 0) &&
|
|
699
|
+
input.icon === undefined &&
|
|
700
|
+
input.cover === undefined &&
|
|
701
|
+
input.archived === undefined
|
|
702
|
+
) {
|
|
703
|
+
resolve({
|
|
704
|
+
status: "error",
|
|
705
|
+
error: {
|
|
706
|
+
code: "invalid_page_update",
|
|
707
|
+
message:
|
|
708
|
+
"updatePage requires at least one of: properties, icon, cover, archived.",
|
|
709
|
+
isRetryable: false,
|
|
710
|
+
},
|
|
711
|
+
})
|
|
712
|
+
return
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
const requestId = this.pendingUpdatePage.allocate(resolve)
|
|
716
|
+
const outbound: UpdatePageMessage = {
|
|
717
|
+
type: "updatePage",
|
|
718
|
+
requestId,
|
|
719
|
+
pageId: input.pageId,
|
|
720
|
+
}
|
|
721
|
+
if (input.properties !== undefined) {
|
|
722
|
+
outbound.properties = input.properties
|
|
723
|
+
}
|
|
724
|
+
if (input.icon !== undefined) {
|
|
725
|
+
outbound.icon = input.icon
|
|
726
|
+
}
|
|
727
|
+
if (input.cover !== undefined) {
|
|
728
|
+
outbound.cover = input.cover
|
|
729
|
+
}
|
|
730
|
+
if (input.archived !== undefined) {
|
|
731
|
+
outbound.archived = input.archived
|
|
732
|
+
}
|
|
733
|
+
this.postToHost(outbound)
|
|
734
|
+
})
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
/**
|
|
738
|
+
* Updates a page on a known data source, resolving any property keys against the data source's
|
|
739
|
+
* `propertyIdsByKey` before sending the bridge message. Used by the per-row `update` callback
|
|
740
|
+
* returned from {@link getDataSourceQueryView}.
|
|
741
|
+
*/
|
|
742
|
+
updateDataSourcePage(args: {
|
|
743
|
+
dataSource: NotionDataSource
|
|
744
|
+
pageId: NotionPageId
|
|
745
|
+
input: NotionDataSourcePageUpdateInput
|
|
746
|
+
}): Promise<NotionDataSourcePageUpdateResult> {
|
|
747
|
+
const { dataSource, pageId, input } = args
|
|
748
|
+
const resolvedProperties =
|
|
749
|
+
input.properties === undefined
|
|
750
|
+
? undefined
|
|
751
|
+
: resolvePropertyWriteMapForDataSource({
|
|
752
|
+
dataSource,
|
|
753
|
+
properties: input.properties,
|
|
754
|
+
operationName: "dataSourcePage.update",
|
|
755
|
+
})
|
|
756
|
+
if (resolvedProperties?.status === "error") {
|
|
757
|
+
return Promise.resolve(resolvedProperties)
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
return this.updatePage({
|
|
761
|
+
pageId,
|
|
762
|
+
properties: resolvedProperties?.properties,
|
|
763
|
+
icon: input.icon,
|
|
764
|
+
cover: input.cover,
|
|
765
|
+
archived: input.archived,
|
|
766
|
+
})
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
/**
|
|
770
|
+
* Translates the public `CreatePageInput["parent"]` into the bridge-native
|
|
771
|
+
* `CreatePageMessageParent`. The `data_source_key` variant is resolved sandbox-side against
|
|
772
|
+
* the data source mapping the host delivered in `init` / `dataSourcesChanged`.
|
|
773
|
+
*/
|
|
774
|
+
private resolveCreatePageParent(parent: CreatePageInput["parent"]):
|
|
775
|
+
| {
|
|
776
|
+
status: "ok"
|
|
777
|
+
parent: CreatePageMessageParent
|
|
778
|
+
dataSource: NotionDataSource | undefined
|
|
779
|
+
}
|
|
780
|
+
| { status: "error"; error: CustomBlockCreatePageErrorInfo } {
|
|
781
|
+
switch (parent.type) {
|
|
782
|
+
case "page_id":
|
|
783
|
+
return { status: "ok", parent, dataSource: undefined }
|
|
784
|
+
case "data_source_id": {
|
|
785
|
+
const dataSource =
|
|
786
|
+
this.hostState.status === "initialized"
|
|
787
|
+
? this.hostState.dataSources.find(
|
|
788
|
+
entry => entry.collectionPointer?.id === parent.data_source_id,
|
|
789
|
+
)
|
|
790
|
+
: undefined
|
|
791
|
+
return { status: "ok", parent, dataSource }
|
|
792
|
+
}
|
|
793
|
+
case "data_source_key": {
|
|
794
|
+
if (this.hostState.status !== "initialized") {
|
|
795
|
+
return {
|
|
796
|
+
status: "error",
|
|
797
|
+
error: {
|
|
798
|
+
code: "unknown_data_source_key",
|
|
799
|
+
message: `Cannot resolve data source key "${parent.key}" before the host has initialized the SDK.`,
|
|
800
|
+
isRetryable: false,
|
|
801
|
+
},
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
const dataSource = this.hostState.dataSources.find(
|
|
805
|
+
entry => entry.key === parent.key,
|
|
806
|
+
)
|
|
807
|
+
if (dataSource === undefined) {
|
|
808
|
+
return {
|
|
809
|
+
status: "error",
|
|
810
|
+
error: {
|
|
811
|
+
code: "unknown_data_source_key",
|
|
812
|
+
message: `Unknown data source key "${parent.key}". Known keys: [${this.hostState.dataSources.map(entry => entry.key).join(", ")}].`,
|
|
813
|
+
isRetryable: false,
|
|
814
|
+
},
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
if (dataSource.collectionPointer === undefined) {
|
|
818
|
+
return {
|
|
819
|
+
status: "error",
|
|
820
|
+
error: {
|
|
821
|
+
code: "unmapped_data_source",
|
|
822
|
+
message: `Data source "${parent.key}" has not been mapped to a database yet.`,
|
|
823
|
+
isRetryable: false,
|
|
824
|
+
},
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
return {
|
|
828
|
+
status: "ok",
|
|
829
|
+
parent: {
|
|
830
|
+
type: "data_source_id",
|
|
831
|
+
data_source_id: dataSource.collectionPointer.id,
|
|
832
|
+
},
|
|
833
|
+
dataSource,
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
default:
|
|
837
|
+
unreachable(parent)
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
/**
|
|
843
|
+
* `postMessage` cloning creates new objects even for bindings that did not change.
|
|
844
|
+
* Preserve their resolved object identity so React effects only run for changed bindings.
|
|
845
|
+
*
|
|
846
|
+
* This comparison runs when the host sends an update instead of on every component render.
|
|
847
|
+
* Bindings are validated JSON-like bridge payloads, so serialization is sufficient here.
|
|
848
|
+
*/
|
|
849
|
+
function reuseDataSourcesForUnchangedBindings(args: {
|
|
850
|
+
previousDataSources: NotionDataSource[]
|
|
851
|
+
previousBindings: NotionDataSourceBindings
|
|
852
|
+
nextDataSources: NotionDataSource[]
|
|
853
|
+
nextBindings: NotionDataSourceBindings
|
|
854
|
+
}): NotionDataSource[] {
|
|
855
|
+
const previousDataSourcesByKey = new Map(
|
|
856
|
+
args.previousDataSources.map(dataSource => [dataSource.key, dataSource]),
|
|
857
|
+
)
|
|
858
|
+
|
|
859
|
+
return args.nextDataSources.map(dataSource => {
|
|
860
|
+
const previousDataSource = previousDataSourcesByKey.get(dataSource.key)
|
|
861
|
+
if (
|
|
862
|
+
previousDataSource !== undefined &&
|
|
863
|
+
JSON.stringify(args.previousBindings[dataSource.key]) ===
|
|
864
|
+
JSON.stringify(args.nextBindings[dataSource.key])
|
|
865
|
+
) {
|
|
866
|
+
return previousDataSource
|
|
867
|
+
}
|
|
868
|
+
return dataSource
|
|
869
|
+
})
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
// The default number of items to return in a live snapshot response if no limit is provided.
|
|
873
|
+
const DEFAULT_DATA_SOURCE_QUERY_LIMIT = 20
|
|
874
|
+
|
|
875
|
+
// The maximum number of items to return in a single live snapshot response.
|
|
876
|
+
const MAX_DATA_SOURCE_QUERY_LIMIT = 999
|
|
877
|
+
|
|
878
|
+
function resolveDataSourceQueryLimit(limit: number | undefined): number {
|
|
879
|
+
if (limit === undefined) {
|
|
880
|
+
return DEFAULT_DATA_SOURCE_QUERY_LIMIT
|
|
881
|
+
}
|
|
882
|
+
if (!Number.isFinite(limit) || !Number.isInteger(limit) || limit < 1) {
|
|
883
|
+
console.warn(
|
|
884
|
+
`[notion-custom-sdk] useDataSource limit must be a positive integer; using ${DEFAULT_DATA_SOURCE_QUERY_LIMIT}.`,
|
|
885
|
+
)
|
|
886
|
+
return DEFAULT_DATA_SOURCE_QUERY_LIMIT
|
|
887
|
+
}
|
|
888
|
+
if (limit > MAX_DATA_SOURCE_QUERY_LIMIT) {
|
|
889
|
+
console.warn(
|
|
890
|
+
`[notion-custom-sdk] useDataSource limit is capped at ${MAX_DATA_SOURCE_QUERY_LIMIT}.`,
|
|
891
|
+
)
|
|
892
|
+
return MAX_DATA_SOURCE_QUERY_LIMIT
|
|
893
|
+
}
|
|
894
|
+
return limit
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
function makeDataSourceSnapshotId(args: { key: string }): string {
|
|
898
|
+
const { key } = args
|
|
899
|
+
return `data-source:${encodeURIComponent(key)}`
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
function formatInvalidHostReason(
|
|
903
|
+
incomingType: string | undefined,
|
|
904
|
+
issues: readonly v.BaseIssue<unknown>[],
|
|
905
|
+
): string {
|
|
906
|
+
const labelled = incomingType
|
|
907
|
+
? `host message of type "${incomingType}"`
|
|
908
|
+
: "host message"
|
|
909
|
+
const first = issues[0]
|
|
910
|
+
if (!first) {
|
|
911
|
+
return `Could not parse ${labelled}: unknown error`
|
|
912
|
+
}
|
|
913
|
+
const path =
|
|
914
|
+
first.path
|
|
915
|
+
?.map(p => String(p.key ?? ""))
|
|
916
|
+
.filter(Boolean)
|
|
917
|
+
.join(".") ?? ""
|
|
918
|
+
const detail = path ? `${path}: ${first.message}` : first.message
|
|
919
|
+
const extra = issues.length > 1 ? ` (+${issues.length - 1} more)` : ""
|
|
920
|
+
return `Could not parse ${labelled}: ${detail}${extra}`
|
|
921
|
+
}
|