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,25 @@
|
|
|
1
|
+
import * as v from "valibot"
|
|
2
|
+
import { manifestSchema } from "../manifest"
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* First message the sandbox sends after mount, kicking off the bridge handshake. The host replies
|
|
6
|
+
* with `init`.
|
|
7
|
+
*/
|
|
8
|
+
export const readyMessageSchema = v.object({
|
|
9
|
+
type: v.literal("ready"),
|
|
10
|
+
/**
|
|
11
|
+
* Used to ensure that the host and client are using the same version of the bridge protocol. A
|
|
12
|
+
* single host needs to support multiple custom blocks built with different versions of the bridge
|
|
13
|
+
* protocol. Increment this number any time a breaking change is made to the bridge protocol.
|
|
14
|
+
*/
|
|
15
|
+
bridgeProtocolVersion: v.optional(v.number()),
|
|
16
|
+
/**
|
|
17
|
+
* The data sources and settings the custom block expects. A sandbox sends `null` if it has no
|
|
18
|
+
* manifest-declared data requirements. `null` is used (rather than `undefined`) so the
|
|
19
|
+
* wire-level distinction between "explicitly no manifest" and "field omitted" survives
|
|
20
|
+
* `postMessage` serialization and runtime validation on the host.
|
|
21
|
+
*/
|
|
22
|
+
manifest: v.union([manifestSchema, v.null_()]),
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
export type ReadyMessage = v.InferOutput<typeof readyMessageSchema>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import * as v from "valibot"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Message sent by the sandbox whenever its measured content height changes, so the host can resize
|
|
5
|
+
* the surrounding iframe to match. The host is free to clamp the value or ignore it. The sandbox
|
|
6
|
+
* always reports the most recent measurement.
|
|
7
|
+
*/
|
|
8
|
+
export const resizeMessageSchema = v.object({
|
|
9
|
+
type: v.literal("resize"),
|
|
10
|
+
height: v.pipe(v.number(), v.finite(), v.minValue(0)),
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
export type ResizeMessage = v.InferOutput<typeof resizeMessageSchema>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import * as v from "valibot"
|
|
2
|
+
import { createPageMessageSchema } from "./createPage"
|
|
3
|
+
import { getPageMessageSchema } from "./getPage"
|
|
4
|
+
import { getUserMessageSchema } from "./getUser"
|
|
5
|
+
import { invalidHostMessageSchema } from "./invalidHostMessage"
|
|
6
|
+
import { listUsersMessageSchema } from "./listUsers"
|
|
7
|
+
import { queryDataSourceMessageSchema } from "./queryDataSource"
|
|
8
|
+
import { readyMessageSchema } from "./ready"
|
|
9
|
+
import { resizeMessageSchema } from "./resize"
|
|
10
|
+
import { updatePageMessageSchema } from "./updatePage"
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Discriminated union of every message the sandbox is allowed to send the host. Useful for hosts
|
|
14
|
+
* to validate inbound traffic from sandboxes without re-implementing validation logic.
|
|
15
|
+
*/
|
|
16
|
+
export const sandboxToHostMessageSchema = v.variant("type", [
|
|
17
|
+
readyMessageSchema,
|
|
18
|
+
queryDataSourceMessageSchema,
|
|
19
|
+
createPageMessageSchema,
|
|
20
|
+
getPageMessageSchema,
|
|
21
|
+
getUserMessageSchema,
|
|
22
|
+
listUsersMessageSchema,
|
|
23
|
+
updatePageMessageSchema,
|
|
24
|
+
resizeMessageSchema,
|
|
25
|
+
invalidHostMessageSchema,
|
|
26
|
+
])
|
|
27
|
+
|
|
28
|
+
export type SandboxToHostMessage = v.InferOutput<
|
|
29
|
+
typeof sandboxToHostMessageSchema
|
|
30
|
+
>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import * as v from "valibot"
|
|
2
|
+
import { notionThemeSchema } from "../theme"
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Message sent by the host every time the theme changes after the initial
|
|
6
|
+
* `init` handshake.
|
|
7
|
+
*/
|
|
8
|
+
export const themeChangedMessageSchema = v.object({
|
|
9
|
+
type: v.literal("themeChanged"),
|
|
10
|
+
theme: notionThemeSchema,
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
export type ThemeChangedMessage = v.InferOutput<
|
|
14
|
+
typeof themeChangedMessageSchema
|
|
15
|
+
>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import * as v from "valibot"
|
|
2
|
+
import {
|
|
3
|
+
notionPageCoverSchema,
|
|
4
|
+
notionPageIconSchema,
|
|
5
|
+
notionPagePropertyWriteMapSchema,
|
|
6
|
+
} from "../pages/page"
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Sandbox -> host: patch an existing page.
|
|
10
|
+
*/
|
|
11
|
+
export const updatePageMessageSchema = v.object({
|
|
12
|
+
type: v.literal("updatePage"),
|
|
13
|
+
requestId: v.string(),
|
|
14
|
+
pageId: v.string(),
|
|
15
|
+
properties: v.optional(notionPagePropertyWriteMapSchema),
|
|
16
|
+
icon: v.optional(notionPageIconSchema),
|
|
17
|
+
cover: v.optional(notionPageCoverSchema),
|
|
18
|
+
archived: v.optional(v.boolean()),
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
export type UpdatePageMessage = v.InferOutput<typeof updatePageMessageSchema>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import * as v from "valibot"
|
|
2
|
+
import { notionPageSchema } from "../pages/page"
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Host -> sandbox: result for a page update request.
|
|
6
|
+
*/
|
|
7
|
+
export const updatePageResultMessageSchema = v.variant("status", [
|
|
8
|
+
v.object({
|
|
9
|
+
type: v.literal("updatePageResult"),
|
|
10
|
+
requestId: v.string(),
|
|
11
|
+
status: v.literal("success"),
|
|
12
|
+
page: notionPageSchema,
|
|
13
|
+
}),
|
|
14
|
+
v.object({
|
|
15
|
+
type: v.literal("updatePageResult"),
|
|
16
|
+
requestId: v.string(),
|
|
17
|
+
status: v.literal("error"),
|
|
18
|
+
error: v.string(),
|
|
19
|
+
}),
|
|
20
|
+
])
|
|
21
|
+
|
|
22
|
+
export type UpdatePageResultMessage = v.InferOutput<
|
|
23
|
+
typeof updatePageResultMessageSchema
|
|
24
|
+
>
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Notion page shapes exchanged over the custom block bridge.
|
|
3
|
+
*
|
|
4
|
+
* These mirror a narrow subset of Notion's public `POST /v1/pages` API, since that's the bridge
|
|
5
|
+
* shape the host delivers for messages like `createPageResult`.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as v from "valibot"
|
|
9
|
+
import type { NotionDataSourceId } from "../ids"
|
|
10
|
+
|
|
11
|
+
declare const notionPageIdBrand: unique symbol
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Branded Notion page ID. Host payloads and SDK APIs use plain strings at runtime,
|
|
15
|
+
* but the brand keeps page IDs from being accidentally mixed with other IDs in TypeScript.
|
|
16
|
+
*/
|
|
17
|
+
export type NotionPageId = string & {
|
|
18
|
+
readonly [notionPageIdBrand]: "NotionPageId"
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Parent reference accepted by `pages.create` / `pages.update` inputs.
|
|
23
|
+
*
|
|
24
|
+
* Mirrors the parent shape Notion's public `POST /v1/pages` API accepts: `page_id` for a
|
|
25
|
+
* page-parented child, `data_source_id` for a database row. `data_source_id` is the internal
|
|
26
|
+
* collection ID.
|
|
27
|
+
*/
|
|
28
|
+
export const notionPageParentInputSchema = v.variant("type", [
|
|
29
|
+
v.object({ type: v.literal("page_id"), page_id: v.string() }),
|
|
30
|
+
v.object({
|
|
31
|
+
type: v.literal("data_source_id"),
|
|
32
|
+
data_source_id: v.string(),
|
|
33
|
+
}),
|
|
34
|
+
])
|
|
35
|
+
export type NotionPageParentInput =
|
|
36
|
+
| { type: "page_id"; page_id: NotionPageId }
|
|
37
|
+
| { type: "data_source_id"; data_source_id: NotionDataSourceId }
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Parent reference returned on a `Page` object from the host.
|
|
41
|
+
*
|
|
42
|
+
* Wider than the input shape: in addition to `page_id` / `data_source_id`, this includes
|
|
43
|
+
* `workspace` (for top-level/team-rooted pages) and `block_id` (for pages nested under a
|
|
44
|
+
* non-page block, e.g. a column or toggle). This mirrors the parent variants the public API
|
|
45
|
+
* surfaces on `Page` responses.
|
|
46
|
+
*/
|
|
47
|
+
export const notionPageParentSchema = v.variant("type", [
|
|
48
|
+
v.object({ type: v.literal("page_id"), page_id: v.string() }),
|
|
49
|
+
v.object({
|
|
50
|
+
type: v.literal("data_source_id"),
|
|
51
|
+
data_source_id: v.string(),
|
|
52
|
+
}),
|
|
53
|
+
v.object({ type: v.literal("workspace"), workspace: v.literal(true) }),
|
|
54
|
+
v.object({ type: v.literal("block_id"), block_id: v.string() }),
|
|
55
|
+
])
|
|
56
|
+
export type NotionPageParent =
|
|
57
|
+
| { type: "page_id"; page_id: NotionPageId }
|
|
58
|
+
| { type: "data_source_id"; data_source_id: NotionDataSourceId }
|
|
59
|
+
| { type: "workspace"; workspace: true }
|
|
60
|
+
| { type: "block_id"; block_id: string }
|
|
61
|
+
|
|
62
|
+
export const notionEmojiIconSchema = v.object({
|
|
63
|
+
type: v.literal("emoji"),
|
|
64
|
+
emoji: v.string(),
|
|
65
|
+
})
|
|
66
|
+
export const notionCustomEmojiIconSchema = v.object({
|
|
67
|
+
type: v.literal("custom_emoji"),
|
|
68
|
+
custom_emoji: v.object({
|
|
69
|
+
id: v.string(),
|
|
70
|
+
name: v.optional(v.string()),
|
|
71
|
+
url: v.optional(v.string()),
|
|
72
|
+
}),
|
|
73
|
+
})
|
|
74
|
+
export const notionExternalFileSchema = v.object({
|
|
75
|
+
type: v.literal("external"),
|
|
76
|
+
external: v.object({ url: v.string() }),
|
|
77
|
+
})
|
|
78
|
+
export const notionHostedFileSchema = v.object({
|
|
79
|
+
type: v.literal("file"),
|
|
80
|
+
file: v.object({
|
|
81
|
+
url: v.string(),
|
|
82
|
+
expiry_time: v.optional(v.string()),
|
|
83
|
+
}),
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
// TODO: Add file upload support.
|
|
87
|
+
export const notionPageIconSchema = v.variant("type", [
|
|
88
|
+
notionEmojiIconSchema,
|
|
89
|
+
notionCustomEmojiIconSchema,
|
|
90
|
+
notionExternalFileSchema,
|
|
91
|
+
notionHostedFileSchema,
|
|
92
|
+
])
|
|
93
|
+
export type NotionPageIcon = v.InferOutput<typeof notionPageIconSchema>
|
|
94
|
+
|
|
95
|
+
export const notionPageCoverSchema = v.variant("type", [
|
|
96
|
+
notionExternalFileSchema,
|
|
97
|
+
notionHostedFileSchema,
|
|
98
|
+
])
|
|
99
|
+
export type NotionPageCover = v.InferOutput<typeof notionPageCoverSchema>
|
|
100
|
+
|
|
101
|
+
const nullableStringSchema = v.nullable(v.string())
|
|
102
|
+
|
|
103
|
+
const notionRichTextItemSchema = v.record(v.string(), v.unknown())
|
|
104
|
+
export type NotionRichTextItem = v.InferOutput<typeof notionRichTextItemSchema>
|
|
105
|
+
|
|
106
|
+
const notionSelectOptionInputSchema = v.union([
|
|
107
|
+
v.object({
|
|
108
|
+
id: v.string(),
|
|
109
|
+
name: v.optional(v.string()),
|
|
110
|
+
color: v.optional(v.string()),
|
|
111
|
+
description: v.optional(nullableStringSchema),
|
|
112
|
+
}),
|
|
113
|
+
v.object({
|
|
114
|
+
name: v.string(),
|
|
115
|
+
id: v.optional(v.string()),
|
|
116
|
+
color: v.optional(v.string()),
|
|
117
|
+
description: v.optional(nullableStringSchema),
|
|
118
|
+
}),
|
|
119
|
+
])
|
|
120
|
+
export type NotionSelectOptionInput = v.InferOutput<
|
|
121
|
+
typeof notionSelectOptionInputSchema
|
|
122
|
+
>
|
|
123
|
+
|
|
124
|
+
const notionDateInputSchema = v.object({
|
|
125
|
+
start: v.string(),
|
|
126
|
+
end: v.optional(nullableStringSchema),
|
|
127
|
+
time_zone: v.optional(nullableStringSchema),
|
|
128
|
+
})
|
|
129
|
+
export type NotionDateInput = v.InferOutput<typeof notionDateInputSchema>
|
|
130
|
+
|
|
131
|
+
const notionUserInputSchema = v.union([
|
|
132
|
+
v.object({
|
|
133
|
+
object: v.optional(v.literal("user")),
|
|
134
|
+
id: v.string(),
|
|
135
|
+
}),
|
|
136
|
+
v.object({
|
|
137
|
+
object: v.literal("group"),
|
|
138
|
+
id: v.string(),
|
|
139
|
+
name: v.optional(nullableStringSchema),
|
|
140
|
+
}),
|
|
141
|
+
])
|
|
142
|
+
export type NotionUserInput = v.InferOutput<typeof notionUserInputSchema>
|
|
143
|
+
|
|
144
|
+
const notionRelationInputSchema = v.object({ id: v.string() })
|
|
145
|
+
export type NotionRelationInput = v.InferOutput<
|
|
146
|
+
typeof notionRelationInputSchema
|
|
147
|
+
>
|
|
148
|
+
|
|
149
|
+
const notionFileInputSchema = v.union([
|
|
150
|
+
v.object({
|
|
151
|
+
type: v.literal("external"),
|
|
152
|
+
name: v.string(),
|
|
153
|
+
external: v.object({ url: v.string() }),
|
|
154
|
+
}),
|
|
155
|
+
v.object({
|
|
156
|
+
type: v.literal("file"),
|
|
157
|
+
name: v.string(),
|
|
158
|
+
file: v.object({
|
|
159
|
+
url: v.string(),
|
|
160
|
+
expiry_time: v.optional(v.string()),
|
|
161
|
+
}),
|
|
162
|
+
}),
|
|
163
|
+
])
|
|
164
|
+
export type NotionFileInput = v.InferOutput<typeof notionFileInputSchema>
|
|
165
|
+
|
|
166
|
+
const notionPlaceInputSchema = v.object({
|
|
167
|
+
lat: v.number(),
|
|
168
|
+
lon: v.number(),
|
|
169
|
+
name: v.optional(nullableStringSchema),
|
|
170
|
+
address: v.optional(nullableStringSchema),
|
|
171
|
+
aws_place_id: v.optional(nullableStringSchema),
|
|
172
|
+
google_place_id: v.optional(nullableStringSchema),
|
|
173
|
+
})
|
|
174
|
+
export type NotionPlaceInput = v.InferOutput<typeof notionPlaceInputSchema>
|
|
175
|
+
|
|
176
|
+
const pagePropertyBaseSchema = {
|
|
177
|
+
id: v.string(),
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* A page property value.
|
|
182
|
+
*/
|
|
183
|
+
export const notionPagePropertyValueSchema = v.variant("type", [
|
|
184
|
+
v.object({
|
|
185
|
+
...pagePropertyBaseSchema,
|
|
186
|
+
type: v.literal("title"),
|
|
187
|
+
title: v.array(notionRichTextItemSchema),
|
|
188
|
+
}),
|
|
189
|
+
v.object({
|
|
190
|
+
...pagePropertyBaseSchema,
|
|
191
|
+
type: v.literal("rich_text"),
|
|
192
|
+
rich_text: v.array(notionRichTextItemSchema),
|
|
193
|
+
}),
|
|
194
|
+
v.object({
|
|
195
|
+
...pagePropertyBaseSchema,
|
|
196
|
+
type: v.literal("number"),
|
|
197
|
+
number: v.nullable(v.number()),
|
|
198
|
+
}),
|
|
199
|
+
v.object({
|
|
200
|
+
...pagePropertyBaseSchema,
|
|
201
|
+
type: v.literal("url"),
|
|
202
|
+
url: nullableStringSchema,
|
|
203
|
+
}),
|
|
204
|
+
v.object({
|
|
205
|
+
...pagePropertyBaseSchema,
|
|
206
|
+
type: v.literal("email"),
|
|
207
|
+
email: nullableStringSchema,
|
|
208
|
+
}),
|
|
209
|
+
v.object({
|
|
210
|
+
...pagePropertyBaseSchema,
|
|
211
|
+
type: v.literal("phone_number"),
|
|
212
|
+
phone_number: nullableStringSchema,
|
|
213
|
+
}),
|
|
214
|
+
v.object({
|
|
215
|
+
...pagePropertyBaseSchema,
|
|
216
|
+
type: v.literal("checkbox"),
|
|
217
|
+
checkbox: v.boolean(),
|
|
218
|
+
}),
|
|
219
|
+
v.object({
|
|
220
|
+
...pagePropertyBaseSchema,
|
|
221
|
+
type: v.literal("select"),
|
|
222
|
+
select: v.nullable(notionSelectOptionInputSchema),
|
|
223
|
+
}),
|
|
224
|
+
v.object({
|
|
225
|
+
...pagePropertyBaseSchema,
|
|
226
|
+
type: v.literal("status"),
|
|
227
|
+
status: v.nullable(notionSelectOptionInputSchema),
|
|
228
|
+
}),
|
|
229
|
+
v.object({
|
|
230
|
+
...pagePropertyBaseSchema,
|
|
231
|
+
type: v.literal("multi_select"),
|
|
232
|
+
multi_select: v.array(notionSelectOptionInputSchema),
|
|
233
|
+
}),
|
|
234
|
+
v.object({
|
|
235
|
+
...pagePropertyBaseSchema,
|
|
236
|
+
type: v.literal("date"),
|
|
237
|
+
date: v.nullable(notionDateInputSchema),
|
|
238
|
+
}),
|
|
239
|
+
v.object({
|
|
240
|
+
...pagePropertyBaseSchema,
|
|
241
|
+
type: v.literal("people"),
|
|
242
|
+
people: v.array(notionUserInputSchema),
|
|
243
|
+
}),
|
|
244
|
+
v.object({
|
|
245
|
+
...pagePropertyBaseSchema,
|
|
246
|
+
type: v.literal("relation"),
|
|
247
|
+
has_more: v.optional(v.boolean()),
|
|
248
|
+
relation: v.array(notionRelationInputSchema),
|
|
249
|
+
}),
|
|
250
|
+
v.object({
|
|
251
|
+
...pagePropertyBaseSchema,
|
|
252
|
+
type: v.literal("files"),
|
|
253
|
+
files: v.array(notionFileInputSchema),
|
|
254
|
+
}),
|
|
255
|
+
v.object({
|
|
256
|
+
...pagePropertyBaseSchema,
|
|
257
|
+
type: v.literal("place"),
|
|
258
|
+
place: v.nullable(notionPlaceInputSchema),
|
|
259
|
+
}),
|
|
260
|
+
])
|
|
261
|
+
export type NotionPagePropertyValue = v.InferOutput<
|
|
262
|
+
typeof notionPagePropertyValueSchema
|
|
263
|
+
>
|
|
264
|
+
|
|
265
|
+
type WithOptionalPropertyId<T> = T extends { id: string }
|
|
266
|
+
? Omit<T, "id"> & { id?: string }
|
|
267
|
+
: never
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Public SDK page property write input. Keys may be raw property IDs or data-source
|
|
271
|
+
* property keys. `id` is optional because the SDK resolves the final raw property ID
|
|
272
|
+
* from the map key before sending the bridge message.
|
|
273
|
+
*/
|
|
274
|
+
export type NotionPagePropertyInputValue =
|
|
275
|
+
WithOptionalPropertyId<NotionPagePropertyValue>
|
|
276
|
+
|
|
277
|
+
export type NotionPagePropertyInputMap = {
|
|
278
|
+
[propertyIdOrKey: string]: NotionPagePropertyInputValue
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export const notionPagePropertyWriteMapSchema = v.record(
|
|
282
|
+
v.string(),
|
|
283
|
+
notionPagePropertyValueSchema,
|
|
284
|
+
)
|
|
285
|
+
export type NotionPagePropertyWriteMap = v.InferOutput<
|
|
286
|
+
typeof notionPagePropertyWriteMapSchema
|
|
287
|
+
>
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* The `Page` object returned in `createPageResult` messages.
|
|
291
|
+
*/
|
|
292
|
+
export const notionPageSchema = v.object({
|
|
293
|
+
object: v.literal("page"),
|
|
294
|
+
id: v.string(),
|
|
295
|
+
parent: notionPageParentSchema,
|
|
296
|
+
properties: notionPagePropertyWriteMapSchema,
|
|
297
|
+
created_time: v.optional(v.string()),
|
|
298
|
+
last_edited_time: v.optional(v.string()),
|
|
299
|
+
icon: v.optional(notionPageIconSchema),
|
|
300
|
+
cover: v.optional(notionPageCoverSchema),
|
|
301
|
+
url: v.optional(v.string()),
|
|
302
|
+
public_url: v.optional(v.string()),
|
|
303
|
+
in_trash: v.optional(v.boolean()),
|
|
304
|
+
})
|
|
305
|
+
export type NotionPage = Omit<
|
|
306
|
+
v.InferOutput<typeof notionPageSchema>,
|
|
307
|
+
"id" | "parent" | "properties" | "icon" | "cover"
|
|
308
|
+
> & {
|
|
309
|
+
id: NotionPageId
|
|
310
|
+
parent: NotionPageParent
|
|
311
|
+
properties: NotionPagePropertyWriteMap
|
|
312
|
+
icon?: NotionPageIcon
|
|
313
|
+
cover?: NotionPageCover
|
|
314
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tracks one-shot request/response pairs over the `postMessage` bridge. Each `allocate` reserves a
|
|
3
|
+
* unique `requestId` and stores its resolver. `resolve` finds the pending resolver by `requestId`
|
|
4
|
+
* and calls it, then removes the entry so the same `requestId` cannot be resolved twice.
|
|
5
|
+
*/
|
|
6
|
+
export class PendingRequests<T> {
|
|
7
|
+
private nextId = 1
|
|
8
|
+
private readonly entries = new Map<string, (value: T) => void>()
|
|
9
|
+
|
|
10
|
+
constructor(private readonly prefix: string) {}
|
|
11
|
+
|
|
12
|
+
allocate(resolver: (value: T) => void): string {
|
|
13
|
+
const requestId = `${this.prefix}-${this.nextId}`
|
|
14
|
+
this.nextId += 1
|
|
15
|
+
this.entries.set(requestId, resolver)
|
|
16
|
+
return requestId
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
resolve(requestId: string, value: T): boolean {
|
|
20
|
+
const resolver = this.entries.get(requestId)
|
|
21
|
+
if (resolver === undefined) {
|
|
22
|
+
return false
|
|
23
|
+
}
|
|
24
|
+
this.entries.delete(requestId)
|
|
25
|
+
resolver(value)
|
|
26
|
+
return true
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CreatePageInput,
|
|
3
|
+
CreatePageResult,
|
|
4
|
+
GetPageResult,
|
|
5
|
+
GetUserResult,
|
|
6
|
+
ListUsersInput,
|
|
7
|
+
ListUsersResult,
|
|
8
|
+
NotionPageId,
|
|
9
|
+
NotionUserId,
|
|
10
|
+
UpdatePageInput,
|
|
11
|
+
UpdatePageResult,
|
|
12
|
+
} from "../types"
|
|
13
|
+
import {
|
|
14
|
+
type CustomBlockHostState,
|
|
15
|
+
type DataSourceQueryView,
|
|
16
|
+
getDataSourceQueryView as getDataSourceQueryViewWithBridge,
|
|
17
|
+
} from "./hostState"
|
|
18
|
+
import type { CustomBlockManifest } from "./manifest"
|
|
19
|
+
import type { InitMessage } from "./messages/init"
|
|
20
|
+
import { SandboxBridge } from "./SandboxBridge"
|
|
21
|
+
|
|
22
|
+
const bridge = new SandboxBridge()
|
|
23
|
+
|
|
24
|
+
export function sendCustomBlockReady(manifest: CustomBlockManifest | null) {
|
|
25
|
+
bridge.sendReady(manifest)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function awaitCustomBlockInit(
|
|
29
|
+
signal?: AbortSignal,
|
|
30
|
+
): Promise<InitMessage> {
|
|
31
|
+
return bridge.awaitInit(signal)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function subscribeToCustomBlockHost(listener: () => void) {
|
|
35
|
+
return bridge.subscribe(listener)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function getCustomBlockHostState(): CustomBlockHostState {
|
|
39
|
+
return bridge.getHostState()
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function queryCustomBlockDataSource(key: string, limit: number) {
|
|
43
|
+
bridge.queryDataSource(key, limit)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Apply an `init` payload to the bridge directly, bypassing the postMessage
|
|
48
|
+
* handshake. Used by the React provider when it needs to seed placeholder
|
|
49
|
+
* state (e.g. the standalone preview fallback when not embedded in Notion).
|
|
50
|
+
* The bridge applies the payload through the same code path as a real host.
|
|
51
|
+
*/
|
|
52
|
+
export function setMockCustomBlockState(message: InitMessage) {
|
|
53
|
+
bridge.setMockState(message)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function postCustomBlockResize(height: number) {
|
|
57
|
+
bridge.postResize(height)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function getUser(userId: NotionUserId): Promise<GetUserResult> {
|
|
61
|
+
return bridge.getUser(userId)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function listUsers(input?: ListUsersInput): Promise<ListUsersResult> {
|
|
65
|
+
return bridge.listUsers(input)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Resolves the host state for a given data-source key into the public {@link DataSourceQueryView},
|
|
70
|
+
* baking in the singleton bridge so per-row `update` calls flow through it.
|
|
71
|
+
*/
|
|
72
|
+
export function getDataSourceQueryView(
|
|
73
|
+
hostState: CustomBlockHostState,
|
|
74
|
+
key: string,
|
|
75
|
+
): DataSourceQueryView {
|
|
76
|
+
return getDataSourceQueryViewWithBridge(hostState, key, args =>
|
|
77
|
+
bridge.updateDataSourcePage(args),
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Page-related SDK APIs exposed under `sdk.pages.*`.
|
|
83
|
+
*/
|
|
84
|
+
export const pages = {
|
|
85
|
+
/**
|
|
86
|
+
* Creates a new Notion page.
|
|
87
|
+
*/
|
|
88
|
+
create(input: CreatePageInput): Promise<CreatePageResult> {
|
|
89
|
+
return bridge.createPage(input)
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Fetches a page by id.
|
|
94
|
+
*/
|
|
95
|
+
get(pageId: NotionPageId): Promise<GetPageResult> {
|
|
96
|
+
return bridge.getPage(pageId)
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Updates an existing page.
|
|
101
|
+
*/
|
|
102
|
+
update(input: UpdatePageInput): Promise<UpdatePageResult> {
|
|
103
|
+
return bridge.updatePage(input)
|
|
104
|
+
},
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Soft-delete a page by archiving it.
|
|
108
|
+
*/
|
|
109
|
+
delete(pageId: NotionPageId): Promise<UpdatePageResult> {
|
|
110
|
+
return bridge.updatePage({ pageId, archived: true })
|
|
111
|
+
},
|
|
112
|
+
}
|
package/bridge/theme.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import * as v from "valibot"
|
|
2
|
+
|
|
3
|
+
declare const notionUserIdBrand: unique symbol
|
|
4
|
+
|
|
5
|
+
export type NotionUserId = string & {
|
|
6
|
+
readonly [notionUserIdBrand]: "NotionUserId"
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const notionUserSchema = v.object({
|
|
10
|
+
object: v.literal("user"),
|
|
11
|
+
id: v.string(),
|
|
12
|
+
name: v.optional(v.string()),
|
|
13
|
+
avatar_url: v.nullable(v.string()),
|
|
14
|
+
type: v.literal("person"),
|
|
15
|
+
person: v.object({
|
|
16
|
+
email: v.string(),
|
|
17
|
+
}),
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
export type NotionUser = v.InferOutput<typeof notionUserSchema>
|
|
21
|
+
|
|
22
|
+
export const notionUserListSchema = v.object({
|
|
23
|
+
object: v.literal("list"),
|
|
24
|
+
results: v.array(notionUserSchema),
|
|
25
|
+
next_cursor: v.nullable(v.string()),
|
|
26
|
+
has_more: v.boolean(),
|
|
27
|
+
type: v.literal("user"),
|
|
28
|
+
user: v.record(v.string(), v.unknown()),
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
export type NotionUserList = v.InferOutput<typeof notionUserListSchema>
|
package/docs/context.md
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Context & theme
|
|
2
|
+
|
|
3
|
+
Every custom block runs inside a larger Notion document. These hooks expose that environment: the block itself, the containers it sits inside, and how the surrounding Notion app is currently presented (e.g. light vs. dark theme).
|
|
4
|
+
|
|
5
|
+
## API
|
|
6
|
+
|
|
7
|
+
### `useCustomBlockContext()`
|
|
8
|
+
|
|
9
|
+
Returns the block's location in the document tree:
|
|
10
|
+
|
|
11
|
+
- `customBlockId` — the block's own ID.
|
|
12
|
+
- `parent` — the immediate container (e.g., toggle, column, callout, the page itself).
|
|
13
|
+
- `page` — the closest enclosing `page` / `collection_view_page` ancestor.
|
|
14
|
+
|
|
15
|
+
Re-renders on `contextChanged` (e.g. the block moves containers).
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
function useCustomBlockContext(): NotionCustomBlockContext;
|
|
19
|
+
|
|
20
|
+
type NotionCustomBlockContext = {
|
|
21
|
+
customBlockId: string;
|
|
22
|
+
parent: { id: string; type: string };
|
|
23
|
+
page: { id: string; type: string };
|
|
24
|
+
};
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### `useTheme()`
|
|
28
|
+
|
|
29
|
+
Returns the host's current theme.
|
|
30
|
+
|
|
31
|
+
Re-renders on every `themeChanged` message.
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
function useTheme(): NotionTheme; // "light" | "dark"
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
```tsx
|
|
38
|
+
import { useCustomBlockContext, useTheme } from "ncblock";
|
|
39
|
+
|
|
40
|
+
export function Header() {
|
|
41
|
+
const { page } = useCustomBlockContext();
|
|
42
|
+
const theme = useTheme();
|
|
43
|
+
return <header data-theme={theme}>Page: {page.id}</header>;
|
|
44
|
+
}
|
|
45
|
+
```
|