ncblock 0.0.4 → 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.
Files changed (56) hide show
  1. package/HOST.md +68 -0
  2. package/bridge/SandboxBridge.ts +659 -0
  3. package/bridge/context.ts +58 -0
  4. package/bridge/dataSources/dataSource.ts +63 -0
  5. package/bridge/dataSources/dataSourcePage.ts +69 -0
  6. package/bridge/dataSources/dataSourceValue.ts +19 -0
  7. package/bridge/dataSources/dateValue.ts +96 -0
  8. package/bridge/dataSources/propertySchema.ts +186 -0
  9. package/bridge/dataSources/recordPointer.ts +13 -0
  10. package/bridge/dataSources/resolve.ts +96 -0
  11. package/bridge/dataSources/resolveProperty.ts +96 -0
  12. package/bridge/hostState.ts +146 -0
  13. package/bridge/ids.ts +30 -0
  14. package/bridge/incomingType.ts +19 -0
  15. package/bridge/loadManifest.ts +54 -0
  16. package/bridge/manifest.ts +53 -0
  17. package/bridge/messages/contextChanged.ts +15 -0
  18. package/bridge/messages/createPage.ts +64 -0
  19. package/bridge/messages/createPageResult.ts +25 -0
  20. package/bridge/messages/dataSourcesChanged.ts +18 -0
  21. package/bridge/messages/getPage.ts +32 -0
  22. package/bridge/messages/getUser.ts +32 -0
  23. package/bridge/messages/hostToSandbox.ts +33 -0
  24. package/bridge/messages/init.ts +20 -0
  25. package/bridge/messages/invalidHostMessage.ts +16 -0
  26. package/bridge/messages/invalidSandboxMessage.ts +18 -0
  27. package/bridge/messages/listUsers.ts +33 -0
  28. package/bridge/messages/queryDataSource.ts +16 -0
  29. package/bridge/messages/queryDataSourceResult.ts +18 -0
  30. package/bridge/messages/ready.ts +25 -0
  31. package/bridge/messages/resize.ts +13 -0
  32. package/bridge/messages/sandboxToHost.ts +30 -0
  33. package/bridge/messages/themeChanged.ts +15 -0
  34. package/bridge/messages/updatePage.ts +21 -0
  35. package/bridge/messages/updatePageResult.ts +24 -0
  36. package/bridge/pages/page.ts +314 -0
  37. package/bridge/pendingRequests.ts +28 -0
  38. package/bridge/sandboxClient.ts +112 -0
  39. package/bridge/theme.ts +5 -0
  40. package/bridge/users/user.ts +31 -0
  41. package/docs/context.md +45 -0
  42. package/docs/data-sources.md +161 -0
  43. package/docs/lifecycle.md +92 -0
  44. package/docs/manifest.md +42 -0
  45. package/docs/pages.md +143 -0
  46. package/docs/users.md +61 -0
  47. package/host.ts +67 -0
  48. package/index.ts +86 -0
  49. package/init.ts +92 -0
  50. package/package.json +15 -5
  51. package/react.tsx +418 -0
  52. package/types.ts +157 -0
  53. package/users.ts +26 -0
  54. package/utils.ts +13 -0
  55. package/vite-plugin/index.d.ts +46 -0
  56. package/vite-plugin/index.js +115 -0
