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.
- package/HOST.md +68 -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,146 @@
|
|
|
1
|
+
import type { NotionCustomBlockContext } from "./context"
|
|
2
|
+
import type { NotionDataSource } from "./dataSources/dataSource"
|
|
3
|
+
import type {
|
|
4
|
+
NotionDataSourcePage,
|
|
5
|
+
NotionDataSourcePageBridge,
|
|
6
|
+
NotionDataSourcePageUpdateInput,
|
|
7
|
+
NotionDataSourcePageUpdateResult,
|
|
8
|
+
} from "./dataSources/dataSourcePage"
|
|
9
|
+
import type { NotionDataSourceValue } from "./dataSources/dataSourceValue"
|
|
10
|
+
import type { NotionPropertySchema } from "./dataSources/propertySchema"
|
|
11
|
+
import type { NotionPageId } from "./pages/page"
|
|
12
|
+
import type { NotionTheme } from "./theme"
|
|
13
|
+
|
|
14
|
+
export type CustomBlockHostState = UninitializedHostState | InitializedHostState
|
|
15
|
+
|
|
16
|
+
export type UninitializedHostState = {
|
|
17
|
+
status: "uninitialized"
|
|
18
|
+
theme: NotionTheme
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type InitializedHostState = {
|
|
22
|
+
status: "initialized"
|
|
23
|
+
theme: NotionTheme
|
|
24
|
+
context: NotionCustomBlockContext
|
|
25
|
+
dataSources: NotionDataSource[]
|
|
26
|
+
dataSourceState: Record<string, DataSourceQueryState>
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type DataSourceQueryState = {
|
|
30
|
+
/** Latest pages from the host as parsed from the bridge. `propertiesByKey` is derived lazily. */
|
|
31
|
+
items: NotionDataSourcePageBridge[]
|
|
32
|
+
isLoading: boolean
|
|
33
|
+
hasMore: boolean
|
|
34
|
+
error?: string
|
|
35
|
+
latestRequestId?: string
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function createEmptyDataSourceQueryState(): DataSourceQueryState {
|
|
39
|
+
return {
|
|
40
|
+
items: [],
|
|
41
|
+
isLoading: false,
|
|
42
|
+
hasMore: false,
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Resolved page + schema views for a single data source, keyed by raw property
|
|
48
|
+
* ID and by user-defined key. Re-derived from `propertyIdsByKey` on every read so
|
|
49
|
+
* renames don't require a re-query.
|
|
50
|
+
*/
|
|
51
|
+
export type DataSourceQueryView = {
|
|
52
|
+
items: NotionDataSourcePage[]
|
|
53
|
+
collectionSchema?: NotionDataSource["collectionSchema"]
|
|
54
|
+
propertySchemasById: { [propertyId: string]: NotionPropertySchema }
|
|
55
|
+
propertySchemasByKey: { [key: string]: NotionPropertySchema | undefined }
|
|
56
|
+
isLoading: boolean
|
|
57
|
+
hasMore: boolean
|
|
58
|
+
error?: string
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const EMPTY_QUERY_VIEW: DataSourceQueryView = {
|
|
62
|
+
items: [],
|
|
63
|
+
propertySchemasById: {},
|
|
64
|
+
propertySchemasByKey: {},
|
|
65
|
+
isLoading: false,
|
|
66
|
+
hasMore: false,
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Per-row callback that resolves property keys against the data source and forwards the
|
|
71
|
+
* resulting write to the bridge. Injected by callers (typically the SDK singleton in `init.ts`)
|
|
72
|
+
* so this module stays free of bridge-instance dependencies.
|
|
73
|
+
*/
|
|
74
|
+
export type UpdateDataSourcePageFn = (args: {
|
|
75
|
+
dataSource: NotionDataSource
|
|
76
|
+
pageId: NotionPageId
|
|
77
|
+
input: NotionDataSourcePageUpdateInput
|
|
78
|
+
}) => Promise<NotionDataSourcePageUpdateResult>
|
|
79
|
+
|
|
80
|
+
export function getDataSourceQueryView(
|
|
81
|
+
hostState: CustomBlockHostState,
|
|
82
|
+
key: string,
|
|
83
|
+
updateDataSourcePage: UpdateDataSourcePageFn,
|
|
84
|
+
): DataSourceQueryView {
|
|
85
|
+
if (hostState.status !== "initialized") {
|
|
86
|
+
return EMPTY_QUERY_VIEW
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const dataSource = hostState.dataSources.find(entry => entry.key === key)
|
|
90
|
+
const queryState =
|
|
91
|
+
hostState.dataSourceState[key] ?? createEmptyDataSourceQueryState()
|
|
92
|
+
|
|
93
|
+
if (dataSource === undefined) {
|
|
94
|
+
return {
|
|
95
|
+
items: [],
|
|
96
|
+
collectionSchema: undefined,
|
|
97
|
+
propertySchemasById: {},
|
|
98
|
+
propertySchemasByKey: {},
|
|
99
|
+
isLoading: queryState.isLoading,
|
|
100
|
+
hasMore: queryState.hasMore,
|
|
101
|
+
error: queryState.error,
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const propertyIdsByKey = dataSource.propertyIdsByKey
|
|
106
|
+
const propertySchemasById = dataSource.propertySchemasById
|
|
107
|
+
|
|
108
|
+
const resolvedItems: NotionDataSourcePage[] = queryState.items.map(entry => {
|
|
109
|
+
const propertiesByKey: {
|
|
110
|
+
[key: string]: NotionDataSourceValue | undefined
|
|
111
|
+
} = {}
|
|
112
|
+
for (const [key, propertyId] of Object.entries(propertyIdsByKey)) {
|
|
113
|
+
propertiesByKey[key] =
|
|
114
|
+
propertyId === undefined ? undefined : entry.propertiesById[propertyId]
|
|
115
|
+
}
|
|
116
|
+
return {
|
|
117
|
+
id: entry.id as NotionPageId,
|
|
118
|
+
propertiesById: entry.propertiesById,
|
|
119
|
+
propertiesByKey,
|
|
120
|
+
update: input =>
|
|
121
|
+
updateDataSourcePage({
|
|
122
|
+
dataSource,
|
|
123
|
+
pageId: entry.id as NotionPageId,
|
|
124
|
+
input,
|
|
125
|
+
}),
|
|
126
|
+
}
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
const propertySchemasByKey: {
|
|
130
|
+
[key: string]: NotionPropertySchema | undefined
|
|
131
|
+
} = {}
|
|
132
|
+
for (const [key, propertyId] of Object.entries(propertyIdsByKey)) {
|
|
133
|
+
propertySchemasByKey[key] =
|
|
134
|
+
propertyId === undefined ? undefined : propertySchemasById[propertyId]
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
items: resolvedItems,
|
|
139
|
+
collectionSchema: dataSource.collectionSchema,
|
|
140
|
+
propertySchemasById,
|
|
141
|
+
propertySchemasByKey,
|
|
142
|
+
isLoading: queryState.isLoading,
|
|
143
|
+
hasMore: queryState.hasMore,
|
|
144
|
+
error: queryState.error,
|
|
145
|
+
}
|
|
146
|
+
}
|
package/bridge/ids.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import * as v from "valibot"
|
|
2
|
+
|
|
3
|
+
declare const notionDataSourceIdBrand: unique symbol
|
|
4
|
+
declare const notionSpaceIdBrand: unique symbol
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Branded Notion data source ID. Host payloads and SDK APIs use plain strings at runtime,
|
|
8
|
+
* but the brand keeps data source IDs from being accidentally mixed with other IDs in TypeScript.
|
|
9
|
+
*/
|
|
10
|
+
export type NotionDataSourceId = string & {
|
|
11
|
+
readonly [notionDataSourceIdBrand]: "NotionDataSourceId"
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const notionDataSourceIdSchema = v.custom<NotionDataSourceId>(
|
|
15
|
+
value => typeof value === "string",
|
|
16
|
+
"Expected a Notion data source ID",
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Branded Notion workspace/space ID. Host payloads and SDK APIs use plain strings at runtime,
|
|
21
|
+
* but the brand keeps space IDs from being accidentally mixed with other IDs in TypeScript.
|
|
22
|
+
*/
|
|
23
|
+
export type NotionSpaceId = string & {
|
|
24
|
+
readonly [notionSpaceIdBrand]: "NotionSpaceId"
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const notionSpaceIdSchema = v.custom<NotionSpaceId>(
|
|
28
|
+
value => typeof value === "string",
|
|
29
|
+
"Expected a Notion space ID",
|
|
30
|
+
)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reads the `type` field off an inbound bridge message, if any.
|
|
3
|
+
*
|
|
4
|
+
* Used by both sides of the bridge after the canonical message schema rejects a payload. We still
|
|
5
|
+
* want to know what type the sender claimed to be sending so we can (a) avoid NACK loops on
|
|
6
|
+
* `invalidHostMessage` / `invalidSandboxMessage` and (b) include the type in the failure reason
|
|
7
|
+
* for debugging.
|
|
8
|
+
*/
|
|
9
|
+
export function readIncomingType(data: unknown): string | undefined {
|
|
10
|
+
if (
|
|
11
|
+
typeof data === "object" &&
|
|
12
|
+
data !== null &&
|
|
13
|
+
"type" in data &&
|
|
14
|
+
typeof data.type === "string"
|
|
15
|
+
) {
|
|
16
|
+
return data.type
|
|
17
|
+
}
|
|
18
|
+
return undefined
|
|
19
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import * as v from "valibot"
|
|
2
|
+
import { type CustomBlockManifest, manifestSchema } from "./manifest"
|
|
3
|
+
|
|
4
|
+
const MANIFEST_URL = "custom_blocks.json"
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Attempts to load a `custom_blocks.json` manifest co-located with the bundle. Treats
|
|
8
|
+
* any non-200 response as "no manifest". Warns on every failure path —
|
|
9
|
+
* templates are expected to ship a manifest, so a missing one is worth
|
|
10
|
+
* surfacing.
|
|
11
|
+
*/
|
|
12
|
+
export async function loadManifest(): Promise<CustomBlockManifest | null> {
|
|
13
|
+
if (typeof fetch !== "function") {
|
|
14
|
+
console.warn(
|
|
15
|
+
`[notion-custom-sdk] no \`fetch\` available; cannot load ${MANIFEST_URL}`,
|
|
16
|
+
)
|
|
17
|
+
return null
|
|
18
|
+
}
|
|
19
|
+
let response: Response
|
|
20
|
+
try {
|
|
21
|
+
response = await fetch(MANIFEST_URL, { credentials: "omit" })
|
|
22
|
+
} catch (error) {
|
|
23
|
+
console.warn(
|
|
24
|
+
`[notion-custom-sdk] no manifest fetched from ${MANIFEST_URL}`,
|
|
25
|
+
error,
|
|
26
|
+
)
|
|
27
|
+
return null
|
|
28
|
+
}
|
|
29
|
+
if (!response.ok) {
|
|
30
|
+
console.warn(
|
|
31
|
+
`[notion-custom-sdk] no manifest at ${MANIFEST_URL} (status ${response.status})`,
|
|
32
|
+
)
|
|
33
|
+
return null
|
|
34
|
+
}
|
|
35
|
+
let json: unknown
|
|
36
|
+
try {
|
|
37
|
+
json = await response.json()
|
|
38
|
+
} catch (error) {
|
|
39
|
+
console.warn(
|
|
40
|
+
`[notion-custom-sdk] manifest at ${MANIFEST_URL} was not valid JSON`,
|
|
41
|
+
error,
|
|
42
|
+
)
|
|
43
|
+
return null
|
|
44
|
+
}
|
|
45
|
+
const parsed = v.safeParse(manifestSchema, json)
|
|
46
|
+
if (!parsed.success) {
|
|
47
|
+
console.warn(
|
|
48
|
+
`[notion-custom-sdk] manifest at ${MANIFEST_URL} did not match schema`,
|
|
49
|
+
parsed.issues,
|
|
50
|
+
)
|
|
51
|
+
return null
|
|
52
|
+
}
|
|
53
|
+
return parsed.output
|
|
54
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import * as v from "valibot"
|
|
2
|
+
import { notionPropertyTypeSchema } from "./dataSources/propertySchema"
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* User-authored manifest declaring the data sources the custom block expects.
|
|
6
|
+
* Lives at `custom_blocks.json` in the project root and is forwarded to the host with
|
|
7
|
+
* the bridge `ready` message so the host can pre-bind data sources, surface
|
|
8
|
+
* configuration UI, etc. The `notionCustomBlock` Vite plugin from
|
|
9
|
+
* `ncblock/vite` wires the JSON file into the dev server and
|
|
10
|
+
* the build output.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Decorative icon attached to a manifest data source. Mirrors the
|
|
15
|
+
* `emoji` / `external` icon variants the public Notion API uses, so the host
|
|
16
|
+
* can render a recognizable affordance next to the slot in setup UI.
|
|
17
|
+
*/
|
|
18
|
+
export const manifestIconSchema = v.variant("type", [
|
|
19
|
+
v.object({
|
|
20
|
+
type: v.literal("emoji"),
|
|
21
|
+
emoji: v.string(),
|
|
22
|
+
}),
|
|
23
|
+
v.object({
|
|
24
|
+
type: v.literal("external"),
|
|
25
|
+
url: v.string(),
|
|
26
|
+
}),
|
|
27
|
+
])
|
|
28
|
+
|
|
29
|
+
export type ManifestIcon = v.InferOutput<typeof manifestIconSchema>
|
|
30
|
+
|
|
31
|
+
export const manifestPropertySchema = v.object({
|
|
32
|
+
name: v.string(),
|
|
33
|
+
description: v.optional(v.string()),
|
|
34
|
+
type: notionPropertyTypeSchema,
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
export type ManifestProperty = v.InferOutput<typeof manifestPropertySchema>
|
|
38
|
+
|
|
39
|
+
export const manifestDataSourceSchema = v.object({
|
|
40
|
+
name: v.string(),
|
|
41
|
+
description: v.optional(v.string()),
|
|
42
|
+
icon: v.optional(manifestIconSchema),
|
|
43
|
+
properties: v.optional(v.record(v.string(), manifestPropertySchema), {}),
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
export type ManifestDataSource = v.InferOutput<typeof manifestDataSourceSchema>
|
|
47
|
+
|
|
48
|
+
export const manifestSchema = v.object({
|
|
49
|
+
version: v.literal(1),
|
|
50
|
+
dataSources: v.record(v.string(), manifestDataSourceSchema),
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
export type CustomBlockManifest = v.InferOutput<typeof manifestSchema>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import * as v from "valibot"
|
|
2
|
+
import { notionCustomBlockContextSchema } from "../context"
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Message sent by the host whenever the custom block's position in the block tree changes (e.g. it
|
|
6
|
+
* is moved into a different parent block, or its enclosing page changes).
|
|
7
|
+
*/
|
|
8
|
+
export const contextChangedMessageSchema = v.object({
|
|
9
|
+
type: v.literal("contextChanged"),
|
|
10
|
+
context: notionCustomBlockContextSchema,
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
export type ContextChangedMessage = v.InferOutput<
|
|
14
|
+
typeof contextChangedMessageSchema
|
|
15
|
+
>
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import * as v from "valibot"
|
|
2
|
+
import type { NotionDataSourceId } from "../ids"
|
|
3
|
+
import type { NotionPageId } from "../pages/page"
|
|
4
|
+
import {
|
|
5
|
+
notionPageCoverSchema,
|
|
6
|
+
notionPageIconSchema,
|
|
7
|
+
notionPagePropertyWriteMapSchema,
|
|
8
|
+
} from "../pages/page"
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Describes where the newly created page should be inserted.
|
|
12
|
+
* - `start` / `end` (default): prepend / append to the request's `parent`
|
|
13
|
+
* page's children.
|
|
14
|
+
* - `before` / `after`: insert as a sibling of the anchor block identified by
|
|
15
|
+
* `blockId`, using the anchor's own structural parent. The anchor can be
|
|
16
|
+
* nested anywhere below the page (inside a toggle, column, callout, etc.).
|
|
17
|
+
*/
|
|
18
|
+
export const notionCreatePagePositionSchema = v.variant("type", [
|
|
19
|
+
v.object({ type: v.literal("start") }),
|
|
20
|
+
v.object({ type: v.literal("end") }),
|
|
21
|
+
v.object({ type: v.literal("before"), blockId: v.string() }),
|
|
22
|
+
v.object({ type: v.literal("after"), blockId: v.string() }),
|
|
23
|
+
])
|
|
24
|
+
|
|
25
|
+
export type NotionCreatePagePosition = v.InferOutput<
|
|
26
|
+
typeof notionCreatePagePositionSchema
|
|
27
|
+
>
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Parent accepted by the outbound `createPage` bridge message.
|
|
31
|
+
*
|
|
32
|
+
* Mirrors Notion's public API `POST /v1/pages` parent shape: `page_id` for a page-parented child,
|
|
33
|
+
* `data_source_id` for a database row. `data_source_id` is the internal collection ID.
|
|
34
|
+
*
|
|
35
|
+
* The SDK additionally accepts a higher-level `data_source_key` variant that resolves to
|
|
36
|
+
* `data_source_id` locally, using the `dataSources` mapping.
|
|
37
|
+
*/
|
|
38
|
+
export const createPageParentSchema = v.variant("type", [
|
|
39
|
+
v.object({ type: v.literal("page_id"), page_id: v.string() }),
|
|
40
|
+
v.object({
|
|
41
|
+
type: v.literal("data_source_id"),
|
|
42
|
+
data_source_id: v.string(),
|
|
43
|
+
}),
|
|
44
|
+
])
|
|
45
|
+
|
|
46
|
+
export type CreatePageMessageParent =
|
|
47
|
+
| { type: "page_id"; page_id: NotionPageId }
|
|
48
|
+
| { type: "data_source_id"; data_source_id: NotionDataSourceId }
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Message sent by the sandbox to ask the host to create a new page on its behalf.
|
|
52
|
+
* The payload mirrors Notion's public `POST /v1/pages` API.
|
|
53
|
+
*/
|
|
54
|
+
export const createPageMessageSchema = v.object({
|
|
55
|
+
type: v.literal("createPage"),
|
|
56
|
+
requestId: v.string(),
|
|
57
|
+
parent: createPageParentSchema,
|
|
58
|
+
properties: notionPagePropertyWriteMapSchema,
|
|
59
|
+
icon: v.optional(notionPageIconSchema),
|
|
60
|
+
cover: v.optional(notionPageCoverSchema),
|
|
61
|
+
position: v.optional(notionCreatePagePositionSchema),
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
export type CreatePageMessage = v.InferOutput<typeof createPageMessageSchema>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import * as v from "valibot"
|
|
2
|
+
import { notionPageSchema } from "../pages/page"
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Message sent by the host in response to a sandbox `createPage` request.
|
|
6
|
+
*/
|
|
7
|
+
export const createPageResultMessageSchema = v.variant("status", [
|
|
8
|
+
v.object({
|
|
9
|
+
type: v.literal("createPageResult"),
|
|
10
|
+
requestId: v.string(),
|
|
11
|
+
status: v.literal("success"),
|
|
12
|
+
/** The newly created page. */
|
|
13
|
+
page: notionPageSchema,
|
|
14
|
+
}),
|
|
15
|
+
v.object({
|
|
16
|
+
type: v.literal("createPageResult"),
|
|
17
|
+
requestId: v.string(),
|
|
18
|
+
status: v.literal("error"),
|
|
19
|
+
error: v.string(),
|
|
20
|
+
}),
|
|
21
|
+
])
|
|
22
|
+
|
|
23
|
+
export type CreatePageResultMessage = v.InferOutput<
|
|
24
|
+
typeof createPageResultMessageSchema
|
|
25
|
+
>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import * as v from "valibot"
|
|
2
|
+
import { notionDataSourceBindingsSchema } from "../dataSources/dataSource"
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Message sent by the host whenever the custom block's data-source mapping changes (e.g. a key is
|
|
6
|
+
* added, removed, or remapped). The SDK replaces its list of configured data sources but keeps any
|
|
7
|
+
* in-flight `useDataSource` query state for keys that still exist.
|
|
8
|
+
*/
|
|
9
|
+
export const dataSourcesChangedMessageSchema = v.object({
|
|
10
|
+
type: v.literal("dataSourcesChanged"),
|
|
11
|
+
dataSources: v.object({
|
|
12
|
+
bindings: notionDataSourceBindingsSchema,
|
|
13
|
+
}),
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
export type DataSourcesChangedMessage = v.InferOutput<
|
|
17
|
+
typeof dataSourcesChangedMessageSchema
|
|
18
|
+
>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import * as v from "valibot"
|
|
2
|
+
import { notionPageSchema } from "../pages/page"
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Sandbox -> host: fetch a page by ID.
|
|
6
|
+
*/
|
|
7
|
+
export const getPageMessageSchema = v.object({
|
|
8
|
+
type: v.literal("getPage"),
|
|
9
|
+
requestId: v.string(),
|
|
10
|
+
pageId: v.string(),
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
export type GetPageMessage = v.InferOutput<typeof getPageMessageSchema>
|
|
14
|
+
|
|
15
|
+
export const getPageResultMessageSchema = v.variant("status", [
|
|
16
|
+
v.object({
|
|
17
|
+
type: v.literal("getPageResult"),
|
|
18
|
+
requestId: v.string(),
|
|
19
|
+
status: v.literal("success"),
|
|
20
|
+
page: notionPageSchema,
|
|
21
|
+
}),
|
|
22
|
+
v.object({
|
|
23
|
+
type: v.literal("getPageResult"),
|
|
24
|
+
requestId: v.string(),
|
|
25
|
+
status: v.literal("error"),
|
|
26
|
+
error: v.string(),
|
|
27
|
+
}),
|
|
28
|
+
])
|
|
29
|
+
|
|
30
|
+
export type GetPageResultMessage = v.InferOutput<
|
|
31
|
+
typeof getPageResultMessageSchema
|
|
32
|
+
>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import * as v from "valibot"
|
|
2
|
+
import { notionUserSchema } from "../users/user"
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Sandbox -> host: fetch a user by ID.
|
|
6
|
+
*/
|
|
7
|
+
export const getUserMessageSchema = v.object({
|
|
8
|
+
type: v.literal("getUser"),
|
|
9
|
+
requestId: v.string(),
|
|
10
|
+
userId: v.string(),
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
export type GetUserMessage = v.InferOutput<typeof getUserMessageSchema>
|
|
14
|
+
|
|
15
|
+
export const getUserResultMessageSchema = v.variant("status", [
|
|
16
|
+
v.object({
|
|
17
|
+
type: v.literal("getUserResult"),
|
|
18
|
+
requestId: v.string(),
|
|
19
|
+
status: v.literal("success"),
|
|
20
|
+
user: notionUserSchema,
|
|
21
|
+
}),
|
|
22
|
+
v.object({
|
|
23
|
+
type: v.literal("getUserResult"),
|
|
24
|
+
requestId: v.string(),
|
|
25
|
+
status: v.literal("error"),
|
|
26
|
+
error: v.string(),
|
|
27
|
+
}),
|
|
28
|
+
])
|
|
29
|
+
|
|
30
|
+
export type GetUserResultMessage = v.InferOutput<
|
|
31
|
+
typeof getUserResultMessageSchema
|
|
32
|
+
>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import * as v from "valibot"
|
|
2
|
+
import { contextChangedMessageSchema } from "./contextChanged"
|
|
3
|
+
import { createPageResultMessageSchema } from "./createPageResult"
|
|
4
|
+
import { dataSourcesChangedMessageSchema } from "./dataSourcesChanged"
|
|
5
|
+
import { getPageResultMessageSchema } from "./getPage"
|
|
6
|
+
import { getUserResultMessageSchema } from "./getUser"
|
|
7
|
+
import { initMessageSchema } from "./init"
|
|
8
|
+
import { invalidSandboxMessageSchema } from "./invalidSandboxMessage"
|
|
9
|
+
import { listUsersResultMessageSchema } from "./listUsers"
|
|
10
|
+
import { queryDataSourceResultMessageSchema } from "./queryDataSourceResult"
|
|
11
|
+
import { themeChangedMessageSchema } from "./themeChanged"
|
|
12
|
+
import { updatePageResultMessageSchema } from "./updatePageResult"
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Discriminated union of every message the host is allowed to send the sandbox.
|
|
16
|
+
*/
|
|
17
|
+
export const hostToSandboxMessageSchema = v.variant("type", [
|
|
18
|
+
initMessageSchema,
|
|
19
|
+
themeChangedMessageSchema,
|
|
20
|
+
contextChangedMessageSchema,
|
|
21
|
+
dataSourcesChangedMessageSchema,
|
|
22
|
+
queryDataSourceResultMessageSchema,
|
|
23
|
+
createPageResultMessageSchema,
|
|
24
|
+
getPageResultMessageSchema,
|
|
25
|
+
getUserResultMessageSchema,
|
|
26
|
+
listUsersResultMessageSchema,
|
|
27
|
+
updatePageResultMessageSchema,
|
|
28
|
+
invalidSandboxMessageSchema,
|
|
29
|
+
])
|
|
30
|
+
|
|
31
|
+
export type HostToSandboxMessage = v.InferOutput<
|
|
32
|
+
typeof hostToSandboxMessageSchema
|
|
33
|
+
>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import * as v from "valibot"
|
|
2
|
+
import { notionCustomBlockContextSchema } from "../context"
|
|
3
|
+
import { notionDataSourceBindingsSchema } from "../dataSources/dataSource"
|
|
4
|
+
import { notionThemeSchema } from "../theme"
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Initialization message sent by the host to the sandbox exactly once, in response to the
|
|
8
|
+
* sandbox's `ready` message. After init, live updates flow through narrower messages
|
|
9
|
+
* (`themeChanged`, `contextChanged`, `dataSourcesChanged`).
|
|
10
|
+
*/
|
|
11
|
+
export const initMessageSchema = v.object({
|
|
12
|
+
type: v.literal("init"),
|
|
13
|
+
theme: notionThemeSchema,
|
|
14
|
+
context: notionCustomBlockContextSchema,
|
|
15
|
+
dataSources: v.object({
|
|
16
|
+
bindings: notionDataSourceBindingsSchema,
|
|
17
|
+
}),
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
export type InitMessage = v.InferOutput<typeof initMessageSchema>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import * as v from "valibot"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Sent by the sandbox back to the host when an inbound host-to-sandbox message could not be parsed
|
|
5
|
+
* (unknown `type`, schema mismatch, etc.). The host should never reply to an `invalidHostMessage`
|
|
6
|
+
* with another bridge error message.
|
|
7
|
+
*/
|
|
8
|
+
export const invalidHostMessageSchema = v.object({
|
|
9
|
+
type: v.literal("invalidHostMessage"),
|
|
10
|
+
/**
|
|
11
|
+
* A human-readable string intended for sandbox-side logging. It has no structured contract.
|
|
12
|
+
*/
|
|
13
|
+
reason: v.string(),
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
export type InvalidHostMessage = v.InferOutput<typeof invalidHostMessageSchema>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import * as v from "valibot"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Sent by the host back to the sandbox when an inbound sandbox-to-host message could not be parsed
|
|
5
|
+
* (unknown `type`, schema mismatch, etc.). The sandbox should never reply to an
|
|
6
|
+
* `invalidSandboxMessage` with another bridge error message.
|
|
7
|
+
*/
|
|
8
|
+
export const invalidSandboxMessageSchema = v.object({
|
|
9
|
+
type: v.literal("invalidSandboxMessage"),
|
|
10
|
+
/**
|
|
11
|
+
* A human-readable string intended for sandbox-side logging. It has no structured contract.
|
|
12
|
+
*/
|
|
13
|
+
reason: v.string(),
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
export type InvalidSandboxMessage = v.InferOutput<
|
|
17
|
+
typeof invalidSandboxMessageSchema
|
|
18
|
+
>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import * as v from "valibot"
|
|
2
|
+
import { notionUserListSchema } from "../users/user"
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Sandbox -> host: list users visible in the custom block's workspace.
|
|
6
|
+
*/
|
|
7
|
+
export const listUsersMessageSchema = v.object({
|
|
8
|
+
type: v.literal("listUsers"),
|
|
9
|
+
requestId: v.string(),
|
|
10
|
+
startCursor: v.optional(v.string()),
|
|
11
|
+
pageSize: v.optional(v.number()),
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
export type ListUsersMessage = v.InferOutput<typeof listUsersMessageSchema>
|
|
15
|
+
|
|
16
|
+
export const listUsersResultMessageSchema = v.variant("status", [
|
|
17
|
+
v.object({
|
|
18
|
+
type: v.literal("listUsersResult"),
|
|
19
|
+
requestId: v.string(),
|
|
20
|
+
status: v.literal("success"),
|
|
21
|
+
list: notionUserListSchema,
|
|
22
|
+
}),
|
|
23
|
+
v.object({
|
|
24
|
+
type: v.literal("listUsersResult"),
|
|
25
|
+
requestId: v.string(),
|
|
26
|
+
status: v.literal("error"),
|
|
27
|
+
error: v.string(),
|
|
28
|
+
}),
|
|
29
|
+
])
|
|
30
|
+
|
|
31
|
+
export type ListUsersResultMessage = v.InferOutput<
|
|
32
|
+
typeof listUsersResultMessageSchema
|
|
33
|
+
>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import * as v from "valibot"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Message sent by the sandbox to ask the host to load (or reload) a data
|
|
5
|
+
* source with a given page size.
|
|
6
|
+
*/
|
|
7
|
+
export const queryDataSourceMessageSchema = v.object({
|
|
8
|
+
type: v.literal("queryDataSource"),
|
|
9
|
+
requestId: v.string(),
|
|
10
|
+
key: v.string(),
|
|
11
|
+
limit: v.number(),
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
export type QueryDataSourceMessage = v.InferOutput<
|
|
15
|
+
typeof queryDataSourceMessageSchema
|
|
16
|
+
>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import * as v from "valibot"
|
|
2
|
+
import { notionDataSourcePageBridgeSchema } from "../dataSources/dataSourcePage"
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Message sent by the host to the sandbox in response to a `queryDataSource` request.
|
|
6
|
+
*/
|
|
7
|
+
export const queryDataSourceResultMessageSchema = v.object({
|
|
8
|
+
type: v.literal("queryDataSourceResult"),
|
|
9
|
+
requestId: v.string(),
|
|
10
|
+
key: v.string(),
|
|
11
|
+
items: v.array(notionDataSourcePageBridgeSchema),
|
|
12
|
+
hasMore: v.boolean(),
|
|
13
|
+
error: v.optional(v.string()),
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
export type QueryDataSourceResultMessage = v.InferOutput<
|
|
17
|
+
typeof queryDataSourceResultMessageSchema
|
|
18
|
+
>
|