ncblock 0.0.3 → 0.0.5
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 +68 -0
- package/README.md +3 -0
- package/bridge/SandboxBridge.ts +659 -0
- package/bridge/context.ts +58 -0
- package/bridge/dataSources/dataSource.ts +63 -0
- package/bridge/dataSources/dataSourcePage.ts +69 -0
- package/bridge/dataSources/dataSourceValue.ts +19 -0
- package/bridge/dataSources/dateValue.ts +96 -0
- package/bridge/dataSources/propertySchema.ts +186 -0
- package/bridge/dataSources/recordPointer.ts +13 -0
- package/bridge/dataSources/resolve.ts +96 -0
- package/bridge/dataSources/resolveProperty.ts +96 -0
- package/bridge/hostState.ts +146 -0
- package/bridge/ids.ts +30 -0
- package/bridge/incomingType.ts +19 -0
- package/bridge/loadManifest.ts +54 -0
- package/bridge/manifest.ts +53 -0
- package/bridge/messages/contextChanged.ts +15 -0
- package/bridge/messages/createPage.ts +64 -0
- package/bridge/messages/createPageResult.ts +25 -0
- package/bridge/messages/dataSourcesChanged.ts +18 -0
- package/bridge/messages/getPage.ts +32 -0
- package/bridge/messages/getUser.ts +32 -0
- package/bridge/messages/hostToSandbox.ts +33 -0
- package/bridge/messages/init.ts +20 -0
- package/bridge/messages/invalidHostMessage.ts +16 -0
- package/bridge/messages/invalidSandboxMessage.ts +18 -0
- package/bridge/messages/listUsers.ts +33 -0
- package/bridge/messages/queryDataSource.ts +16 -0
- package/bridge/messages/queryDataSourceResult.ts +18 -0
- package/bridge/messages/ready.ts +25 -0
- package/bridge/messages/resize.ts +13 -0
- package/bridge/messages/sandboxToHost.ts +30 -0
- package/bridge/messages/themeChanged.ts +15 -0
- package/bridge/messages/updatePage.ts +21 -0
- package/bridge/messages/updatePageResult.ts +24 -0
- package/bridge/pages/page.ts +314 -0
- package/bridge/pendingRequests.ts +28 -0
- package/bridge/sandboxClient.ts +112 -0
- package/bridge/theme.ts +5 -0
- package/bridge/users/user.ts +31 -0
- package/docs/context.md +45 -0
- package/docs/data-sources.md +161 -0
- package/docs/lifecycle.md +92 -0
- package/docs/manifest.md +42 -0
- package/docs/pages.md +143 -0
- package/docs/users.md +61 -0
- package/host.ts +67 -0
- package/index.ts +86 -0
- package/init.ts +92 -0
- package/package.json +15 -5
- package/react.tsx +418 -0
- package/types.ts +157 -0
- package/users.ts +26 -0
- package/utils.ts +13 -0
- package/vite-plugin/index.d.ts +46 -0
- package/vite-plugin/index.js +115 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# Data sources
|
|
2
|
+
|
|
3
|
+
A custom block declares its **data sources** — semantic keys like `people` or `tasks` — in `custom_blocks.json`. When the block is added to a Notion page, a user maps each key to a real Notion database. Inside the block, you read by the semantic key and the SDK handles the lookup for you. Use `useDataSource(key)` for the rows themselves, and `useDataSourceDefinitions()` when you need the schema (debug panels, schema-driven UIs).
|
|
4
|
+
|
|
5
|
+
## Pages within a data source
|
|
6
|
+
|
|
7
|
+
Each row returned by `useDataSource` is a `NotionDataSourcePage` — `{ id, propertiesById, propertiesByKey, update }`. Read property values through either of the two views:
|
|
8
|
+
|
|
9
|
+
- `propertiesByKey[key]` — keyed by the semantic property keys you declared in the manifest.
|
|
10
|
+
- `propertiesById[propertyId]` — keyed by the raw Notion property ID.
|
|
11
|
+
|
|
12
|
+
The four built-ins (`created_time`, `last_edited_time`, `created_by`, `last_edited_by`) are always present in `propertiesById` (and `collectionSchema.propertiesById`), never in the `*ByKey` views — they don't have semantic keys.
|
|
13
|
+
|
|
14
|
+
### Updating a row
|
|
15
|
+
|
|
16
|
+
Each page carries its own `update` helper:
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
await row.update({
|
|
20
|
+
properties: {
|
|
21
|
+
score: { type: "number", number: 8 }, // semantic key
|
|
22
|
+
},
|
|
23
|
+
icon: { type: "emoji", emoji: "✅" },
|
|
24
|
+
});
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Because the helper is bound to its data source, you can write property values keyed by **either** semantic keys or raw IDs — the SDK resolves them before sending the request. The input and result shapes are `NotionDataSourcePageUpdateInput` and `NotionDataSourcePageUpdateResult`.
|
|
28
|
+
|
|
29
|
+
Use `row.update(...)` whenever you already have a row in hand. For pages you don't have a row for (e.g. you only have a `pageId`), drop down to the top-level [`pages` API](./pages.md) — it covers create / get / update / delete and accepts raw property IDs only.
|
|
30
|
+
|
|
31
|
+
## API
|
|
32
|
+
|
|
33
|
+
### `useDataSource(key, initialLimit?)`
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
function useDataSource(key: string, initialLimit?: number): UseDataSourceResult;
|
|
37
|
+
|
|
38
|
+
type UseDataSourceResult = {
|
|
39
|
+
items: NotionDataSourcePage[];
|
|
40
|
+
collectionSchema?: NotionCollectionSchema;
|
|
41
|
+
propertySchemasById: { [propertyId: string]: NotionPropertySchema };
|
|
42
|
+
propertySchemasByKey: { [key: string]: NotionPropertySchema | undefined };
|
|
43
|
+
isLoading: boolean;
|
|
44
|
+
hasMore: boolean;
|
|
45
|
+
fetchMore: () => void;
|
|
46
|
+
error?: string;
|
|
47
|
+
};
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Reads the data source mapped to `key`. `initialLimit` defaults to 20. `fetchMore()` re-requests a larger prefix (no cursors yet); page growth tracks `initialLimit`, so pass a larger value to opt into bigger batches. `propertySchemasByKey` is `undefined` for declared-but-unbound slots. The hook resets to `initialLimit` when the underlying definition changes.
|
|
51
|
+
|
|
52
|
+
### `useDataSourceDefinitions()`
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
function useDataSourceDefinitions(): NotionDataSource[];
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Resolved data-source definitions (semantic key plus `collectionSchema`, `propertyIdsByKey`, derived `propertySchemasById`). Most templates use `useDataSource` instead — reach for this hook only when rendering the configuration itself (debug panels, schema-driven UIs).
|
|
59
|
+
|
|
60
|
+
## Example: querying a data source
|
|
61
|
+
|
|
62
|
+
A typical data-driven view picks a key, calls `useDataSource`, schema-checks the rows, and surfaces a setup hint when the mapped collection is missing the expected fields. Trimmed from `templates/radar-chart`:
|
|
63
|
+
|
|
64
|
+
```tsx
|
|
65
|
+
import {
|
|
66
|
+
type NotionDataSourcePage,
|
|
67
|
+
useDataSource,
|
|
68
|
+
} from "ncblock";
|
|
69
|
+
|
|
70
|
+
const KEY = "people";
|
|
71
|
+
|
|
72
|
+
function isComplete(item: NotionDataSourcePage): boolean {
|
|
73
|
+
return (
|
|
74
|
+
typeof item.propertiesByKey.name === "string" &&
|
|
75
|
+
typeof item.propertiesByKey.score === "number" &&
|
|
76
|
+
Number.isFinite(item.propertiesByKey.score)
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function ScoreList() {
|
|
81
|
+
const { items, isLoading, hasMore, fetchMore, error } = useDataSource(KEY);
|
|
82
|
+
|
|
83
|
+
if (error) return <div role="alert">Couldn't load: {error}</div>;
|
|
84
|
+
if (isLoading && items.length === 0) return <div>Loading…</div>;
|
|
85
|
+
|
|
86
|
+
const ready = items.filter(isComplete);
|
|
87
|
+
if (ready.length === 0) {
|
|
88
|
+
return (
|
|
89
|
+
<div>
|
|
90
|
+
Map a data source with key <code>{KEY}</code> exposing <code>name</code>{" "}
|
|
91
|
+
(text) and <code>score</code> (number).
|
|
92
|
+
</div>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<div>
|
|
98
|
+
<ul>
|
|
99
|
+
{ready.map((item) => (
|
|
100
|
+
<li key={item.id}>
|
|
101
|
+
{String(item.propertiesByKey.name)} —{" "}
|
|
102
|
+
{Number(item.propertiesByKey.score)}
|
|
103
|
+
</li>
|
|
104
|
+
))}
|
|
105
|
+
</ul>
|
|
106
|
+
{hasMore ? (
|
|
107
|
+
<button type="button" onClick={fetchMore} disabled={isLoading}>
|
|
108
|
+
{isLoading ? "Loading…" : "Load more"}
|
|
109
|
+
</button>
|
|
110
|
+
) : null}
|
|
111
|
+
</div>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Types
|
|
117
|
+
|
|
118
|
+
### Rows & values
|
|
119
|
+
|
|
120
|
+
- `NotionDataSource` — a resolved data source: semantic key, `collectionSchema`, `propertyIdsByKey`, `propertySchemasById`.
|
|
121
|
+
- `NotionDataSourcePage` — a single row exposed to app code: `{ id, propertiesById, propertiesByKey, update }`.
|
|
122
|
+
- `NotionDataSourceValue` — the discriminated union of values that can appear inside `propertiesById[propertyId]`. Date values branch into the `NotionDateValue` union below.
|
|
123
|
+
- `NotionDataSourcePageUpdateInput` / `NotionDataSourcePageUpdateResult` — input and result for the per-page `update` helper.
|
|
124
|
+
|
|
125
|
+
### Property schemas
|
|
126
|
+
|
|
127
|
+
- `NotionPropertySchema` — schema for a single property (type plus type-specific config like `select` options).
|
|
128
|
+
- `NotionPropertyType` — string-literal union of every supported property type.
|
|
129
|
+
- `NOTION_PROPERTY_TYPES` — runtime list of those literals (handy for switch coverage and validation).
|
|
130
|
+
- `NotionPropertyOption` — a single `select` / `multi_select` / `status` option (`{ id, name, color }`).
|
|
131
|
+
- `NotionPropertyColor` — the color literal used by options and groups.
|
|
132
|
+
- `NotionStatusGroup` — the `status` property's "To do / In progress / Done" grouping.
|
|
133
|
+
- `NotionDualProperty` — properties that have both a primary and a secondary axis (e.g. `unique_id` prefix + number).
|
|
134
|
+
- `NotionBuiltinPropertyId` — string-literal union of the four synthetic property IDs (`created_time`, `last_edited_time`, `created_by`, `last_edited_by`).
|
|
135
|
+
- `NOTION_BUILTIN_PROPERTY_IDS` — runtime list of the four built-in IDs.
|
|
136
|
+
|
|
137
|
+
### Collection schema & pointers
|
|
138
|
+
|
|
139
|
+
- `NotionCollectionSchema` — host-supplied schema for the bound collection, including raw property schemas.
|
|
140
|
+
- `NotionRecordPointer` — `{ id, table }`. Generic reference to any Notion record (page, block, collection row). Exported for convenience; `useDataSource` and the `pages` API don't take one as input.
|
|
141
|
+
|
|
142
|
+
### IDs
|
|
143
|
+
|
|
144
|
+
Branded string types — they're plain strings at runtime but TypeScript distinguishes them.
|
|
145
|
+
|
|
146
|
+
- `NotionDataSourceId`
|
|
147
|
+
- `NotionSpaceId`
|
|
148
|
+
|
|
149
|
+
### Date values
|
|
150
|
+
|
|
151
|
+
Returned wherever a date / date-range value appears (e.g. inside `propertiesByKey` for a `date` property). All-day values use `NotionDate*` shapes; values with a time component use `NotionDateTime*`.
|
|
152
|
+
|
|
153
|
+
- `NotionDateValue` — discriminated union covering every date-shaped value below.
|
|
154
|
+
- `NotionDate`
|
|
155
|
+
- `NotionDateRange`
|
|
156
|
+
- `NotionDateTime`
|
|
157
|
+
- `NotionDateTimeRange`
|
|
158
|
+
- `NotionDateReminder`
|
|
159
|
+
- `NotionDateTimeReminder`
|
|
160
|
+
- `NotionTimeReminder`
|
|
161
|
+
- `NotionNoReminder`
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# Lifecycle
|
|
2
|
+
|
|
3
|
+
The SDK ↔ host handshake, the React wrapper that runs it, and the auto-resize hook that keeps the iframe in sync with your content.
|
|
4
|
+
|
|
5
|
+
## Handshake
|
|
6
|
+
|
|
7
|
+
`initCustomBlock()` posts `ready` to `window.parent` and awaits the host's `init` (theme, context, and `dataSources: { bindings }` keyed by semantic data-source key, which the SDK resolves against the manifest). The promise resolves with the normalized initial state, captured in `CustomBlockInitial` — `await` it before mounting React so hooks always see populated state.
|
|
8
|
+
|
|
9
|
+
- Default `timeoutMs` is 2000; rejects with `TimeoutError` if the host doesn't respond.
|
|
10
|
+
- In a top-level browser tab (no parent frame), rejects with `NotInIframeError`. `<NotionCustomBlock>` catches this, seeds placeholders, and renders `children` behind a warning banner so dev-time previews still work.
|
|
11
|
+
- After init, `themeChanged`, `contextChanged`, and `dataSourcesChanged` push updates and the relevant hooks re-render.
|
|
12
|
+
- `initCustomBlock` is idempotent; subsequent calls return the same promise.
|
|
13
|
+
|
|
14
|
+
## Sizing
|
|
15
|
+
|
|
16
|
+
The host owns width and height. Inside the iframe, `100vh` ≠ a screen and there's no meaningful "device width" — only iframe width. Layouts must reflow from a phone column to a desktop block.
|
|
17
|
+
|
|
18
|
+
- **Self-sizing content** is the default — `<NotionCustomBlock>` measures `#root` and posts `resize` messages so the iframe tracks your content. Pass `autoResize={false}` for full-bleed views, or to drive `useCustomBlockAutoResize` yourself.
|
|
19
|
+
- Prefer container queries (`@container`) over viewport queries.
|
|
20
|
+
|
|
21
|
+
## API
|
|
22
|
+
|
|
23
|
+
All hooks below assume `initCustomBlock()` has resolved — they throw if called before that. Inside `<NotionCustomBlock>` (or past the `isLoaded` gate of `useCustomBlockInit`), single-value hooks return non-nullable values.
|
|
24
|
+
|
|
25
|
+
### `<NotionCustomBlock>`
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
type NotionCustomBlockProps = InitCustomBlockOptions & {
|
|
29
|
+
children: ReactNode;
|
|
30
|
+
fallback?: ReactNode;
|
|
31
|
+
errorFallback?: ReactNode | ((error: Error) => ReactNode);
|
|
32
|
+
autoResize?: boolean; // defaults to true
|
|
33
|
+
};
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Top-level wrapper. Runs the handshake, gates `children`, and (by default) drives auto-resize. `fallback` replaces the loading view (default `null`); `errorFallback` replaces the inline `<p role="alert">` shown if init rejects. `timeoutMs` flows through to `initCustomBlock`. Pass `autoResize={false}` for full-bleed views or to call `useCustomBlockAutoResize` yourself.
|
|
37
|
+
|
|
38
|
+
### `useCustomBlockInit(opts?)`
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
function useCustomBlockInit(opts?: InitCustomBlockOptions): UseCustomBlockInitResult;
|
|
42
|
+
|
|
43
|
+
type UseCustomBlockInitResult =
|
|
44
|
+
| { isLoaded: false; error: undefined }
|
|
45
|
+
| { isLoaded: false; error: Error }
|
|
46
|
+
| { isLoaded: true; error: undefined; initial: CustomBlockInitial };
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
React wrapper around `initCustomBlock` for templates that prefer not to use top-level `await`. Multiple components calling it share the same handshake.
|
|
50
|
+
|
|
51
|
+
```tsx
|
|
52
|
+
function Root() {
|
|
53
|
+
const init = useCustomBlockInit();
|
|
54
|
+
if (init.error) return <p role="alert">Init failed: {init.error.message}</p>;
|
|
55
|
+
if (!init.isLoaded) return null;
|
|
56
|
+
return <App />;
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### `initCustomBlock(opts?)`
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
function initCustomBlock(opts?: InitCustomBlockOptions): Promise<CustomBlockInitial>;
|
|
64
|
+
|
|
65
|
+
type InitCustomBlockOptions = { timeoutMs?: number };
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
The lower-level promise API. `<NotionCustomBlock>` and `useCustomBlockInit` both call it for you. Reach for it directly only when you want to `await` init at module scope (e.g. before `ReactDOM.createRoot`).
|
|
69
|
+
|
|
70
|
+
### `NotInIframeError`
|
|
71
|
+
|
|
72
|
+
Thrown when `initCustomBlock` is called in a top-level tab (no parent frame). `<NotionCustomBlock>` catches it and falls back to a standalone preview with a warning banner; direct callers can `instanceof NotInIframeError` to apply their own policy.
|
|
73
|
+
|
|
74
|
+
### `useCustomBlockAutoResize({ enabled? })`
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
function useCustomBlockAutoResize(args?: { enabled?: boolean }): void;
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Measures `#root` with `Math.ceil(scrollHeight)` and posts `resize` messages, deduping unchanged values. `<NotionCustomBlock>` runs this for you — only call it directly when you want to drive `enabled` yourself (e.g. a debug toggle), and pair with `autoResize={false}` so it doesn't run twice.
|
|
81
|
+
|
|
82
|
+
```tsx
|
|
83
|
+
<NotionCustomBlock autoResize={false}>
|
|
84
|
+
<App />
|
|
85
|
+
</NotionCustomBlock>;
|
|
86
|
+
|
|
87
|
+
function App() {
|
|
88
|
+
const [enabled, setEnabled] = useState(true);
|
|
89
|
+
useCustomBlockAutoResize({ enabled });
|
|
90
|
+
return <div>…</div>;
|
|
91
|
+
}
|
|
92
|
+
```
|
package/docs/manifest.md
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Manifest
|
|
2
|
+
|
|
3
|
+
A custom view declares its required data sources in `custom_blocks.json` at the project root. Notion uses the manifest to know what semantic keys the block expects, what shape each property should be, and what to show when an admin is configuring the block.
|
|
4
|
+
|
|
5
|
+
```json
|
|
6
|
+
{
|
|
7
|
+
"version": 1,
|
|
8
|
+
"dataSources": {
|
|
9
|
+
"tasks": {
|
|
10
|
+
"name": "Tasks",
|
|
11
|
+
"description": "The collection of tasks to render",
|
|
12
|
+
"properties": {
|
|
13
|
+
"title": { "name": "Title", "type": "title" },
|
|
14
|
+
"dueDate": { "name": "Due date", "type": "date" }
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
`initCustomBlock()` fetches `custom_blocks.json` and forwards it with `ready`. The `notionCustomBlock()` Vite plugin from `ncblock/vite` serves it in dev and emits it into `dist/` on build. If the file is missing or invalid, the SDK sends `manifest: null`.
|
|
22
|
+
|
|
23
|
+
## Vite plugin
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
import { defineConfig } from "vite";
|
|
27
|
+
import react from "@vitejs/plugin-react";
|
|
28
|
+
import { notionCustomBlock } from "ncblock/vite";
|
|
29
|
+
|
|
30
|
+
export default defineConfig({
|
|
31
|
+
plugins: [react(), notionCustomBlock()],
|
|
32
|
+
});
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
In dev, the plugin serves `custom_blocks.json` from the project root so HMR + the SDK handshake see the same file. On `vite build`, it emits `custom_blocks.json` into `dist/` as a separate asset alongside the bundled HTML and JS.
|
|
36
|
+
|
|
37
|
+
## Types
|
|
38
|
+
|
|
39
|
+
- `CustomBlockManifest` — the parsed shape of `custom_blocks.json`.
|
|
40
|
+
- `ManifestDataSource` — a single entry in `dataSources` (name, description, properties).
|
|
41
|
+
- `ManifestProperty` — a single property declaration inside a `ManifestDataSource`.
|
|
42
|
+
- `ManifestIcon` — the icon variant accepted on a `ManifestDataSource`.
|
package/docs/pages.md
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# `pages` API
|
|
2
|
+
|
|
3
|
+
Helpers for creating, reading, updating, and archiving Notion pages from inside a custom block. The SDK forwards each call to Notion on your behalf.
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
import { pages } from "ncblock";
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Every helper returns a discriminated result instead of throwing:
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
const result = await pages.get(pageId);
|
|
13
|
+
if (result.status === "success") {
|
|
14
|
+
// result.page is a NotionPage
|
|
15
|
+
} else {
|
|
16
|
+
// result.error is a human-readable string
|
|
17
|
+
}
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Always check `result.status` before reading `result.page`.
|
|
21
|
+
|
|
22
|
+
## Creating pages
|
|
23
|
+
|
|
24
|
+
`pages.create` mirrors Notion's [`POST /v1/pages`](https://developers.notion.com/reference/post-page). Pass a parent, a property map, and (optionally) an `icon`, `cover`, or `position`:
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
const result = await pages.create({
|
|
28
|
+
parent: { type: "data_source_key", key: "tasks" },
|
|
29
|
+
properties: {
|
|
30
|
+
title: {
|
|
31
|
+
type: "title",
|
|
32
|
+
title: [{ type: "text", text: { content: "New task" } }],
|
|
33
|
+
},
|
|
34
|
+
dueDate: { type: "date", date: { start: "2026-06-01" } },
|
|
35
|
+
},
|
|
36
|
+
icon: { type: "emoji", emoji: "📝" },
|
|
37
|
+
position: { type: "end" },
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
if (result.status === "success") {
|
|
41
|
+
console.log("created", result.page.id);
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Choosing a parent
|
|
46
|
+
|
|
47
|
+
`parent` is a `CreatePageParent`:
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
type CreatePageParent =
|
|
51
|
+
| { type: "page_id"; page_id: NotionPageId }
|
|
52
|
+
| { type: "data_source_id"; data_source_id: NotionDataSourceId }
|
|
53
|
+
| { type: "data_source_key"; key: string };
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
`type: "data_source_key"` is the recommended form inside a custom view. Pass the semantic key you declared in `custom_blocks.json` (e.g. `"tasks"`) and the SDK looks up the corresponding data source for you. The other two variants exist for the rarer case where you already have a raw Notion ID in hand.
|
|
57
|
+
|
|
58
|
+
### Property keys
|
|
59
|
+
|
|
60
|
+
`properties` is a `NotionPagePropertyInputMap`. Two niceties versus the raw API:
|
|
61
|
+
|
|
62
|
+
- **Keys** can be either raw Notion property IDs _or_ the data-source property keys you declared in the manifest. The SDK resolves keys → IDs before forwarding the request.
|
|
63
|
+
- **Values** (`NotionPagePropertyInputValue`) may omit `id` — the SDK fills in the final raw ID for you.
|
|
64
|
+
|
|
65
|
+
So if your manifest declares `title` and `dueDate`, you can write them by name (as in the example above) instead of looking up the raw IDs.
|
|
66
|
+
|
|
67
|
+
### Where the new page lands
|
|
68
|
+
|
|
69
|
+
`position` is a `NotionCreatePagePosition` and controls placement inside the parent:
|
|
70
|
+
|
|
71
|
+
- `{ type: "start" }` / `{ type: "end" }` — prepend or append (default).
|
|
72
|
+
- `{ type: "before", blockId }` / `{ type: "after", blockId }` — insert as a sibling of the given block (which can be nested anywhere under the parent).
|
|
73
|
+
|
|
74
|
+
## Reading pages
|
|
75
|
+
|
|
76
|
+
`pages.get(pageId)` fetches a single page by ID:
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
const result = await pages.get(pageId);
|
|
80
|
+
if (result.status === "error") return;
|
|
81
|
+
|
|
82
|
+
const page = result.page; // NotionPage
|
|
83
|
+
console.log(page.properties);
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
`page` mirrors Notion's public API shape, with `id`, `parent`, `properties`, optional `icon` / `cover`, etc. — see `NotionPage` in the types list below.
|
|
87
|
+
|
|
88
|
+
## Updating pages
|
|
89
|
+
|
|
90
|
+
`pages.update` writes back to a page. The optional fields (`properties`, `icon`, `cover`, `archived`) are independent — supply only what you want to change:
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
const result = await pages.update({
|
|
94
|
+
pageId,
|
|
95
|
+
properties: {
|
|
96
|
+
"%5C%3FX%3D": {
|
|
97
|
+
id: "%5C%3FX%3D",
|
|
98
|
+
type: "checkbox",
|
|
99
|
+
checkbox: true,
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
icon: { type: "emoji", emoji: "✅" },
|
|
103
|
+
});
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
The `properties` map (a `NotionPagePropertyWriteMap`) is keyed by **raw Notion property ID**, and each value must repeat that ID as its own `id` field — semantic data-source keys aren't accepted here. To update a row by configured custom-block key without writing out the raw IDs, use the `update` helper on the row returned from `useDataSource` instead; the SDK handles the key → ID resolution for you.
|
|
107
|
+
|
|
108
|
+
If you call `pages.update` with no fields to change, it short-circuits and resolves with `{ status: "error", error: "updatePage requires at least one of: properties, icon, cover, archived." }` — no request is sent.
|
|
109
|
+
|
|
110
|
+
## Deleting (archiving) pages
|
|
111
|
+
|
|
112
|
+
`pages.delete(pageId)` is a thin wrapper around `pages.update({ pageId, archived: true })`. Notion treats archive and trash the same way for pages:
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
await pages.delete(pageId);
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
To restore a page, call `pages.update({ pageId, archived: false })`.
|
|
119
|
+
|
|
120
|
+
## Icons, covers, and file uploads
|
|
121
|
+
|
|
122
|
+
File-upload references aren't enabled for custom blocks yet. For icons, covers, and file properties, use one of:
|
|
123
|
+
|
|
124
|
+
- An emoji icon — `{ type: "emoji", emoji: "🟢" }`
|
|
125
|
+
- An external URL — `{ type: "external", external: { url: "https://example.com/cover.png" } }`
|
|
126
|
+
- An existing hosted file URL returned by Notion — `{ type: "file", file: { url: existingUrl } }`
|
|
127
|
+
|
|
128
|
+
Do **not** send `{ type: "file_upload", file_upload: { id } }`; the host will reject it.
|
|
129
|
+
|
|
130
|
+
## Types
|
|
131
|
+
|
|
132
|
+
- `NotionPage` — the page record returned by every successful call.
|
|
133
|
+
- `NotionPageId` — branded string ID for a page.
|
|
134
|
+
- `NotionPageIcon` / `NotionPageCover` — icon and cover variants Notion supports.
|
|
135
|
+
- `NotionPageParent` — the page's parent reference as returned by the API.
|
|
136
|
+
- `NotionPagePropertyValue` — a single property value as returned on a `NotionPage`.
|
|
137
|
+
- `NotionPagePropertyInputValue` — a single property value as accepted by `pages.create` / `pages.update` (may omit `id` for `create`).
|
|
138
|
+
- `NotionPagePropertyInputMap` — input map for `pages.create` (keys can be raw property IDs or semantic keys).
|
|
139
|
+
- `NotionPagePropertyWriteMap` — input map for `pages.update` (raw property IDs only).
|
|
140
|
+
- `NotionCreatePagePosition` — `start` / `end` / `before` / `after` insertion variants.
|
|
141
|
+
- `CreatePageInput` / `CreatePageParent` / `CreatePageResult`.
|
|
142
|
+
- `GetPageResult`.
|
|
143
|
+
- `UpdatePageInput` / `UpdatePageResult`.
|
package/docs/users.md
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# `users` API
|
|
2
|
+
|
|
3
|
+
Helpers for reading workspace users from inside a custom block. The SDK forwards each call to Notion on your behalf.
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
import { users, type NotionUserId } from "ncblock"
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Every helper returns a discriminated result instead of throwing:
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
const result = await users.get(userId)
|
|
13
|
+
if (result.status === "success") {
|
|
14
|
+
// result.user is a NotionUser
|
|
15
|
+
} else {
|
|
16
|
+
// result.error is a human-readable string
|
|
17
|
+
}
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Always check `result.status` before reading `result.user` / `result.list`.
|
|
21
|
+
|
|
22
|
+
## Listing users
|
|
23
|
+
|
|
24
|
+
`users.list(input?)` returns workspace users visible to the current custom block, mirroring Notion's [`GET /v1/users`](https://developers.notion.com/reference/get-users) shape.
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
const result = await users.list({ pageSize: 50 })
|
|
28
|
+
if (result.status === "error") return
|
|
29
|
+
|
|
30
|
+
for (const user of result.list.results) {
|
|
31
|
+
console.log(user.id, user.name, user.person.email)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (result.list.has_more && result.list.next_cursor) {
|
|
35
|
+
const next = await users.list({ startCursor: result.list.next_cursor })
|
|
36
|
+
// ...
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
`ListUsersInput` accepts `pageSize` and `startCursor`; both are optional. `ListUsersResult` resolves to either `{ status: "success", list }` or `{ status: "error", error }`, where `list` is a `NotionUserList` (with `results`, `next_cursor`, `has_more`).
|
|
41
|
+
|
|
42
|
+
## Reading a single user
|
|
43
|
+
|
|
44
|
+
`users.get(userId)` fetches one `NotionUser` by `NotionUserId`:
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
const result = await users.get(userId)
|
|
48
|
+
if (result.status === "success") {
|
|
49
|
+
console.log(result.user.name)
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
`GetUserResult` follows the same success/error shape and returns `{ status: "success", user }` when the host resolves the user.
|
|
54
|
+
|
|
55
|
+
## Types
|
|
56
|
+
|
|
57
|
+
- `NotionUser` — the user record returned by `users.get` and inside `NotionUserList.results`.
|
|
58
|
+
- `NotionUserId` — branded string ID for a user.
|
|
59
|
+
- `NotionUserList` — paginated list shape returned by `users.list`.
|
|
60
|
+
- `ListUsersInput` / `ListUsersResult`.
|
|
61
|
+
- `GetUserResult`.
|
package/host.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host-side entrypoint for `ncblock/host`.
|
|
3
|
+
*
|
|
4
|
+
* Re-exports the bridge valibot schemas, data-source binding types, and the
|
|
5
|
+
* context parser so anything implementing the host side of the bridge — the
|
|
6
|
+
* dev shell today, anything else later — can validate inbound sandbox traffic
|
|
7
|
+
* and shape outbound traffic without reaching into private `bridge/*` paths.
|
|
8
|
+
*
|
|
9
|
+
* Nothing here is intended for use inside a sandboxed custom block; that
|
|
10
|
+
* surface lives in the default `ncblock` entrypoint.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export {
|
|
14
|
+
notionCustomBlockContextSchema,
|
|
15
|
+
parseNotionCustomBlockContext,
|
|
16
|
+
} from "./bridge/context"
|
|
17
|
+
export type {
|
|
18
|
+
NotionDataSourceBinding,
|
|
19
|
+
NotionDataSourceBindings,
|
|
20
|
+
} from "./bridge/dataSources/dataSource"
|
|
21
|
+
export {
|
|
22
|
+
notionDataSourceBindingSchema,
|
|
23
|
+
notionDataSourceBindingsSchema,
|
|
24
|
+
} from "./bridge/dataSources/dataSource"
|
|
25
|
+
export type { NotionDataSourcePageBridge } from "./bridge/dataSources/dataSourcePage"
|
|
26
|
+
export { notionDataSourcePageBridgeSchema } from "./bridge/dataSources/dataSourcePage"
|
|
27
|
+
export { readIncomingType } from "./bridge/incomingType"
|
|
28
|
+
export type { ContextChangedMessage } from "./bridge/messages/contextChanged"
|
|
29
|
+
export { contextChangedMessageSchema } from "./bridge/messages/contextChanged"
|
|
30
|
+
export type { CreatePageMessage } from "./bridge/messages/createPage"
|
|
31
|
+
export { createPageMessageSchema } from "./bridge/messages/createPage"
|
|
32
|
+
export type { CreatePageResultMessage } from "./bridge/messages/createPageResult"
|
|
33
|
+
export { createPageResultMessageSchema } from "./bridge/messages/createPageResult"
|
|
34
|
+
export type { DataSourcesChangedMessage } from "./bridge/messages/dataSourcesChanged"
|
|
35
|
+
export { dataSourcesChangedMessageSchema } from "./bridge/messages/dataSourcesChanged"
|
|
36
|
+
export type {
|
|
37
|
+
GetPageMessage,
|
|
38
|
+
GetPageResultMessage,
|
|
39
|
+
} from "./bridge/messages/getPage"
|
|
40
|
+
export {
|
|
41
|
+
getPageMessageSchema,
|
|
42
|
+
getPageResultMessageSchema,
|
|
43
|
+
} from "./bridge/messages/getPage"
|
|
44
|
+
export type { HostToSandboxMessage } from "./bridge/messages/hostToSandbox"
|
|
45
|
+
export { hostToSandboxMessageSchema } from "./bridge/messages/hostToSandbox"
|
|
46
|
+
export type { InitMessage } from "./bridge/messages/init"
|
|
47
|
+
export { initMessageSchema } from "./bridge/messages/init"
|
|
48
|
+
export type { InvalidHostMessage } from "./bridge/messages/invalidHostMessage"
|
|
49
|
+
export { invalidHostMessageSchema } from "./bridge/messages/invalidHostMessage"
|
|
50
|
+
export type { InvalidSandboxMessage } from "./bridge/messages/invalidSandboxMessage"
|
|
51
|
+
export { invalidSandboxMessageSchema } from "./bridge/messages/invalidSandboxMessage"
|
|
52
|
+
export type { QueryDataSourceMessage } from "./bridge/messages/queryDataSource"
|
|
53
|
+
export { queryDataSourceMessageSchema } from "./bridge/messages/queryDataSource"
|
|
54
|
+
export type { QueryDataSourceResultMessage } from "./bridge/messages/queryDataSourceResult"
|
|
55
|
+
export { queryDataSourceResultMessageSchema } from "./bridge/messages/queryDataSourceResult"
|
|
56
|
+
export type { ReadyMessage } from "./bridge/messages/ready"
|
|
57
|
+
export { readyMessageSchema } from "./bridge/messages/ready"
|
|
58
|
+
export type { ResizeMessage } from "./bridge/messages/resize"
|
|
59
|
+
export { resizeMessageSchema } from "./bridge/messages/resize"
|
|
60
|
+
export type { SandboxToHostMessage } from "./bridge/messages/sandboxToHost"
|
|
61
|
+
export { sandboxToHostMessageSchema } from "./bridge/messages/sandboxToHost"
|
|
62
|
+
export type { ThemeChangedMessage } from "./bridge/messages/themeChanged"
|
|
63
|
+
export { themeChangedMessageSchema } from "./bridge/messages/themeChanged"
|
|
64
|
+
export type { UpdatePageMessage } from "./bridge/messages/updatePage"
|
|
65
|
+
export { updatePageMessageSchema } from "./bridge/messages/updatePage"
|
|
66
|
+
export type { UpdatePageResultMessage } from "./bridge/messages/updatePageResult"
|
|
67
|
+
export { updatePageResultMessageSchema } from "./bridge/messages/updatePageResult"
|
package/index.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public entrypoint for `ncblock`.
|
|
3
|
+
*
|
|
4
|
+
* Everything re-exported here is part of the SDK's public API. Anything not
|
|
5
|
+
* re-exported is internal and may change without notice. Consumers should
|
|
6
|
+
* import exclusively from `ncblock`, never from deeper paths,
|
|
7
|
+
* so internal files (e.g. `./bridge/*`) can be refactored without breaking
|
|
8
|
+
* downstream code.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export type { NotionCustomBlockContext } from "./bridge/context"
|
|
12
|
+
export type {
|
|
13
|
+
NotionCollectionSchema,
|
|
14
|
+
NotionDataSource,
|
|
15
|
+
} from "./bridge/dataSources/dataSource"
|
|
16
|
+
export type {
|
|
17
|
+
NotionDataSourcePage,
|
|
18
|
+
NotionDataSourcePageUpdateInput,
|
|
19
|
+
NotionDataSourcePageUpdateResult,
|
|
20
|
+
} from "./bridge/dataSources/dataSourcePage"
|
|
21
|
+
export type { NotionDataSourceValue } from "./bridge/dataSources/dataSourceValue"
|
|
22
|
+
export type {
|
|
23
|
+
NotionDate,
|
|
24
|
+
NotionDateRange,
|
|
25
|
+
NotionDateReminder,
|
|
26
|
+
NotionDateTime,
|
|
27
|
+
NotionDateTimeRange,
|
|
28
|
+
NotionDateTimeReminder,
|
|
29
|
+
NotionDateValue,
|
|
30
|
+
NotionNoReminder,
|
|
31
|
+
NotionTimeReminder,
|
|
32
|
+
} from "./bridge/dataSources/dateValue"
|
|
33
|
+
export type {
|
|
34
|
+
NotionBuiltinPropertyId,
|
|
35
|
+
NotionDualProperty,
|
|
36
|
+
NotionPropertyColor,
|
|
37
|
+
NotionPropertyOption,
|
|
38
|
+
NotionPropertySchema,
|
|
39
|
+
NotionPropertyType,
|
|
40
|
+
NotionStatusGroup,
|
|
41
|
+
} from "./bridge/dataSources/propertySchema"
|
|
42
|
+
export {
|
|
43
|
+
NOTION_BUILTIN_PROPERTY_IDS,
|
|
44
|
+
NOTION_PROPERTY_TYPES,
|
|
45
|
+
} from "./bridge/dataSources/propertySchema"
|
|
46
|
+
export type { NotionRecordPointer } from "./bridge/dataSources/recordPointer"
|
|
47
|
+
export type { NotionDataSourceId, NotionSpaceId } from "./bridge/ids"
|
|
48
|
+
export type {
|
|
49
|
+
CustomBlockManifest,
|
|
50
|
+
ManifestDataSource,
|
|
51
|
+
ManifestIcon,
|
|
52
|
+
ManifestProperty,
|
|
53
|
+
} from "./bridge/manifest"
|
|
54
|
+
export type { NotionCreatePagePosition } from "./bridge/messages/createPage"
|
|
55
|
+
export type {
|
|
56
|
+
NotionPage,
|
|
57
|
+
NotionPageCover,
|
|
58
|
+
NotionPageIcon,
|
|
59
|
+
NotionPageId,
|
|
60
|
+
NotionPageParent,
|
|
61
|
+
NotionPagePropertyInputMap,
|
|
62
|
+
NotionPagePropertyInputValue,
|
|
63
|
+
NotionPagePropertyValue,
|
|
64
|
+
NotionPagePropertyWriteMap,
|
|
65
|
+
} from "./bridge/pages/page"
|
|
66
|
+
export { pages } from "./bridge/sandboxClient"
|
|
67
|
+
export type { NotionTheme } from "./bridge/theme"
|
|
68
|
+
export {
|
|
69
|
+
type CustomBlockInitial,
|
|
70
|
+
type InitCustomBlockOptions,
|
|
71
|
+
initCustomBlock,
|
|
72
|
+
NotInIframeError,
|
|
73
|
+
} from "./init"
|
|
74
|
+
export {
|
|
75
|
+
NotionCustomBlock,
|
|
76
|
+
type NotionCustomBlockProps,
|
|
77
|
+
type UseCustomBlockInitResult,
|
|
78
|
+
useCustomBlockAutoResize,
|
|
79
|
+
useCustomBlockContext,
|
|
80
|
+
useCustomBlockInit,
|
|
81
|
+
useDataSource,
|
|
82
|
+
useDataSourceDefinitions,
|
|
83
|
+
useTheme,
|
|
84
|
+
} from "./react"
|
|
85
|
+
export * from "./types"
|
|
86
|
+
export { users } from "./users"
|