package/init.ts ADDED
@@ -0,0 +1,92 @@
1
+ import type { NotionCustomBlockContext } from "./bridge/context"
2
+ import type { NotionDataSource } from "./bridge/dataSources/dataSource"
3
+ import { loadManifest } from "./bridge/loadManifest"
4
+ import {
5
+ awaitCustomBlockInit,
6
+ getCustomBlockHostState,
7
+ sendCustomBlockReady,
8
+ } from "./bridge/sandboxClient"
9
+ import type { NotionTheme } from "./bridge/theme"
10
+
11
+ /**
12
+ * The payload sent by the host in the `init` message in response to the sandbox's `ready` message.
13
+ */
14
+ export type CustomBlockInitial = {
15
+ theme: NotionTheme
16
+ context: NotionCustomBlockContext
17
+ dataSources: NotionDataSource[]
18
+ }
19
+
20
+ /**
21
+ * Error thrown when the SDK is loaded in a top-level standalone window with no parent frame.
22
+ * `postMessage` would just hit the same window and the handshake can never complete.
23
+ * `<NotionCustomBlock>` catches this specifically and falls back to a standalone preview with a
24
+ * warning banner. Direct callers can `instanceof` it to apply their own policy.
25
+ */
26
+ export class NotInIframeError extends Error {
27
+ constructor(message: string = NOT_IN_IFRAME_MESSAGE) {
28
+ super(message)
29
+ this.name = "NotInIframeError"
30
+ }
31
+ }
32
+
33
+ /**
34
+ * Options for {@link initCustomBlock}.
35
+ */
36
+ export type InitCustomBlockOptions = {
37
+ /**
38
+ * How long to wait for the host's `init` response before rejecting with an error.
39
+ *
40
+ * @default 2000 - Short enough that a misconfigured embed surfaces quickly, long enough to
41
+ * absorb a real host's worst-case init latency.
42
+ */
43
+ timeoutMs?: number
44
+ }
45
+
46
+ const DEFAULT_INIT_TIMEOUT_MS = 2000
47
+
48
+ const NOT_IN_IFRAME_MESSAGE =
49
+ "<NotionCustomBlock> only works inside an iframe — use the dev shell or deploy to Notion."
50
+
51
+ let initPromise: Promise<CustomBlockInitial> | undefined
52
+
53
+ /**
54
+ * Performs the SDK <-> host handshake: loads `custom_blocks.json`, posts
55
+ * `ready`, then awaits the host's `init` message. Resolves with that payload.
56
+ *
57
+ * Rejects with a `TimeoutError` if the host doesn't respond inside `timeoutMs`.
58
+ *
59
+ * Idempotent: subsequent calls return the same promise as the first and ignore any new options.
60
+ * Mount your React tree (or call any SDK hook / `subscribeToCustomBlockHost`) only after the
61
+ * returned promise resolves.
62
+ */
63
+ export function initCustomBlock(
64
+ opts: InitCustomBlockOptions = {},
65
+ ): Promise<CustomBlockInitial> {
66
+ initPromise ??= (async () => {
67
+ // Fail fast with a typed error when rendered as a standalone tab and not in a parent frame.
68
+ // Otherwise, it would eventually hit the timeout, since `postMessage` to `window.parent`
69
+ // would just hit the same window and never arrive.
70
+ if (typeof window !== "undefined" && window.parent === window) {
71
+ throw new NotInIframeError()
72
+ }
73
+
74
+ // Load the manifest and send it to the host.
75
+ const manifest = await loadManifest()
76
+
77
+ sendCustomBlockReady(manifest)
78
+
79
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_INIT_TIMEOUT_MS
80
+ const message = await awaitCustomBlockInit(AbortSignal.timeout(timeoutMs))
81
+
82
+ const hostState = getCustomBlockHostState()
83
+
84
+ return {
85
+ theme: message.theme,
86
+ context: message.context as NotionCustomBlockContext,
87
+ dataSources:
88
+ hostState.status === "initialized" ? hostState.dataSources : [],
89
+ }
90
+ })()
91
+ return initPromise
92
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ncblock",
3
- "version": "0.0.4",
3
+ "version": "0.0.5",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -15,16 +15,26 @@
15
15
  "types": "./dist/./host.d.ts"
16
16
  },
17
17
  "./vite": {
18
- "import": "./dist/./vite-plugin/index.js",
19
- "types": "./dist/./vite-plugin/index.d.d.ts"
18
+ "import": "./vite-plugin/index.js",
19
+ "types": "./vite-plugin/index.d.ts"
20
20
  }
