@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
package/HOST.md
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# @notionhq/custom-blocks/host
|
|
2
|
+
|
|
3
|
+
Host-side entrypoint. Use this when implementing the host end of the custom block bridge — the Notion app, the dev shell, tests, mocks, anything that talks to a sandboxed custom block over `postMessage`. Nothing here is meant for use inside a custom block and everything here should be considered a private API. The main author-facing API surface lives in `@notionhq/custom-blocks`.
|
|
4
|
+
|
|
5
|
+
Most exports here are thin re-exports of internal `bridge/*` modules. Hosts validate inbound sandbox traffic against the same valibot schemas the SDK uses on the sandbox side, so both ends of the bridge agree on shape.
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import {
|
|
9
|
+
pageChangedMessageSchema,
|
|
10
|
+
parentChangedMessageSchema,
|
|
11
|
+
readIncomingType,
|
|
12
|
+
sandboxToHostMessageSchema,
|
|
13
|
+
type PageChangedMessage,
|
|
14
|
+
type ParentChangedMessage,
|
|
15
|
+
type SandboxToHostMessage,
|
|
16
|
+
} from "@notionhq/custom-blocks/host";
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
`readIncomingType()` is intentionally best-effort. Use it after validation fails so the host can avoid NACK loops and include a useful reason in `invalidSandboxMessage`.
|
|
20
|
+
|
|
21
|
+
`createCustomBlockHost()` is an optional protocol loop for lightweight hosts. It owns iframe message listener setup, `ready` / `init`, protocol version checks, malformed-message NACKs, request routing, auto-resize callbacks, state-change messages, cleanup, and latest-query refresh bookkeeping. It deliberately does not know about any host's data model, permissions, iframe policy, analytics, or UI state.
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import { createCustomBlockHost } from "@notionhq/custom-blocks/host";
|
|
25
|
+
|
|
26
|
+
const host = createCustomBlockHost({
|
|
27
|
+
iframe,
|
|
28
|
+
initialState: {
|
|
29
|
+
theme,
|
|
30
|
+
blockId,
|
|
31
|
+
parent,
|
|
32
|
+
page,
|
|
33
|
+
currentUser,
|
|
34
|
+
dataSources: { bindings },
|
|
35
|
+
},
|
|
36
|
+
handlers: {
|
|
37
|
+
queryDataSource: async (message) => ({ items: [], hasMore: false }),
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Use it when the generic protocol loop is helpful. Hosts with more specialized runtime needs can keep using the schemas and message types directly.
|
|
43
|
+
|
|
44
|
+
Related types: `CustomBlockHostOptions`, `CustomBlockHostInitialState`, `CustomBlockHostInitialDataSources`, `CustomBlockHostDataSourcesPayload`, `CustomBlockHostHandlers`, `CustomBlockHostHandle`, `CustomBlockHostLogDirection`, `CustomBlockHostQueryDataSourceResult`, `CustomBlockHostCreatePageResult`, `CustomBlockHostGetPageResult`, `CustomBlockHostUpdatePageResult`, `CustomBlockHostGetUserResult`, and `CustomBlockHostListUsersResult`.
|
|
45
|
+
|
|
46
|
+
## Bridge protocol
|
|
47
|
+
|
|
48
|
+
### Lifecycle
|
|
49
|
+
|
|
50
|
+
The bridge is established with a handshake: the sandbox posts a `ready` when `initCustomBlock()` starts, and the host validates it and replies exactly once with `init`. Hosts should log and ignore duplicate `ready` messages instead of re-running the handshake. After that, narrower messages stream live state updates.
|
|
51
|
+
|
|
52
|
+
The sandbox starts by sending `ready`. It declares the bridge protocol version, includes the custom blocks package semver as `sdkVersion` when available, and tells the host whether manifest discovery produced a usable manifest. Hosts should reject init when `ready` reports a manifest error, echoing the sandbox's error code:
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
// sandbox → host
|
|
56
|
+
{ type: "ready", status: "success", bridgeProtocolVersion, sdkVersion, manifest: { /* custom_blocks.json */ } }
|
|
57
|
+
|
|
58
|
+
// sandbox → host, no manifest / no declared data requirements
|
|
59
|
+
{ type: "ready", status: "success", bridgeProtocolVersion, sdkVersion, manifest: null }
|
|
60
|
+
|
|
61
|
+
// sandbox → host, manifest failed to load or validate
|
|
62
|
+
{ type: "ready", status: "error", bridgeProtocolVersion, sdkVersion, error: { code: "manifest_invalid", message: "..." } }
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
The host replies with exactly one `init`, which has two statuses. On success it carries the full initial state — `theme`, `blockId`, `parent`, `page`, `currentUser` (the `NotionUser` shape from the user result messages), and `dataSources: { bindings }`:
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
// host → sandbox
|
|
69
|
+
{ type: "init", status: "success", theme, blockId, parent, page: { id }, currentUser, dataSources: { bindings } }
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
On failure it carries `error: { code, message, isRetryable }` (`CustomBlockInitErrorInfo` / `customBlockInitErrorInfoSchema`, with `code` drawn from `CustomBlockInitErrorCode` / `customBlockInitErrorCodeSchema`). The SDK surfaces this as a `CustomBlockInitError`:
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
// host → sandbox
|
|
76
|
+
{ type: "init", status: "error", error: { code: "context_unavailable", message: "...", isRetryable: true } }
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
After `init`, narrower messages update live state without re-running the handshake — `themeChanged`, `parentChanged`, `pageChanged`, `currentUserChanged`, and `dataSourcesChanged`. Each replaces just its slice of state:
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
// host → sandbox, any time after init
|
|
83
|
+
{ type: "themeChanged", theme }
|
|
84
|
+
{ type: "parentChanged", parent }
|
|
85
|
+
{ type: "pageChanged", page: { id } }
|
|
86
|
+
{ type: "dataSourcesChanged", dataSources: { bindings } }
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Versioning
|
|
90
|
+
|
|
91
|
+
`ready` includes `bridgeProtocolVersion` (the host <-> sandbox version) and `sdkVersion` (the package semver). Hosts should reject or fail `init` when the sandbox reports a `bridgeProtocolVersion` the host does not implement, but should treat `sdkVersion` as purely analytics metadata.
|
|
92
|
+
|
|
93
|
+
If the sandbox reports a version below the host's supported minimum, reply with `init.status: "error"` and `error.code: "unsupported_protocol_version"`. If the reported version is structurally invalid (not a positive integer — e.g. `0`, negative, fractional, `NaN`, or `Infinity`), reply with `error.code: "invalid_protocol_version"` instead.
|
|
94
|
+
|
|
95
|
+
### Conventions
|
|
96
|
+
|
|
97
|
+
Every request/response pair uses a string `requestId`. The sender tracks pending requests by id; the receiver echoes the id on the matching result. The sandbox drops stale results that no longer match the latest outstanding request for that operation.
|
|
98
|
+
|
|
99
|
+
Both sides validate inbound messages with valibot schemas. Failed parses are logged and NACKed with `invalidHostMessage` or `invalidSandboxMessage`. Never answer a NACK with another NACK.
|
|
100
|
+
|
|
101
|
+
Block-author APIs use semantic data-source keys, but sandbox-to-host object references are resolved to raw IDs before posting.
|
|
102
|
+
|
|
103
|
+
Every bridge error payload uses the same `{ code, message, isRetryable }` shape. `CustomBlockErrorInfo<TCode>` is generic so each bridge API can expose its own code union while preserving one wire format. Hosts should pick stable codes and put user/developer-readable detail in `message`; sandboxes should branch on `code` and `isRetryable`, not message text.
|
|
104
|
+
|
|
105
|
+
`code` is any string on the wire. Schemas validate only the error _shape_ (`code`/`message`/`isRetryable` types); per-API code unions are TypeScript open-enums for autocomplete. Receivers MUST accept well-shaped errors with unknown codes so newer senders can add codes without breaking older receivers.
|
|
106
|
+
|
|
107
|
+
Host implementers can use the API-specific aliases and schemas when shaping outbound failures: `CustomBlockCreatePageErrorCode` / `CustomBlockCreatePageErrorInfo` / `customBlockCreatePageErrorCodeSchema` / `customBlockCreatePageErrorInfoSchema`, `CustomBlockGetPageErrorCode` / `CustomBlockGetPageErrorInfo` / `customBlockGetPageErrorCodeSchema` / `customBlockGetPageErrorInfoSchema`, `CustomBlockGetUserErrorCode` / `CustomBlockGetUserErrorInfo` / `customBlockGetUserErrorCodeSchema` / `customBlockGetUserErrorInfoSchema`, `CustomBlockListUsersErrorCode` / `CustomBlockListUsersErrorInfo` / `customBlockListUsersErrorCodeSchema` / `customBlockListUsersErrorInfoSchema`, `CustomBlockQueryDataSourceErrorCode` / `CustomBlockQueryDataSourceErrorInfo` / `customBlockQueryDataSourceErrorCodeSchema` / `customBlockQueryDataSourceErrorInfoSchema`, and `CustomBlockUpdatePageErrorCode` / `CustomBlockUpdatePageErrorInfo` / `customBlockUpdatePageErrorCodeSchema` / `customBlockUpdatePageErrorInfoSchema`. Shared categories are exported as `CustomBlockPropertyErrorCode` / `CustomBlockPropertyErrorInfo` / `customBlockPropertyErrorCodeSchema` and `CustomBlockDataSourceResolutionErrorCode` / `CustomBlockDataSourceResolutionErrorInfo` / `customBlockDataSourceResolutionErrorCodeSchema`. The broad fallback schema remains `customBlockErrorInfoSchema`.
|
|
108
|
+
|
|
109
|
+
`queryDataSource` carries the resolved raw `dataSourceId` and a sandbox-generated `snapshotId`; semantic data-source keys never cross the bridge for queries. `createPage` arrives with `parent` already resolved to `page_id` or `data_source_id`.
|
|
110
|
+
|
|
111
|
+
## Data sources
|
|
112
|
+
|
|
113
|
+
Types:
|
|
114
|
+
|
|
115
|
+
- `CustomBlockPage` — the current page slice carried in `init.page` and `pageChanged`.
|
|
116
|
+
- `NotionDataSourceBinding` — a single binding (collection pointer + schema + property mapping).
|
|
117
|
+
- `NotionDataSourceBindings` — keyed-by-semantic-key map of bindings, the shape carried in `init.dataSources` and `dataSourcesChanged`.
|
|
118
|
+
- `NotionDataSourcePageBridge` — wire shape for a single page (raw property IDs in `propertiesById`). The SDK derives the consumer-facing `NotionDataSourcePage` from it.
|
|
119
|
+
|
|
120
|
+
Schemas:
|
|
121
|
+
|
|
122
|
+
- `notionBlockIdSchema`
|
|
123
|
+
- `customBlockPageSchema`
|
|
124
|
+
- `notionDataSourceIdSchema`
|
|
125
|
+
- `notionDataSourceBindingSchema`
|
|
126
|
+
- `notionDataSourceBindingsSchema`
|
|
127
|
+
- `notionDataSourcePageBridgeSchema`
|
|
128
|
+
- `notionPageIdSchema`
|
|
129
|
+
- `notionParentSchema`
|
|
130
|
+
|
|
131
|
+
## Sandbox → host messages
|
|
132
|
+
|
|
133
|
+
Messages sent from the sandbox to the host. Parse `window` `message` events with `sandboxToHostMessageSchema` (or per-message schemas). The type / schema column points to the payload shape.
|
|
134
|
+
|
|
135
|
+
| Wire type | Type / schema | Behavior |
|
|
136
|
+
| -------------------- | --------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
137
|
+
| `ready` | `ReadyMessage` / `readyMessageSchema` | One-shot handshake; carries `bridgeProtocolVersion`, `sdkVersion`, and the manifest. |
|
|
138
|
+
| `queryDataSource` | `QueryDataSourceMessage` / `queryDataSourceMessageSchema` | `requestId`-keyed request for the current rows in a raw `dataSourceId`. `snapshotId` names the SDK result slot to update, so later refreshes for the same `useDataSource` call replace the same snapshot instead of creating a new one. |
|
|
139
|
+
| `createPage` | `CreatePageMessage` / `createPageMessageSchema` | `requestId`-keyed page creation; parent is `page_id` or `data_source_id`. |
|
|
140
|
+
| `getPage` | `GetPageMessage` / `getPageMessageSchema` | `requestId`-keyed page fetch by page id. |
|
|
141
|
+
| `updatePage` | `UpdatePageMessage` / `updatePageMessageSchema` | `requestId`-keyed patch (properties, icon, cover, or `archived`). |
|
|
142
|
+
| `getUser` | `GetUserMessage` / `getUserMessageSchema` | `requestId`-keyed user fetch by user id. |
|
|
143
|
+
| `listUsers` | `ListUsersMessage` / `listUsersMessageSchema` | `requestId`-keyed user list with optional `startCursor` and `pageSize`. |
|
|
144
|
+
| `resize` | `ResizeMessage` / `resizeMessageSchema` | Latest measured content height from auto-resize. |
|
|
145
|
+
| `invalidHostMessage` | `InvalidHostMessage` / `invalidHostMessageSchema` | Sandbox-side NACK for a host message it could not parse. |
|
|
146
|
+
|
|
147
|
+
`SandboxToHostMessage` / `sandboxToHostMessageSchema` is the discriminated union over all of the above.
|
|
148
|
+
|
|
149
|
+
## Host → sandbox messages
|
|
150
|
+
|
|
151
|
+
Messages sent from the host to the sandbox. Same `{ wire type, type / schema, behavior }` shape; the type / schema column points to the payload shape.
|
|
152
|
+
|
|
153
|
+
| Wire type | Type / schema | Behavior |
|
|
154
|
+
| ----------------------- | --------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
|
|
155
|
+
| `init` | `InitMessage` / `initMessageSchema` | Sent exactly once in response to `ready`; carries the success/error payload described under [Lifecycle](#lifecycle). |
|
|
156
|
+
| `themeChanged` | `ThemeChangedMessage` / `themeChangedMessageSchema` | Replaces the current theme. |
|
|
157
|
+
| `parentChanged` | `ParentChangedMessage` / `parentChangedMessageSchema` | Replaces the custom block parent without disturbing theme, block ID, page ID, user, or query state. |
|
|
158
|
+
| `pageChanged` | `PageChangedMessage` / `pageChangedMessageSchema` | Replaces the nearest page ancestor without disturbing theme, block ID, parent, user, or query state. |
|
|
159
|
+
| `currentUserChanged` | `CurrentUserChangedMessage` / `currentUserChangedMessageSchema` | Replaces the current viewer record. Send when any viewer field changes (name, avatar, email). |
|
|
160
|
+
| `dataSourcesChanged` | `DataSourcesChangedMessage` / `dataSourcesChangedMessageSchema` | Replaces data-source bindings; the sandbox preserves cached query state for keys that still exist and drops removed keys. |
|
|
161
|
+
| `queryDataSourceResult` | `QueryDataSourceResultMessage` / `queryDataSourceResultMessageSchema` | `requestId` / `snapshotId`-keyed response with `items`, `hasMore`, and optional `error`. |
|
|
162
|
+
| `createPageResult` | `CreatePageResultMessage` / `createPageResultMessageSchema` | `requestId`-keyed page response with `status: "success"` or `status: "error"`. |
|
|
163
|
+
| `getPageResult` | `GetPageResultMessage` / `getPageResultMessageSchema` | Same success/error shape as `createPageResult`. |
|
|
164
|
+
| `updatePageResult` | `UpdatePageResultMessage` / `updatePageResultMessageSchema` | Same success/error shape as `createPageResult`. |
|
|
165
|
+
| `getUserResult` | `GetUserResultMessage` / `getUserResultMessageSchema` | `requestId`-keyed user response with `status: "success"` or `status: "error"`. |
|
|
166
|
+
| `listUsersResult` | `ListUsersResultMessage` / `listUsersResultMessageSchema` | Same success/error shape as `getUserResult`. |
|
|
167
|
+
| `invalidSandboxMessage` | `InvalidSandboxMessage` / `invalidSandboxMessageSchema` | Host-side NACK for a sandbox message it could not parse. |
|
|
168
|
+
|
|
169
|
+
`HostToSandboxMessage` / `hostToSandboxMessageSchema` is the discriminated union over all of the above.
|
|
170
|
+
|
|
171
|
+
For result messages with `status: "error"` and for `queryDataSourceResult.error`, `error` always has the `CustomBlockErrorInfo<TCode>` shape. The concrete result types narrow `TCode` to the API-specific union, such as `CustomBlockCreatePageErrorCode`, `CustomBlockUpdatePageErrorCode`, or `CustomBlockQueryDataSourceErrorCode`.
|
package/README.md
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# @notionhq/custom-blocks
|
|
2
|
+
|
|
3
|
+
> [!NOTE]
|
|
4
|
+
> Nothing to see here. This is **extreme** alpha and currently only works for an unreleased product.
|
|
5
|
+
|
|
6
|
+
SDK for building Notion custom view blocks.
|
|
7
|
+
|
|
8
|
+
A custom view runs as a sandboxed `<iframe>` inside a Notion block on iOS, Android, and desktop. The only channel between your view and Notion is a `postMessage` bridge — this SDK wraps it in typed React hooks plus a small framework-neutral runtime API for non-React renderers.
|
|
9
|
+
|
|
10
|
+
> **Pre-release.** Breaking changes may land at any time before 1.0.
|
|
11
|
+
|
|
12
|
+
## Quick start
|
|
13
|
+
|
|
14
|
+
```tsx
|
|
15
|
+
// src/index.tsx
|
|
16
|
+
import { NotionCustomBlock } from "@notionhq/custom-blocks/react";
|
|
17
|
+
import ReactDOM from "react-dom/client";
|
|
18
|
+
import { App } from "./App";
|
|
19
|
+
|
|
20
|
+
ReactDOM.createRoot(document.getElementById("root")!).render(
|
|
21
|
+
<NotionCustomBlock>
|
|
22
|
+
<App />
|
|
23
|
+
</NotionCustomBlock>,
|
|
24
|
+
);
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
```tsx
|
|
28
|
+
// src/App.tsx
|
|
29
|
+
import { useBlockId, useTheme } from "@notionhq/custom-blocks/react";
|
|
30
|
+
|
|
31
|
+
export function App() {
|
|
32
|
+
const blockId = useBlockId();
|
|
33
|
+
const theme = useTheme();
|
|
34
|
+
return <div data-theme={theme}>Hello from {blockId}.</div>;
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
`<NotionCustomBlock>` runs the SDK ↔ host handshake (sends `ready` with the manifest, awaits `init`) and only mounts `children` once it resolves. Inside the wrapper, every hook returns non-nullable values — there's no separate gating component to write. It also runs `useCustomBlockAutoResize` for you by default; pass `autoResize={false}` to opt out.
|
|
39
|
+
|
|
40
|
+
## Reference
|
|
41
|
+
|
|
42
|
+
API surface, one page per category. Import framework-neutral APIs from `@notionhq/custom-blocks`; import React hooks and components from `@notionhq/custom-blocks/react`. Hover docs in your editor cover the per-field detail; these pages cover usage shape and the gotchas.
|
|
43
|
+
|
|
44
|
+
- [`docs/lifecycle.md`](./docs/lifecycle.md) — `<NotionCustomBlock>`, `useCustomBlockInit`, `initCustomBlock`, `customBlock.autoResize`, `NotInIframeError`, `useCustomBlockAutoResize`. The handshake, the React wrapper, sizing.
|
|
45
|
+
- [`docs/block-location.md`](./docs/block-location.md) — `useBlockId`, `useParent`, `usePage`, `useTheme`. Where the block sits in the document tree and the host's color scheme.
|
|
46
|
+
- [`docs/data-sources.md`](./docs/data-sources.md) — `useDataSource`, `useManifest`, `customBlock.getManifest`, the row, property, and date-value types, plus a worked example.
|
|
47
|
+
- [`docs/pages.md`](./docs/pages.md) — `pages.create / get / update / delete`, parent variants (including the recommended `data_source_key`), property input shapes.
|
|
48
|
+
- [`docs/users.md`](./docs/users.md) — `users.list / get`, the `NotionUser` shape, paging.
|
|
49
|
+
- [`docs/manifest.md`](./docs/manifest.md) — `custom_blocks.json`, the Vite plugin, manifest types.
|
|
50
|
+
|
|
51
|
+
## Forbidden APIs
|
|
52
|
+
|
|
53
|
+
No top-level navigation, `window.open`, or auth redirects. No direct network requests — cross-origin work goes through the host (and is exposed via SDK hooks).
|
|
54
|
+
|
|
55
|
+
## Bridge protocol
|
|
56
|
+
|
|
57
|
+
The bridge speaks a versioned `postMessage` protocol. You shouldn't need protocol-level details to build a view. If you're implementing a host, see [`HOST.md`](./HOST.md).
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { extractId, formatUuid } from "./ids.js";
|
|
2
|
+
import { ntnApi, ntnApiPatch, ntnWorkersCapabilities } from "./ntn.js";
|
|
3
|
+
// ── Capability resolution ───────────────────────────────────────
|
|
4
|
+
function normalizeCapabilities(raw) {
|
|
5
|
+
if (Array.isArray(raw)) {
|
|
6
|
+
return raw;
|
|
7
|
+
}
|
|
8
|
+
if (raw && typeof raw === "object") {
|
|
9
|
+
const obj = raw;
|
|
10
|
+
for (const field of ["capabilities", "results", "data"]) {
|
|
11
|
+
if (Array.isArray(obj[field])) {
|
|
12
|
+
return obj[field];
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return [];
|
|
17
|
+
}
|
|
18
|
+
function capabilityKey(cap) {
|
|
19
|
+
for (const field of ["key", "name", "id"]) {
|
|
20
|
+
const value = cap[field];
|
|
21
|
+
if (typeof value === "string" && value.length > 0) {
|
|
22
|
+
return value;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
function capabilityType(cap) {
|
|
28
|
+
// `ntn` reports the capability kind under `_tag`; accept `type` too in case
|
|
29
|
+
// the wire shape shifts.
|
|
30
|
+
for (const field of ["_tag", "type"]) {
|
|
31
|
+
const value = cap[field];
|
|
32
|
+
if (typeof value === "string") {
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
function isCustomBlockCapability(cap) {
|
|
39
|
+
const type = capabilityType(cap);
|
|
40
|
+
// When no type field is present we can't filter — keep it and let the
|
|
41
|
+
// count-based selection below decide.
|
|
42
|
+
if (type === undefined) {
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
return type === "customBlock" || type === "custom_block";
|
|
46
|
+
}
|
|
47
|
+
function firstString(obj, fields) {
|
|
48
|
+
for (const field of fields) {
|
|
49
|
+
const value = obj[field];
|
|
50
|
+
if (typeof value === "string" && value.length > 0) {
|
|
51
|
+
return value;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
function capabilityDefinitionId(cap) {
|
|
57
|
+
const top = firstString(cap, ["definition_id", "definitionId"]);
|
|
58
|
+
if (top) {
|
|
59
|
+
return top;
|
|
60
|
+
}
|
|
61
|
+
// The real `ntn` shape nests it under `state.definitionId`.
|
|
62
|
+
const state = cap.state;
|
|
63
|
+
if (state && typeof state === "object") {
|
|
64
|
+
const nested = firstString(state, [
|
|
65
|
+
"definitionId",
|
|
66
|
+
"definition_id",
|
|
67
|
+
]);
|
|
68
|
+
if (nested) {
|
|
69
|
+
return nested;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// Some shapes nest the definition under a `definition` object.
|
|
73
|
+
const definition = cap.definition;
|
|
74
|
+
if (definition && typeof definition === "object") {
|
|
75
|
+
const id = definition.id;
|
|
76
|
+
if (typeof id === "string" && id.length > 0) {
|
|
77
|
+
return id;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Resolve a worker's custom-block capability to a `definition_id`.
|
|
84
|
+
*
|
|
85
|
+
* - Filters `ntn workers capabilities list` down to customBlock capabilities.
|
|
86
|
+
* - With `capability` set, picks the entry with that key.
|
|
87
|
+
* - With exactly one candidate, uses it.
|
|
88
|
+
* - With more than one and no `capability`, errors with the available keys.
|
|
89
|
+
*/
|
|
90
|
+
export function resolveDefinitionId(capability) {
|
|
91
|
+
const raw = ntnWorkersCapabilities();
|
|
92
|
+
const all = normalizeCapabilities(raw);
|
|
93
|
+
if (all.length === 0) {
|
|
94
|
+
console.error("Error: no worker capabilities found. Deploy the worker first with `ntn workers deploy`.\n");
|
|
95
|
+
console.error(" Raw `ntn workers capabilities list --json` output:");
|
|
96
|
+
console.error(JSON.stringify(raw, null, "\t"));
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
99
|
+
const customBlocks = all.filter(isCustomBlockCapability);
|
|
100
|
+
if (customBlocks.length === 0) {
|
|
101
|
+
console.error("Error: this worker exposes no customBlock capability to attach.\n");
|
|
102
|
+
process.exit(1);
|
|
103
|
+
}
|
|
104
|
+
let selected;
|
|
105
|
+
if (capability) {
|
|
106
|
+
const match = customBlocks.find(c => capabilityKey(c) === capability);
|
|
107
|
+
if (!match) {
|
|
108
|
+
const keys = customBlocks.map(c => capabilityKey(c) ?? "(unnamed)");
|
|
109
|
+
console.error(`Error: no customBlock capability named '${capability}'.\n`);
|
|
110
|
+
console.error(` Available: ${keys.join(", ")}`);
|
|
111
|
+
process.exit(1);
|
|
112
|
+
}
|
|
113
|
+
selected = match;
|
|
114
|
+
}
|
|
115
|
+
else if (customBlocks.length > 1) {
|
|
116
|
+
const keys = customBlocks.map(c => capabilityKey(c) ?? "(unnamed)");
|
|
117
|
+
console.error(`Error: this worker exposes ${customBlocks.length} customBlock capabilities. Pick one with --capability <key>.\n`);
|
|
118
|
+
console.error(` Available: ${keys.join(", ")}`);
|
|
119
|
+
process.exit(1);
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
selected = customBlocks[0];
|
|
123
|
+
}
|
|
124
|
+
const definitionId = capabilityDefinitionId(selected);
|
|
125
|
+
if (!definitionId) {
|
|
126
|
+
console.error("Error: could not read a definition_id from the selected capability.\n");
|
|
127
|
+
console.error(" Raw capability entry:");
|
|
128
|
+
console.error(JSON.stringify(selected, null, "\t"));
|
|
129
|
+
process.exit(1);
|
|
130
|
+
}
|
|
131
|
+
return { definitionId, capabilityKey: capabilityKey(selected) };
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Body for pointing a custom block at a worker's definition. The update-block
|
|
135
|
+
* endpoint requires the payload be keyed by block type — a top-level
|
|
136
|
+
* `definition_id` is rejected (`should be not present`). So it nests under
|
|
137
|
+
* `custom_block`, matching how `patchBlock` (datasources.ts) sets data_sources.
|
|
138
|
+
*/
|
|
139
|
+
function definitionPatchBody(definitionId) {
|
|
140
|
+
return { custom_block: { definition_id: definitionId } };
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Point an existing custom block at a worker's customBlock definition.
|
|
144
|
+
*
|
|
145
|
+
* Resolves the block, verifies it's a `custom_block`, resolves the worker's
|
|
146
|
+
* definition_id (erroring on ambiguity), then PATCHes the block. Returns the
|
|
147
|
+
* dashed block UUID so callers (e.g. `create`) can chain.
|
|
148
|
+
*/
|
|
149
|
+
export function attach(options) {
|
|
150
|
+
const uuid = formatUuid(extractId(options.idOrUrl));
|
|
151
|
+
if (!options.jsonOutput) {
|
|
152
|
+
console.log(`\nFetching block ${uuid}…`);
|
|
153
|
+
}
|
|
154
|
+
const block = ntnApi(`/v1/blocks/${uuid}`);
|
|
155
|
+
if (block.type !== "custom_block") {
|
|
156
|
+
console.error(`Error: block ${uuid} is type "${block.type}", not "custom_block".\n`);
|
|
157
|
+
console.error(" Pass a custom block's ID/URL, or create one with `ncblock create`.");
|
|
158
|
+
process.exit(1);
|
|
159
|
+
}
|
|
160
|
+
const { definitionId, capabilityKey: key } = resolveDefinitionId(options.capability);
|
|
161
|
+
if (!options.jsonOutput) {
|
|
162
|
+
console.log(`Attaching definition ${definitionId}${key ? ` (capability "${key}")` : ""}…`);
|
|
163
|
+
}
|
|
164
|
+
ntnApiPatch(`/v1/blocks/${uuid}`, definitionPatchBody(definitionId));
|
|
165
|
+
const emitResult = options.emitResult ?? true;
|
|
166
|
+
if (options.jsonOutput) {
|
|
167
|
+
if (emitResult) {
|
|
168
|
+
console.log(JSON.stringify({
|
|
169
|
+
action: "attach",
|
|
170
|
+
block_id: uuid,
|
|
171
|
+
definition_id: definitionId,
|
|
172
|
+
capability: key,
|
|
173
|
+
}, null, "\t"));
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
console.log(`\n✓ Attached block ${uuid} to definition ${definitionId}\n`);
|
|
178
|
+
}
|
|
179
|
+
return uuid;
|
|
180
|
+
}
|
package/bin/cli/cli.js
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { attach } from "./attach.js";
|
|
3
|
+
import { create } from "./create.js";
|
|
4
|
+
import { connect, getDataSources } from "./datasources.js";
|
|
5
|
+
import { deploy } from "./deploy.js";
|
|
6
|
+
import { pullData } from "./pullData.js";
|
|
7
|
+
import { pullManifest } from "./pullManifest.js";
|
|
8
|
+
const HELP = `
|
|
9
|
+
ncblock — Notion Custom Block SDK CLI
|
|
10
|
+
|
|
11
|
+
Usage:
|
|
12
|
+
ncblock connect <database-url-or-id> [options]
|
|
13
|
+
ncblock connect [--block <id>] [--bind <key>=<ds-id>] [options]
|
|
14
|
+
ncblock data_sources get <block-id-or-url>
|
|
15
|
+
ncblock deploy <dist-path> [--block <id>] [options]
|
|
16
|
+
ncblock pull_data <id-or-url> [--limit <n>] [--out <path>]
|
|
17
|
+
ncblock attach <block-id-or-url> [--capability <key>]
|
|
18
|
+
ncblock create [--parent <id-or-url>] [--capability <key>]
|
|
19
|
+
|
|
20
|
+
Commands:
|
|
21
|
+
connect Pull the data source schema and PATCH the block(s) in .notion/target.json
|
|
22
|
+
data_sources get Read data source bindings from a custom block
|
|
23
|
+
deploy Run \`ntn custom deploy\` for each block; appends --block IDs into target.json on success
|
|
24
|
+
pull_data Resolve an ID/URL to a data source; print its schema + N rows as JSON to stdout
|
|
25
|
+
attach Point a custom block at the worker's definition (from \`ntn workers capabilities\`)
|
|
26
|
+
create Create a custom block (private page parent by default) and attach it
|
|
27
|
+
|
|
28
|
+
Options:
|
|
29
|
+
--block <id> Block ID/URL (repeatable; accumulates into target.json's block_id[])
|
|
30
|
+
--bind <key>=<id> Bind a manifest key to a data source ID (repeatable)
|
|
31
|
+
--capability <key> customBlock capability key to attach (required if the worker has more than one)
|
|
32
|
+
--parent <id> Parent ID/URL for \`create\` (a page hosts the block; omit for a new private page)
|
|
33
|
+
--limit <n> Rows to pull for \`pull_data\` (default: 25, max: 100)
|
|
34
|
+
--env <name> Notion env (defaults to target.json's env, or "production")
|
|
35
|
+
--key <key> Data source key in manifest (default: "default") — for connect <id> only
|
|
36
|
+
--out <path> Output path — manifest for connect, JSON file for pull_data
|
|
37
|
+
--manifest <path> Path to custom_blocks.json (default: "./custom_blocks.json")
|
|
38
|
+
--dry-run Print the intended action without performing it
|
|
39
|
+
--json Emit machine-parseable JSON output
|
|
40
|
+
--help, -h Show this help message
|
|
41
|
+
|
|
42
|
+
Examples:
|
|
43
|
+
ncblock connect <db-url>
|
|
44
|
+
ncblock connect --block <block-id> --bind default=<db-id>
|
|
45
|
+
ncblock data_sources get <block-id>
|
|
46
|
+
ncblock deploy dist/
|
|
47
|
+
ncblock pull_data <db-url> --limit 50 --out sample.json
|
|
48
|
+
ncblock attach <block-url> --capability hello
|
|
49
|
+
ncblock create
|
|
50
|
+
`;
|
|
51
|
+
function parseArgs(argv) {
|
|
52
|
+
const flags = {};
|
|
53
|
+
const bindings = {};
|
|
54
|
+
const blocks = [];
|
|
55
|
+
const positional = [];
|
|
56
|
+
let i = 2;
|
|
57
|
+
while (i < argv.length) {
|
|
58
|
+
const arg = argv[i];
|
|
59
|
+
if (arg === "--key") {
|
|
60
|
+
flags.key = argv[++i];
|
|
61
|
+
}
|
|
62
|
+
else if (arg === "--out") {
|
|
63
|
+
flags.out = argv[++i];
|
|
64
|
+
}
|
|
65
|
+
else if (arg === "--dry-run") {
|
|
66
|
+
flags.dryRun = true;
|
|
67
|
+
}
|
|
68
|
+
else if (arg === "--json") {
|
|
69
|
+
flags.jsonOutput = true;
|
|
70
|
+
}
|
|
71
|
+
else if (arg === "--quiet") {
|
|
72
|
+
flags.quiet = true;
|
|
73
|
+
}
|
|
74
|
+
else if (arg === "--manifest") {
|
|
75
|
+
flags.manifest = argv[++i];
|
|
76
|
+
}
|
|
77
|
+
else if (arg === "--capability") {
|
|
78
|
+
flags.capability = argv[++i];
|
|
79
|
+
}
|
|
80
|
+
else if (arg === "--parent") {
|
|
81
|
+
flags.parent = argv[++i];
|
|
82
|
+
}
|
|
83
|
+
else if (arg === "--limit") {
|
|
84
|
+
flags.limit = argv[++i];
|
|
85
|
+
}
|
|
86
|
+
else if (arg === "--env") {
|
|
87
|
+
flags.env = argv[++i];
|
|
88
|
+
}
|
|
89
|
+
else if (arg === "--block") {
|
|
90
|
+
const value = argv[++i];
|
|
91
|
+
if (!value) {
|
|
92
|
+
console.error("Error: --block requires a value.\n");
|
|
93
|
+
console.error("Usage: --block <id-or-url>");
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
96
|
+
blocks.push(value);
|
|
97
|
+
}
|
|
98
|
+
else if (arg === "--bind") {
|
|
99
|
+
const pair = argv[++i];
|
|
100
|
+
const eq = pair?.indexOf("=");
|
|
101
|
+
if (!pair || eq === undefined || eq < 1) {
|
|
102
|
+
console.error(`Error: invalid --bind value: ${pair}\n`);
|
|
103
|
+
console.error("Expected format: --bind <key>=<data-source-id>");
|
|
104
|
+
process.exit(1);
|
|
105
|
+
}
|
|
106
|
+
bindings[pair.slice(0, eq)] = pair.slice(eq + 1);
|
|
107
|
+
}
|
|
108
|
+
else if (arg === "--help" || arg === "-h") {
|
|
109
|
+
flags.help = true;
|
|
110
|
+
}
|
|
111
|
+
else if (!arg.startsWith("-")) {
|
|
112
|
+
positional.push(arg);
|
|
113
|
+
}
|
|
114
|
+
i++;
|
|
115
|
+
}
|
|
116
|
+
return { flags, bindings, blocks, positional };
|
|
117
|
+
}
|
|
118
|
+
function main() {
|
|
119
|
+
const { flags, bindings, blocks, positional } = parseArgs(process.argv);
|
|
120
|
+
if (flags.help || positional.length === 0) {
|
|
121
|
+
console.log(HELP);
|
|
122
|
+
process.exit(0);
|
|
123
|
+
}
|
|
124
|
+
const [command, subcommand, ...rest] = positional;
|
|
125
|
+
// `manifest pull` is kept as an internal/advanced command — not advertised
|
|
126
|
+
// in HELP, but still callable for users who want pull without the PATCH
|
|
127
|
+
// step. Prefer `ncblock connect <id>`.
|
|
128
|
+
if (command === "manifest" && subcommand === "pull") {
|
|
129
|
+
const idOrUrl = rest[0];
|
|
130
|
+
if (!idOrUrl) {
|
|
131
|
+
console.error("Error: missing <id-or-url> argument.\n");
|
|
132
|
+
console.error("Usage: ncblock manifest pull <id-or-url> [options]");
|
|
133
|
+
process.exit(1);
|
|
134
|
+
}
|
|
135
|
+
pullManifest({
|
|
136
|
+
idOrUrl,
|
|
137
|
+
key: flags.key,
|
|
138
|
+
out: flags.out,
|
|
139
|
+
dryRun: flags.dryRun,
|
|
140
|
+
quiet: flags.quiet,
|
|
141
|
+
});
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (command === "connect") {
|
|
145
|
+
// `ncblock connect <database-url-or-id>` pulls the schema first then
|
|
146
|
+
// runs the binding/PATCH flow against target.json. The positional
|
|
147
|
+
// must point at a database (or a data source); ID-kind classification
|
|
148
|
+
// (block vs view vs wrong-workspace) happens upstream in `init.ts`.
|
|
149
|
+
connect({
|
|
150
|
+
idOrUrl: subcommand,
|
|
151
|
+
blockIds: blocks,
|
|
152
|
+
bindings,
|
|
153
|
+
key: flags.key,
|
|
154
|
+
out: flags.out,
|
|
155
|
+
manifest: flags.manifest,
|
|
156
|
+
env: flags.env,
|
|
157
|
+
dryRun: flags.dryRun,
|
|
158
|
+
jsonOutput: flags.jsonOutput,
|
|
159
|
+
quiet: flags.quiet,
|
|
160
|
+
});
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
if (command === "data_sources" && subcommand === "get") {
|
|
164
|
+
const idOrUrl = rest[0];
|
|
165
|
+
if (!idOrUrl) {
|
|
166
|
+
console.error("Error: missing <block-id-or-url> argument.\n");
|
|
167
|
+
console.error("Usage: ncblock data_sources get <block-id-or-url>");
|
|
168
|
+
process.exit(1);
|
|
169
|
+
}
|
|
170
|
+
getDataSources(idOrUrl);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
if (command === "deploy") {
|
|
174
|
+
const distPath = subcommand;
|
|
175
|
+
if (!distPath) {
|
|
176
|
+
console.error("Error: missing <dist-path> argument.\n");
|
|
177
|
+
console.error("Usage: ncblock deploy <dist-path> [--block <id>...] [--env <name>]");
|
|
178
|
+
process.exit(1);
|
|
179
|
+
}
|
|
180
|
+
deploy({
|
|
181
|
+
distPath,
|
|
182
|
+
blockIds: blocks,
|
|
183
|
+
env: flags.env,
|
|
184
|
+
dryRun: flags.dryRun,
|
|
185
|
+
jsonOutput: flags.jsonOutput,
|
|
186
|
+
});
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
// The commands below route their API calls through ntn.ts, which resolves
|
|
190
|
+
// the env from `process.env.ENV` (falling back to target.json). Honor an
|
|
191
|
+
// explicit --env by setting it here so every ntn call inherits it.
|
|
192
|
+
if (flags.env) {
|
|
193
|
+
process.env.ENV = flags.env;
|
|
194
|
+
}
|
|
195
|
+
if (command === "pull_data") {
|
|
196
|
+
const idOrUrl = subcommand;
|
|
197
|
+
if (!idOrUrl) {
|
|
198
|
+
console.error("Error: missing <id-or-url> argument.\n");
|
|
199
|
+
console.error("Usage: ncblock pull_data <id-or-url> [--limit <n>] [--out <path>]");
|
|
200
|
+
process.exit(1);
|
|
201
|
+
}
|
|
202
|
+
const limitRaw = flags.limit;
|
|
203
|
+
const limit = limitRaw === undefined ? undefined : Number(limitRaw);
|
|
204
|
+
if (limit !== undefined && !Number.isFinite(limit)) {
|
|
205
|
+
console.error(`Error: --limit must be a number, got '${limitRaw}'.`);
|
|
206
|
+
process.exit(1);
|
|
207
|
+
}
|
|
208
|
+
pullData({
|
|
209
|
+
idOrUrl,
|
|
210
|
+
limit,
|
|
211
|
+
out: flags.out,
|
|
212
|
+
});
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
if (command === "attach") {
|
|
216
|
+
const idOrUrl = subcommand;
|
|
217
|
+
if (!idOrUrl) {
|
|
218
|
+
console.error("Error: missing <block-id-or-url> argument.\n");
|
|
219
|
+
console.error("Usage: ncblock attach <block-id-or-url> [--capability <key>]");
|
|
220
|
+
process.exit(1);
|
|
221
|
+
}
|
|
222
|
+
attach({
|
|
223
|
+
idOrUrl,
|
|
224
|
+
capability: flags.capability,
|
|
225
|
+
jsonOutput: flags.jsonOutput,
|
|
226
|
+
});
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
if (command === "create") {
|
|
230
|
+
create({
|
|
231
|
+
parent: flags.parent,
|
|
232
|
+
capability: flags.capability,
|
|
233
|
+
jsonOutput: flags.jsonOutput,
|
|
234
|
+
});
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
console.error(`Unknown command: ${positional.join(" ")}\n`);
|
|
238
|
+
console.log(HELP);
|
|
239
|
+
process.exit(1);
|
|
240
|
+
}
|
|
241
|
+
try {
|
|
242
|
+
main();
|
|
243
|
+
}
|
|
244
|
+
catch (error) {
|
|
245
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
246
|
+
console.error(`\nError: ${message}\n`);
|
|
247
|
+
process.exit(1);
|
|
248
|
+
}
|