21
21
  },
22
22
  "scripts": {
23
23
  "test": "vitest run --environment jsdom"
24
24
  },
25
25
  "files": [
26
- "dist",
27
- "README.md"
26
+ "index.ts",
27
+ "host.ts",
28
+ "HOST.md",
29
+ "docs",
30
+ "react.tsx",
31
+ "init.ts",
32
+ "users.ts",
33
+ "types.ts",
34
+ "utils.ts",
35
+ "vite-plugin",
36
+ "bridge",
37
+ "dist"
28
38
  ],
29
39
  "peerDependencies": {
30
40
  "react": "^19.2.5"
package/react.tsx ADDED
@@ -0,0 +1,418 @@
1
+ import {
2
+ type ReactNode,
3
+ useCallback,
4
+ useEffect,
5
+ useState,
6
+ useSyncExternalStore,
7
+ } from "react"
8
+ import type { NotionCustomBlockContext } from "./bridge/context"
9
+ import type { NotionDataSource } from "./bridge/dataSources/dataSource"
10
+ import type { CustomBlockHostState } from "./bridge/hostState"
11
+ import {
12
+ getCustomBlockHostState,
13
+ getDataSourceQueryView,
14
+ postCustomBlockResize,
15
+ queryCustomBlockDataSource,
16
+ setMockCustomBlockState,
17
+ subscribeToCustomBlockHost,
18
+ } from "./bridge/sandboxClient"
19
+ import type { NotionTheme } from "./bridge/theme"
20
+ import {
21
+ type CustomBlockInitial,
22
+ type InitCustomBlockOptions,
23
+ initCustomBlock,
24
+ NotInIframeError,
25
+ } from "./init"
26
+ import type { UseDataSourceResult } from "./types"
27
+
28
+ const DEFAULT_DATA_SOURCE_QUERY_LIMIT = 20
29
+
30
+ function useCustomBlockHost() {
31
+ return useSyncExternalStore(
32
+ subscribeToCustomBlockHost,
33
+ getCustomBlockHostState,
34
+ )
35
+ }
36
+
37
+ function assertInitialized(
38
+ host: CustomBlockHostState,
39
+ hookName: string,
40
+ ): asserts host is Extract<CustomBlockHostState, { status: "initialized" }> {
41
+ if (host.status !== "initialized") {
42
+ throw new Error(
43
+ `${hookName} called before \`initCustomBlock\` resolved. Await it before mounting your tree.`,
44
+ )
45
+ }
46
+ }
47
+
48
+ /**
49
+ * Discriminated state returned by {@link useCustomBlockInit}.
50
+ *
51
+ * Branch on `isLoaded`/`error`:
52
+ * - `{ isLoaded: false, error: undefined }` — handshake in progress.
53
+ * - `{ isLoaded: false, error: Error }` — handshake failed (most commonly a
54
+ * `TimeoutError` because the host never sent `init`).
55
+ * - `{ isLoaded: true, initial }` — handshake complete; safe to render
56
+ * children that call `useTheme`, `useCustomBlockContext`, etc.
57
+ */
58
+ export type UseCustomBlockInitResult =
59
+ | { isLoaded: false; error: undefined }
60
+ | { isLoaded: false; error: Error }
61
+ | { isLoaded: true; error: undefined; initial: CustomBlockInitial }
62
+
63
+ /**
64
+ * React wrapper around {@link initCustomBlock}. Kicks off the SDK ↔ host
65
+ * handshake on mount and returns a discriminated state object so the rest of
66
+ * the tree can render inside the `isLoaded === true` branch (where every
67
+ * other SDK hook is guaranteed to return a populated value).
68
+ *
69
+ * Idempotent — multiple components can call this; they share the same
70
+ * underlying handshake promise.
71
+ *
72
+ * @example
73
+ * function Root() {
74
+ * const init = useCustomBlockInit()
75
+ * if (init.error) return <p role="alert">Init failed: {init.error.message}</p>
76
+ * if (!init.isLoaded) return null
77
+ * return <App />
78
+ * }
79
+ */
80
+ export function useCustomBlockInit(
81
+ opts?: InitCustomBlockOptions,
82
+ ): UseCustomBlockInitResult {
83
+ const [state, setState] = useState<UseCustomBlockInitResult>({
84
+ isLoaded: false,
85
+ error: undefined,
86
+ })
87
+ useEffect(() => {
88
+ let cancelled = false
89
+ initCustomBlock(opts).then(
90
+ initial => {
91
+ if (!cancelled) {
92
+ setState({ isLoaded: true, error: undefined, initial })
93
+ }
94
+ },
95
+ err => {
96
+ if (!cancelled) {
97
+ setState({
98
+ isLoaded: false,
99
+ error: err instanceof Error ? err : new Error(String(err)),
100
+ })
101
+ }
102
+ },
103
+ )
104
+ return () => {
105
+ cancelled = true
106
+ }
107
+ // `initCustomBlock` caches its result, so options after the first call
108
+ // are ignored — re-running on opts changes would be misleading.
109
+ // eslint-disable-next-line react-hooks/exhaustive-deps
110
+ }, [])
111
+ return state
112
+ }
113
+
114
+ /**
115
+ * Props accepted by {@link NotionCustomBlock}.
116
+ */
117
+ export type NotionCustomBlockProps = InitCustomBlockOptions & {
118
+ children: ReactNode
119
+ /**
120
+ * Rendered while the SDK ↔ host handshake is in progress. Defaults to
121
+ * `null` (nothing).
122
+ */
123
+ fallback?: ReactNode
124
+ /**
125
+ * Rendered when the handshake fails. Either a node, or a function that
126
+ * receives the `Error`. When omitted, a small inline `<p role="alert">` is
127
+ * rendered with the error message — replace it for production templates.
128
+ */
129
+ errorFallback?: ReactNode | ((error: Error) => ReactNode)
130
+ /**
131
+ * Whether the provider should automatically post resize messages so the
132
+ * host iframe matches the content height of `#root`. Defaults to `true`.
133
+ * Pass `false` when you want to use the default block size and are ok
134
+ * with scrollbars within the Notion client.
135
+ *
136
+ * @default true
137
+ */
138
+ autoResize?: boolean
139
+ }
140
+
141
+ /**
142
+ * Top-level wrapper that runs the SDK ↔ host handshake and gates `children`
143
+ * until it resolves. Passes `timeoutMs` straight through to
144
+ * {@link initCustomBlock}.
145
+ *
146
+ * Templates that prefer not to write a `Root` gating component (or top-level
147
+ * `await`) can mount their app entirely inside this provider:
148
+ *
149
+ * ```tsx
150
+ * ReactDOM.createRoot(root).render(
151
+ * <NotionCustomBlock>
152
+ * <App />
153
+ * </NotionCustomBlock>,
154
+ * )
155
+ * ```
156
+ *
157
+ * Inside `children`, every SDK hook is guaranteed to return a populated
158
+ * value. Outside the provider (or during the loading window), they throw.
159
+ */
160
+ export function NotionCustomBlock({
161
+ children,
162
+ timeoutMs,
163
+ fallback = null,
164
+ errorFallback,
165
+ autoResize = true,
166
+ }: NotionCustomBlockProps) {
167
+ const init = useCustomBlockInit({ timeoutMs })
168
+ useCustomBlockAutoResize({ enabled: autoResize })
169
+ const isStandalone = init.error instanceof NotInIframeError
170
+ const host = useCustomBlockHost()
171
+
172
+ useEffect(() => {
173
+ if (!isStandalone) {
174
+ return
175
+ }
176
+ console.warn(`[notion-custom-sdk] ${init.error?.message}`)
177
+ setMockCustomBlockState({
178
+ type: "init",
179
+ theme: "light",
180
+ context: {
181
+ customBlockId: "",
182
+ parent: { id: "", type: "" },
183
+ page: { id: "" },
184
+ },
185
+ dataSources: { bindings: {} },
186
+ })
187
+ }, [isStandalone, init.error])
188
+
189
+ if (init.error && !isStandalone) {
190
+ if (errorFallback === undefined) {
191
+ return (
192
+ <p role="alert">Notion custom view init failed: {init.error.message}</p>
193
+ )
194
+ }
195
+ return (
196
+ <>
197
+ {typeof errorFallback === "function"
198
+ ? errorFallback(init.error)
199
+ : errorFallback}
200
+ </>
201
+ )
202
+ }
203
+ if (isStandalone) {
204
+ // Wait for the effect to seed placeholder host state; otherwise hooks
205
+ // in `children` would throw.
206
+ if (host.status !== "initialized") {
207
+ return <>{fallback}</>
208
+ }
209
+ return (
210
+ <>
211
+ <div role="status" style={STANDALONE_BANNER_STYLE}>
212
+ Notion host not detected — running in standalone preview. SDK hooks
213
+ return placeholder values until embedded in Notion.
214
+ </div>
215
+ {children}
216
+ </>
217
+ )
218
+ }
219
+ if (!init.isLoaded) {
220
+ return <>{fallback}</>
221
+ }
222
+ return <>{children}</>
223
+ }
224
+
225
+ const STANDALONE_BANNER_STYLE = {
226
+ padding: "8px 12px",
227
+ background: "#fff8e1",
228
+ color: "#5d4200",
229
+ borderBottom: "1px solid #f0d77b",
230
+ fontSize: 13,
231
+ fontFamily: "system-ui, sans-serif",
232
+ lineHeight: 1.4,
233
+ } as const
234
+
235
+ /**
236
+ * Returns the block's location in the document tree. Re-renders when the host sends
237
+ * `contextChanged` (e.g. the block moves into a different container or its enclosing
238
+ * page changes).
239
+ *
240
+ * Throws if called before `initCustomBlock` has resolved — `await` it before mounting.
241
+ *
242
+ * @example
243
+ * const ctx = useCustomBlockContext()
244
+ * console.log(ctx.customBlockId, ctx.parent.type)
245
+ */
246
+ export function useCustomBlockContext(): NotionCustomBlockContext {
247
+ const host = useCustomBlockHost()
248
+ assertInitialized(host, "useCustomBlockContext")
249
+ return host.context
250
+ }
251
+
252
+ /**
253
+ * Returns the host's current theme. Re-renders on every `themeChanged` message from the host.
254
+ *
255
+ * Throws if called before `initCustomBlock` has resolved — `await` it before mounting.
256
+ *
257
+ * @example
258
+ * const theme = useTheme()
259
+ */
260
+ export function useTheme(): NotionTheme {
261
+ const host = useCustomBlockHost()
262
+ assertInitialized(host, "useTheme")
263
+ return host.theme
264
+ }
265
+
266
+ /**
267
+ * Returns the raw data-source definitions — semantic keys plus optional `collectionPointer`,
268
+ * `collectionSchema`, `propertyIdsByKey`, and derived `propertySchemasById`. Most templates should use
269
+ * `useDataSource(key)` instead — this hook is for views that render the configuration
270
+ * itself (debug panels, schema-driven UIs).
271
+ *
272
+ * Throws if called before `initCustomBlock` has resolved.
273
+ */
274
+ export function useDataSourceDefinitions(): NotionDataSource[] {
275
+ const host = useCustomBlockHost()
276
+ assertInitialized(host, "useDataSourceDefinitions")
277
+ return host.dataSources
278
+ }
279
+
280
+ /**
281
+ * Reads from the data source mapped to the given semantic `key`.
282
+ *
283
+ * The first render kicks off a query for the first `initialLimit` items. Calling
284
+ * `fetchMore()` re-requests a larger prefix (the bridge does not expose cursors yet);
285
+ * page growth tracks `initialLimit`. The hook automatically resets to `initialLimit`
286
+ * when the underlying data source definition changes.
287
+ *
288
+ * @param key - The semantic data-source key the block is wired to (e.g. `"people"`).
289
+ * @param initialLimit - Page size for the first query, and the increment used by
290
+ * `fetchMore`. Defaults to 20.
291
+ *
292
+ * @example
293
+ * const { items, isLoading, hasMore, fetchMore, error } = useDataSource("default")
294
+ */
295
+ export function useDataSource(
296
+ key: string,
297
+ initialLimit: number = DEFAULT_DATA_SOURCE_QUERY_LIMIT,
298
+ ): UseDataSourceResult {
299
+ const host = useCustomBlockHost()
300
+ const [limit, setLimit] = useState(initialLimit)
301
+ const matchingDataSource =
302
+ host.status === "initialized"
303
+ ? host.dataSources.find(dataSource => dataSource.key === key)
304
+ : undefined
305
+ // Serialize to avoid re-querying when `dataSourcesChanged` rebuilds the array with equal entries.
306
+ const matchingSignature = matchingDataSource
307
+ ? JSON.stringify(matchingDataSource)
308
+ : null
309
+
310
+ useEffect(() => {
311
+ // A new key or source definition means we should restart from the initial page size.
312
+ setLimit(initialLimit)
313
+ }, [matchingSignature, key, initialLimit])
314
+
315
+ const isInitialized = host.status === "initialized"
316
+ useEffect(() => {
317
+ if (!isInitialized) {
318
+ return
319
+ }
320
+
321
+ queryCustomBlockDataSource(key, limit)
322
+ }, [matchingSignature, isInitialized, key, limit])
323
+
324
+ const view = getDataSourceQueryView(host, key)
325
+ const fetchMore = useCallback(() => {
326
+ if (!view.hasMore || view.isLoading) {
327
+ return
328
+ }
329
+
330
+ // The bridge does not expose cursors yet, so "fetch more" means re-requesting a
331
+ // larger prefix of items and letting the host return the expanded result set.
332
+ // Page growth tracks the caller-provided `initialLimit` so templates can opt
333
+ // into larger batches.
334
+ setLimit(currentLimit => currentLimit + initialLimit)
335
+ }, [view.hasMore, view.isLoading, initialLimit])
336
+
337
+ return {
338
+ items: view.items,
339
+ collectionSchema: view.collectionSchema,
340
+ propertySchemasById: view.propertySchemasById,
341
+ propertySchemasByKey: view.propertySchemasByKey,
342
+ isLoading: view.isLoading,
343
+ hasMore: view.hasMore,
344
+ fetchMore,
345
+ error: view.error,
346
+ }
347
+ }
348
+
349
+ /**
350
+ * Measures the sandbox's `#root` element and posts `resize` messages so the host iframe
351
+ * matches the block's content height. Uses `Math.ceil(scrollHeight)` and dedupes
352
+ * unchanged values.
353
+ *
354
+ * `<NotionCustomBlock>` calls this hook for you by default — only reach for it directly
355
+ * when you need to drive `enabled` yourself (e.g. behind a debug toggle). In that case,
356
+ * pass `autoResize={false}` to the provider to avoid running it twice. For full-bleed
357
+ * views that should fill their slot, pass `autoResize={false}` and skip the hook.
358
+ *
359
+ * @param args.enabled - When `false`, suspends measurement. Useful for conditional
360
+ * measurement (e.g. while a debug toggle is off). Defaults to `true`.
361
+ *
362
+ * @example
363
+ * <NotionCustomBlock autoResize={false}>
364
+ * <App />
365
+ * </NotionCustomBlock>
366
+ *
367
+ * function App() {
368
+ * const [enabled, setEnabled] = useState(true)
369
+ * useCustomBlockAutoResize({ enabled })
370
+ * return <div>…</div>
371
+ * }
372
+ */
373
+ export function useCustomBlockAutoResize(
374
+ args: {
375
+ /**
376
+ * Whether or not the hook is enabled. To disable this behavior, pass `false`. This is
377
+ * provided as an argument to allow for conditional disabling of the hook.
378
+ *
379
+ * @default true
380
+ */
381
+ enabled?: boolean
382
+ } = {},
383
+ ): void {
384
+ const { enabled = true } = args
385
+ useEffect(() => {
386
+ if (!enabled) {
387
+ return
388
+ }
389
+ if (typeof window === "undefined") {
390
+ return
391
+ }
392
+ const target = document.getElementById("root")
393
+ if (!(target instanceof HTMLElement)) {
394
+ return
395
+ }
396
+
397
+ let lastHeight = -1
398
+ const post = () => {
399
+ const next = Math.ceil(target.scrollHeight)
400
+ if (next === lastHeight) {
401
+ return
402
+ }
403
+ lastHeight = next
404
+ postCustomBlockResize(next)
405
+ }
406
+
407
+ post()
408
+
409
+ if (typeof ResizeObserver === "undefined") {
410
+ return
411
+ }
412
+ const observer = new ResizeObserver(post)
413
+ observer.observe(target)
414
+ return () => {
415
+ observer.disconnect()
416
+ }
417
+ }, [enabled])
418
+ }
package/types.ts ADDED
@@ -0,0 +1,157 @@
1
+ import type { NotionCollectionSchema } from "./bridge/dataSources/dataSource"
2
+ import type {
3
+ NotionDataSourcePage,
4
+ NotionDataSourcePageUpdateInput,
5
+ NotionDataSourcePageUpdateResult,
6
+ } from "./bridge/dataSources/dataSourcePage"
7
+ import type { NotionPropertySchema } from "./bridge/dataSources/propertySchema"
8
+ import type { NotionDataSourceId } from "./bridge/ids"
9
+ import type { NotionCreatePagePosition } from "./bridge/messages/createPage"
10
+ import type {
11
+ NotionPage,
12
+ NotionPageCover,
13
+ NotionPageIcon,
14
+ NotionPageId,
15
+ NotionPagePropertyInputMap,
16
+ NotionPagePropertyWriteMap,
17
+ } from "./bridge/pages/page"
18
+ import type { NotionUser, NotionUserList } from "./bridge/users/user"
19
+
20
+ export type { NotionDataSourceId, NotionSpaceId } from "./bridge/ids"
21
+ export type { NotionPageId } from "./bridge/pages/page"
22
+ export type {
23
+ NotionUser,
24
+ NotionUserId,
25
+ NotionUserList,
26
+ } from "./bridge/users/user"
27
+ export type {
28
+ NotionDataSourcePageUpdateInput,
29
+ NotionDataSourcePageUpdateResult,
30
+ }
31
+
32
+ /**
33
+ * Return shape of `useDataSource`.
34
+ *
35
+ * - `items` — the rows the host has returned so far. Empty until the first response arrives.
36
+ * - `isLoading` — `true` while a query is in flight.
37
+ * - `hasMore` — `true` if the host indicated more rows are available beyond the current page.
38
+ * - `fetchMore` — re-requests a larger prefix of items (no-op while loading or when
39
+ * `hasMore` is `false`). The page grows by `initialLimit` per call.
40
+ * - `error` — a human-readable error string if the most recent query failed.
41
+ */
42
+ export type UseDataSourceResult = {
43
+ items: NotionDataSourcePage[]
44
+ /**
45
+ * Collection/data-source schema for the bound Notion data source, including raw property
46
+ * schemas. Undefined when the semantic data-source key has not been bound.
47
+ */
48
+ collectionSchema?: NotionCollectionSchema
49
+ /**
50
+ * Per-property schemas keyed by raw property ID, including the four synthetic built-ins
51
+ * (`created_time`, `last_edited_time`, `created_by`, `last_edited_by`).
52
+ */
53
+ propertySchemasById: { [propertyId: string]: NotionPropertySchema }
54
+ /**
55
+ * Per-property schemas keyed by user-defined keys from the data source's `propertyIdsByKey`.
56
+ * Built-ins are NOT included here. Keys mapped to `undefined` indicate a declared-but-unbound
57
+ * slot.
58
+ */
59
+ propertySchemasByKey: { [key: string]: NotionPropertySchema | undefined }
60
+ isLoading: boolean
61
+ hasMore: boolean
62
+ fetchMore: () => void
63
+ error?: string
64
+ }
65
+
66
+ /**
67
+ * Parent reference accepted by `sdk.pages.create`. Mirrors Notion's public `POST /v1/pages`
68
+ * parent shape; see https://developers.notion.com/reference/data-source.
69
+ *
70
+ * - `page_id`: Create a child page under the given Notion page.
71
+ * - `data_source_id`: Create a row inside the given Notion data source (aka the internal
72
+ * collection). The public API's `data_source_id` is the same UUID as the `collectionPointer.id`
73
+ * exposed in `dataSources`, so either value works here.
74
+ * - `data_source_key`: Create a row inside the data source that the custom block was configured with
75
+ * under this semantic key. The SDK resolves the key to a `data_source_id` locally before
76
+ * sending the request to the host.
77
+ */
78
+ export type CreatePageParent =
79
+ | { type: "page_id"; page_id: NotionPageId }
80
+ | { type: "data_source_id"; data_source_id: NotionDataSourceId }
81
+ | { type: "data_source_key"; key: string }
82
+
83
+ /**
84
+ * Input accepted by `sdk.pages.create`. Mirrors the Notion public API `POST /v1/pages` API.
85
+ */
86
+ export type CreatePageInput = {
87
+ parent: CreatePageParent
88
+ properties: NotionPagePropertyInputMap
89
+ icon?: NotionPageIcon
90
+ cover?: NotionPageCover
91
+ position?: NotionCreatePagePosition
92
+ }
93
+
94
+ /**
95
+ * The result of a `sdk.pages.create` API call.
96
+ */
97
+ export type CreatePageResult =
98
+ | {
99
+ status: "success"
100
+ /** The newly created page. */
101
+ page: NotionPage
102
+ }
103
+ | { status: "error"; error: string }
104
+
105
+ /**
106
+ * Input accepted by `sdk.pages.update`.
107
+ *
108
+ * `properties` is keyed by raw Notion property ID. To update a row with configured
109
+ * custom-block property keys, use the `update` helper on pages returned from
110
+ * `useDataSource`.
111
+ */
112
+ export type UpdatePageInput = {
113
+ pageId: NotionPageId
114
+ properties?: NotionPagePropertyWriteMap
115
+ icon?: NotionPageIcon
116
+ cover?: NotionPageCover
117
+ archived?: boolean
118
+ }
119
+
120
+ /**
121
+ * Result of `sdk.pages.get`.
122
+ */
123
+ export type GetPageResult =
124
+ | {
125
+ status: "success"
126
+ page: NotionPage
127
+ }
128
+ | { status: "error"; error: string }
129
+
130
+ /**
131
+ * Result of `sdk.pages.update` / `sdk.pages.delete`.
132
+ */
133
+ export type UpdatePageResult =
134
+ | {
135
+ status: "success"
136
+ page: NotionPage
137
+ }
138
+ | { status: "error"; error: string }
139
+
140
+ export type ListUsersInput = {
141
+ startCursor?: string
142
+ pageSize?: number
143
+ }
144
+
145
+ export type ListUsersResult =
146
+ | {
147
+ status: "success"
148
+ list: NotionUserList
149
+ }
150
+ | { status: "error"; error: string }
151
+
152
+ export type GetUserResult =
153
+ | {
154
+ status: "success"
155
+ user: NotionUser
156
+ }
157
+ | { status: "error"; error: string }