ncblock 0.0.2 → 0.0.3
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/README.md +12 -242
- package/dist/bridge/SandboxBridge.d.ts +64 -0
- package/dist/bridge/SandboxBridge.d.ts.map +1 -0
- package/dist/{notion.js → bridge/SandboxBridge.js} +80 -337
- package/dist/bridge/dataSources/resolve.d.ts +22 -0
- package/dist/bridge/dataSources/resolve.d.ts.map +1 -0
- package/dist/bridge/dataSources/resolve.js +65 -0
- package/dist/bridge/dataSources/resolveProperty.d.ts +36 -0
- package/dist/bridge/dataSources/resolveProperty.d.ts.map +1 -0
- package/dist/bridge/dataSources/resolveProperty.js +67 -0
- package/dist/bridge/hostState.d.ts +57 -0
- package/dist/bridge/hostState.d.ts.map +1 -0
- package/dist/bridge/hostState.js +65 -0
- package/dist/bridge/loadManifest.d.ts +9 -0
- package/dist/bridge/loadManifest.d.ts.map +1 -0
- package/dist/bridge/loadManifest.js +41 -0
- package/dist/bridge/messages/getUser.d.ts +32 -0
- package/dist/bridge/messages/getUser.d.ts.map +1 -0
- package/dist/bridge/messages/getUser.js +24 -0
- package/dist/bridge/messages/hostToSandbox.d.ts +45 -0
- package/dist/bridge/messages/hostToSandbox.d.ts.map +1 -1
- package/dist/bridge/messages/hostToSandbox.js +4 -0
- package/dist/bridge/messages/listUsers.d.ts +40 -0
- package/dist/bridge/messages/listUsers.d.ts.map +1 -0
- package/dist/bridge/messages/listUsers.js +25 -0
- package/dist/bridge/messages/sandboxToHost.d.ts +9 -0
- package/dist/bridge/messages/sandboxToHost.d.ts.map +1 -1
- package/dist/bridge/messages/sandboxToHost.js +4 -0
- package/dist/bridge/sandboxClient.d.ts +46 -0
- package/dist/bridge/sandboxClient.d.ts.map +1 -0
- package/dist/bridge/sandboxClient.js +72 -0
- package/dist/bridge/users/user.d.ts +36 -0
- package/dist/bridge/users/user.d.ts.map +1 -0
- package/dist/bridge/users/user.js +19 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/init.d.ts +44 -0
- package/dist/init.d.ts.map +1 -0
- package/dist/init.js +49 -0
- package/dist/react.d.ts +1 -1
- package/dist/react.d.ts.map +1 -1
- package/dist/react.js +2 -1
- package/dist/types.d.ts +20 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/users.d.ts +15 -0
- package/dist/users.d.ts.map +1 -0
- package/dist/users.js +18 -0
- package/package.json +1 -1
- package/dist/notion.d.ts +0 -113
- package/dist/notion.d.ts.map +0 -1
- package/dist/pages.d.ts +0 -23
- package/dist/pages.d.ts.map +0 -1
- package/dist/pages.js +0 -30
|
@@ -1,86 +1,18 @@
|
|
|
1
1
|
import * as v from "valibot";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
2
|
+
import { unreachable } from "../utils";
|
|
3
|
+
import { resolveDataSources } from "./dataSources/resolve";
|
|
4
|
+
import { resolvePropertyWriteMapForDataSource } from "./dataSources/resolveProperty";
|
|
5
|
+
import { createEmptyDataSourceQueryState, } from "./hostState";
|
|
6
|
+
import { readIncomingType } from "./incomingType";
|
|
7
|
+
import { hostToSandboxMessageSchema } from "./messages/hostToSandbox";
|
|
8
|
+
import { PendingRequests } from "./pendingRequests";
|
|
8
9
|
/**
|
|
9
10
|
* Used to ensure that the host and client are using the same version of the bridge protocol. A
|
|
10
11
|
* single host needs to support multiple custom blocks built with different versions of the bridge
|
|
11
12
|
* protocol. Increment this number any time a breaking change is made to the bridge protocol.
|
|
12
13
|
*/
|
|
13
|
-
const CUSTOM_BLOCK_BRIDGE_PROTOCOL_VERSION = 1;
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Resolves a public SDK property write map into the ID-keyed bridge shape.
|
|
17
|
-
*/
|
|
18
|
-
function resolvePropertyWriteMapForDataSource(args) {
|
|
19
|
-
const { dataSource, properties, operationName } = args;
|
|
20
|
-
const resolvedProperties = {};
|
|
21
|
-
for (const [identifier, value] of Object.entries(properties)) {
|
|
22
|
-
const propertyIdResult = resolvePropertyIdentifierForDataSource({
|
|
23
|
-
dataSource,
|
|
24
|
-
identifier,
|
|
25
|
-
operationName,
|
|
26
|
-
});
|
|
27
|
-
if (propertyIdResult.status === "error") {
|
|
28
|
-
return propertyIdResult;
|
|
29
|
-
}
|
|
30
|
-
const propertyId = propertyIdResult.propertyId;
|
|
31
|
-
if (value.id !== undefined &&
|
|
32
|
-
value.id !== identifier &&
|
|
33
|
-
value.id !== propertyId) {
|
|
34
|
-
return {
|
|
35
|
-
status: "error",
|
|
36
|
-
error: `Property ${identifier} resolved to ${propertyId} but value id was ${value.id}.`,
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
if (resolvedProperties[propertyId] !== undefined) {
|
|
40
|
-
return {
|
|
41
|
-
status: "error",
|
|
42
|
-
error: `Cannot set property ${propertyId} more than once.`,
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
const parsedValue = v.safeParse(notionPagePropertyValueSchema, {
|
|
46
|
-
...value,
|
|
47
|
-
id: propertyId,
|
|
48
|
-
});
|
|
49
|
-
if (!parsedValue.success) {
|
|
50
|
-
return {
|
|
51
|
-
status: "error",
|
|
52
|
-
error: `Invalid value for property ${identifier}.`,
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
resolvedProperties[propertyId] = parsedValue.output;
|
|
56
|
-
}
|
|
57
|
-
return { status: "success", properties: resolvedProperties };
|
|
58
|
-
}
|
|
59
|
-
/**
|
|
60
|
-
* Treats matching data-source keys as aliases for property IDs; all other identifiers are raw IDs.
|
|
61
|
-
*/
|
|
62
|
-
function resolvePropertyIdentifierForDataSource(args) {
|
|
63
|
-
const { dataSource, identifier, operationName } = args;
|
|
64
|
-
if (dataSource !== undefined && identifier in dataSource.propertyIdsByKey) {
|
|
65
|
-
const propertyId = dataSource.propertyIdsByKey[identifier];
|
|
66
|
-
if (propertyId === undefined) {
|
|
67
|
-
return {
|
|
68
|
-
status: "error",
|
|
69
|
-
error: `${operationName} cannot resolve property key "${identifier}" because it is not bound to a Notion property.`,
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
return { status: "success", propertyId };
|
|
73
|
-
}
|
|
74
|
-
return { status: "success", propertyId: identifier };
|
|
75
|
-
}
|
|
76
|
-
function createEmptyDataSourceQueryState() {
|
|
77
|
-
return {
|
|
78
|
-
items: [],
|
|
79
|
-
isLoading: false,
|
|
80
|
-
hasMore: false,
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
|
-
class NotionCustomBlockBridge {
|
|
14
|
+
export const CUSTOM_BLOCK_BRIDGE_PROTOCOL_VERSION = 1;
|
|
15
|
+
export class SandboxBridge {
|
|
84
16
|
constructor() {
|
|
85
17
|
this.hostState = {
|
|
86
18
|
status: "uninitialized",
|
|
@@ -90,6 +22,8 @@ class NotionCustomBlockBridge {
|
|
|
90
22
|
this.nextRequestId = 1;
|
|
91
23
|
this.pendingCreatePage = new PendingRequests("custom-block-create-page");
|
|
92
24
|
this.pendingGetPage = new PendingRequests("custom-block-get-page");
|
|
25
|
+
this.pendingGetUser = new PendingRequests("custom-block-get-user");
|
|
26
|
+
this.pendingListUsers = new PendingRequests("custom-block-list-users");
|
|
93
27
|
this.pendingUpdatePage = new PendingRequests("custom-block-update-page");
|
|
94
28
|
this.initMessage = new Promise(resolve => {
|
|
95
29
|
this.resolveInit = resolve;
|
|
@@ -205,6 +139,24 @@ class NotionCustomBlockBridge {
|
|
|
205
139
|
}
|
|
206
140
|
return;
|
|
207
141
|
}
|
|
142
|
+
case "getUserResult": {
|
|
143
|
+
const result = message.status === "success"
|
|
144
|
+
? { status: "success", user: message.user }
|
|
145
|
+
: { status: "error", error: message.error };
|
|
146
|
+
if (!this.pendingGetUser.resolve(message.requestId, result)) {
|
|
147
|
+
console.warn(`[notion-custom-sdk] getUserResult for unknown requestId ${message.requestId}`);
|
|
148
|
+
}
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
case "listUsersResult": {
|
|
152
|
+
const result = message.status === "success"
|
|
153
|
+
? { status: "success", list: message.list }
|
|
154
|
+
: { status: "error", error: message.error };
|
|
155
|
+
if (!this.pendingListUsers.resolve(message.requestId, result)) {
|
|
156
|
+
console.warn(`[notion-custom-sdk] listUsersResult for unknown requestId ${message.requestId}`);
|
|
157
|
+
}
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
208
160
|
case "updatePageResult": {
|
|
209
161
|
const result = message.status === "success"
|
|
210
162
|
? { status: "success", page: message.page }
|
|
@@ -270,7 +222,7 @@ class NotionCustomBlockBridge {
|
|
|
270
222
|
});
|
|
271
223
|
});
|
|
272
224
|
}
|
|
273
|
-
|
|
225
|
+
sendReady(manifest) {
|
|
274
226
|
if (typeof window === "undefined") {
|
|
275
227
|
return;
|
|
276
228
|
}
|
|
@@ -412,6 +364,31 @@ class NotionCustomBlockBridge {
|
|
|
412
364
|
window.parent.postMessage(outbound, "*");
|
|
413
365
|
});
|
|
414
366
|
}
|
|
367
|
+
getUser(userId) {
|
|
368
|
+
return new Promise(resolve => {
|
|
369
|
+
const requestId = this.pendingGetUser.allocate(resolve);
|
|
370
|
+
const outbound = {
|
|
371
|
+
type: "getUser",
|
|
372
|
+
requestId,
|
|
373
|
+
userId,
|
|
374
|
+
};
|
|
375
|
+
console.debug("[notion-custom-sdk] outbound postMessage", outbound);
|
|
376
|
+
window.parent.postMessage(outbound, "*");
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
listUsers(input = {}) {
|
|
380
|
+
return new Promise(resolve => {
|
|
381
|
+
const requestId = this.pendingListUsers.allocate(resolve);
|
|
382
|
+
const outbound = {
|
|
383
|
+
type: "listUsers",
|
|
384
|
+
requestId,
|
|
385
|
+
startCursor: input.startCursor,
|
|
386
|
+
pageSize: input.pageSize,
|
|
387
|
+
};
|
|
388
|
+
console.debug("[notion-custom-sdk] outbound postMessage", outbound);
|
|
389
|
+
window.parent.postMessage(outbound, "*");
|
|
390
|
+
});
|
|
391
|
+
}
|
|
415
392
|
updatePage(input) {
|
|
416
393
|
return new Promise(resolve => {
|
|
417
394
|
if ((input.properties === undefined ||
|
|
@@ -447,6 +424,31 @@ class NotionCustomBlockBridge {
|
|
|
447
424
|
window.parent.postMessage(outbound, "*");
|
|
448
425
|
});
|
|
449
426
|
}
|
|
427
|
+
/**
|
|
428
|
+
* Updates a page on a known data source, resolving any property keys against the data source's
|
|
429
|
+
* `propertyIdsByKey` before sending the bridge message. Used by the per-row `update` callback
|
|
430
|
+
* returned from {@link getDataSourceQueryView}.
|
|
431
|
+
*/
|
|
432
|
+
updateDataSourcePage(args) {
|
|
433
|
+
const { dataSource, pageId, input } = args;
|
|
434
|
+
const resolvedProperties = input.properties === undefined
|
|
435
|
+
? undefined
|
|
436
|
+
: resolvePropertyWriteMapForDataSource({
|
|
437
|
+
dataSource,
|
|
438
|
+
properties: input.properties,
|
|
439
|
+
operationName: "dataSourcePage.update",
|
|
440
|
+
});
|
|
441
|
+
if (resolvedProperties?.status === "error") {
|
|
442
|
+
return Promise.resolve(resolvedProperties);
|
|
443
|
+
}
|
|
444
|
+
return this.updatePage({
|
|
445
|
+
pageId,
|
|
446
|
+
properties: resolvedProperties?.properties,
|
|
447
|
+
icon: input.icon,
|
|
448
|
+
cover: input.cover,
|
|
449
|
+
archived: input.archived,
|
|
450
|
+
});
|
|
451
|
+
}
|
|
450
452
|
/**
|
|
451
453
|
* Translates the public `CreatePageInput["parent"]` into the bridge-native
|
|
452
454
|
* `CreatePageMessageParent`. The `data_source_key` variant is resolved sandbox-side against
|
|
@@ -496,65 +498,6 @@ class NotionCustomBlockBridge {
|
|
|
496
498
|
}
|
|
497
499
|
}
|
|
498
500
|
}
|
|
499
|
-
function resolveDataSources(args) {
|
|
500
|
-
if (args.manifest === null) {
|
|
501
|
-
return Object.entries(args.dataSourceBindings).map(([key, binding]) => ({
|
|
502
|
-
key,
|
|
503
|
-
collectionPointer: binding.collectionPointer,
|
|
504
|
-
collectionSchema: binding.collectionSchema,
|
|
505
|
-
propertyIdsByKey: { ...(binding.propertyIdsByKey ?? {}) },
|
|
506
|
-
propertySchemasById: binding.collectionSchema?.propertiesById ?? {},
|
|
507
|
-
}));
|
|
508
|
-
}
|
|
509
|
-
return Object.entries(args.manifest.dataSources).flatMap(([key, manifestDataSource]) => {
|
|
510
|
-
const binding = args.dataSourceBindings[key];
|
|
511
|
-
const propertySchemasById = binding?.collectionSchema?.propertiesById ?? {};
|
|
512
|
-
const bindingPropertyIdsByKey = binding?.propertyIdsByKey ?? {};
|
|
513
|
-
const propertyIdsByKey = {};
|
|
514
|
-
const manifestProperties = Object.entries(manifestDataSource.properties ?? {});
|
|
515
|
-
for (const [propertyKey, manifestProperty] of manifestProperties) {
|
|
516
|
-
const propertyId = propertyKey in bindingPropertyIdsByKey
|
|
517
|
-
? bindingPropertyIdsByKey[propertyKey]
|
|
518
|
-
: findPropertyIdByManifestProperty(propertySchemasById, {
|
|
519
|
-
key: propertyKey,
|
|
520
|
-
type: manifestProperty.type,
|
|
521
|
-
});
|
|
522
|
-
const propertySchema = propertyId === undefined ? undefined : propertySchemasById[propertyId];
|
|
523
|
-
propertyIdsByKey[propertyKey] =
|
|
524
|
-
propertySchema?.type === manifestProperty.type
|
|
525
|
-
? propertyId
|
|
526
|
-
: undefined;
|
|
527
|
-
}
|
|
528
|
-
return [
|
|
529
|
-
{
|
|
530
|
-
key,
|
|
531
|
-
collectionPointer: binding?.collectionPointer,
|
|
532
|
-
collectionSchema: binding?.collectionSchema,
|
|
533
|
-
propertyIdsByKey,
|
|
534
|
-
propertySchemasById,
|
|
535
|
-
},
|
|
536
|
-
];
|
|
537
|
-
});
|
|
538
|
-
}
|
|
539
|
-
function findPropertyIdByManifestProperty(propertySchemasById, manifestProperty) {
|
|
540
|
-
// `ManifestProperty.name` is display copy for setup UI. Binding fallback is based
|
|
541
|
-
// on the stable semantic key so copy changes don't affect property resolution.
|
|
542
|
-
const propertySchemaForKey = propertySchemasById[manifestProperty.key];
|
|
543
|
-
if (propertySchemaForKey?.type === manifestProperty.type) {
|
|
544
|
-
return manifestProperty.key;
|
|
545
|
-
}
|
|
546
|
-
const normalizedKey = normalizePropertyName(manifestProperty.key);
|
|
547
|
-
for (const [propertyId, propertySchema] of Object.entries(propertySchemasById)) {
|
|
548
|
-
if (propertySchema.type === manifestProperty.type &&
|
|
549
|
-
normalizePropertyName(propertySchema.name) === normalizedKey) {
|
|
550
|
-
return propertyId;
|
|
551
|
-
}
|
|
552
|
-
}
|
|
553
|
-
return undefined;
|
|
554
|
-
}
|
|
555
|
-
function normalizePropertyName(value) {
|
|
556
|
-
return value.trim().toLowerCase();
|
|
557
|
-
}
|
|
558
501
|
function formatInvalidHostReason(incomingType, issues) {
|
|
559
502
|
const labelled = incomingType
|
|
560
503
|
? `host message of type "${incomingType}"`
|
|
@@ -571,203 +514,3 @@ function formatInvalidHostReason(incomingType, issues) {
|
|
|
571
514
|
const extra = issues.length > 1 ? ` (+${issues.length - 1} more)` : "";
|
|
572
515
|
return `Could not parse ${labelled}: ${detail}${extra}`;
|
|
573
516
|
}
|
|
574
|
-
/**
|
|
575
|
-
* Attempts to load a `custom_blocks.json` manifest co-located with the bundle. Treats
|
|
576
|
-
* any non-200 response as "no manifest".
|
|
577
|
-
*/
|
|
578
|
-
async function tryFetchManifest() {
|
|
579
|
-
if (typeof fetch !== "function") {
|
|
580
|
-
console.warn(`[notion-custom-sdk] no \`fetch\` available; cannot load ${MANIFEST_URL}`);
|
|
581
|
-
return null;
|
|
582
|
-
}
|
|
583
|
-
let response;
|
|
584
|
-
try {
|
|
585
|
-
response = await fetch(MANIFEST_URL, { credentials: "omit" });
|
|
586
|
-
}
|
|
587
|
-
catch (error) {
|
|
588
|
-
console.warn(`[notion-custom-sdk] no manifest fetched from ${MANIFEST_URL}`, error);
|
|
589
|
-
return null;
|
|
590
|
-
}
|
|
591
|
-
if (!response.ok) {
|
|
592
|
-
console.warn(`[notion-custom-sdk] no manifest at ${MANIFEST_URL} (status ${response.status})`);
|
|
593
|
-
return null;
|
|
594
|
-
}
|
|
595
|
-
let json;
|
|
596
|
-
try {
|
|
597
|
-
json = await response.json();
|
|
598
|
-
}
|
|
599
|
-
catch (error) {
|
|
600
|
-
console.warn(`[notion-custom-sdk] manifest at ${MANIFEST_URL} was not valid JSON`, error);
|
|
601
|
-
return null;
|
|
602
|
-
}
|
|
603
|
-
const parsed = v.safeParse(manifestSchema, json);
|
|
604
|
-
if (!parsed.success) {
|
|
605
|
-
console.warn(`[notion-custom-sdk] manifest at ${MANIFEST_URL} did not match schema`, parsed.issues);
|
|
606
|
-
return null;
|
|
607
|
-
}
|
|
608
|
-
return parsed.output;
|
|
609
|
-
}
|
|
610
|
-
const bridge = new NotionCustomBlockBridge();
|
|
611
|
-
export function subscribeToCustomBlockHost(listener) {
|
|
612
|
-
return bridge.subscribe(listener);
|
|
613
|
-
}
|
|
614
|
-
export function getCustomBlockHostState() {
|
|
615
|
-
return bridge.getHostState();
|
|
616
|
-
}
|
|
617
|
-
export function queryCustomBlockDataSource(key, limit) {
|
|
618
|
-
bridge.queryDataSource(key, limit);
|
|
619
|
-
}
|
|
620
|
-
/**
|
|
621
|
-
* Apply an `init` payload to the bridge directly, bypassing the postMessage
|
|
622
|
-
* handshake. Used by the React provider when it needs to seed placeholder
|
|
623
|
-
* state (e.g. the standalone preview fallback when not embedded in Notion).
|
|
624
|
-
* The bridge applies the payload through the same code path as a real host.
|
|
625
|
-
*/
|
|
626
|
-
export function setMockCustomBlockState(message) {
|
|
627
|
-
bridge.setMockState(message);
|
|
628
|
-
}
|
|
629
|
-
/**
|
|
630
|
-
* Thrown by {@link initCustomBlock} when the SDK is loaded in a top-level
|
|
631
|
-
* window (no parent frame) — `postMessage` would just hit the same window and
|
|
632
|
-
* the handshake can never complete. `<NotionCustomBlock>` catches this
|
|
633
|
-
* specifically and falls back to a standalone preview with a warning banner;
|
|
634
|
-
* direct callers can `instanceof` it to apply their own policy.
|
|
635
|
-
*/
|
|
636
|
-
export class NotInIframeError extends Error {
|
|
637
|
-
constructor(message = NOT_IN_IFRAME_MESSAGE) {
|
|
638
|
-
super(message);
|
|
639
|
-
this.name = "NotInIframeError";
|
|
640
|
-
}
|
|
641
|
-
}
|
|
642
|
-
const DEFAULT_INIT_TIMEOUT_MS = 2000;
|
|
643
|
-
const NOT_IN_IFRAME_MESSAGE = "<NotionCustomBlock> only works inside an iframe — use the dev shell or deploy to Notion.";
|
|
644
|
-
let initPromise;
|
|
645
|
-
/**
|
|
646
|
-
* Performs the SDK ↔ host handshake: loads `custom_blocks.json`, posts
|
|
647
|
-
* `ready`, then awaits the host's first `init` message. Resolves with that
|
|
648
|
-
* payload; rejects with a
|
|
649
|
-
* `TimeoutError` if the host doesn't respond inside `timeoutMs`.
|
|
650
|
-
*
|
|
651
|
-
* Idempotent: subsequent calls return the same promise as the first and
|
|
652
|
-
* ignore any new options. Mount your React tree (or call any SDK hook /
|
|
653
|
-
* `subscribeToCustomBlockHost`) only after the returned promise resolves.
|
|
654
|
-
*
|
|
655
|
-
* @example
|
|
656
|
-
* // src/index.tsx
|
|
657
|
-
* const initial = await initCustomBlock()
|
|
658
|
-
* console.log(initial.theme, initial.context.customBlockId)
|
|
659
|
-
* ReactDOM.createRoot(root).render(<App />)
|
|
660
|
-
*/
|
|
661
|
-
export function initCustomBlock(opts = {}) {
|
|
662
|
-
initPromise ?? (initPromise = (async () => {
|
|
663
|
-
// Standalone tab (no parent frame) — fail fast with a typed error
|
|
664
|
-
// rather than letting `awaitInit` time out, since `postMessage` to
|
|
665
|
-
// `window.parent` would just hit the same window and never arrive.
|
|
666
|
-
// `<NotionCustomBlock>` catches this specifically to render a warning
|
|
667
|
-
// banner + children for dev-time previews.
|
|
668
|
-
if (typeof window !== "undefined" && window.parent === window) {
|
|
669
|
-
throw new NotInIframeError();
|
|
670
|
-
}
|
|
671
|
-
const manifest = await tryFetchManifest();
|
|
672
|
-
await bridge.sendReady(manifest);
|
|
673
|
-
const timeoutMs = opts.timeoutMs ?? DEFAULT_INIT_TIMEOUT_MS;
|
|
674
|
-
const message = await bridge.awaitInit(AbortSignal.timeout(timeoutMs));
|
|
675
|
-
const hostState = bridge.getHostState();
|
|
676
|
-
return {
|
|
677
|
-
theme: message.theme,
|
|
678
|
-
context: message.context,
|
|
679
|
-
dataSources: hostState.status === "initialized" ? hostState.dataSources : [],
|
|
680
|
-
};
|
|
681
|
-
})());
|
|
682
|
-
return initPromise;
|
|
683
|
-
}
|
|
684
|
-
const EMPTY_QUERY_VIEW = {
|
|
685
|
-
items: [],
|
|
686
|
-
propertySchemasById: {},
|
|
687
|
-
propertySchemasByKey: {},
|
|
688
|
-
isLoading: false,
|
|
689
|
-
hasMore: false,
|
|
690
|
-
};
|
|
691
|
-
export function getDataSourceQueryView(hostState, key) {
|
|
692
|
-
if (hostState.status !== "initialized") {
|
|
693
|
-
return EMPTY_QUERY_VIEW;
|
|
694
|
-
}
|
|
695
|
-
const dataSource = hostState.dataSources.find(entry => entry.key === key);
|
|
696
|
-
const queryState = hostState.dataSourceState[key] ?? createEmptyDataSourceQueryState();
|
|
697
|
-
if (dataSource === undefined) {
|
|
698
|
-
return {
|
|
699
|
-
items: [],
|
|
700
|
-
collectionSchema: undefined,
|
|
701
|
-
propertySchemasById: {},
|
|
702
|
-
propertySchemasByKey: {},
|
|
703
|
-
isLoading: queryState.isLoading,
|
|
704
|
-
hasMore: queryState.hasMore,
|
|
705
|
-
error: queryState.error,
|
|
706
|
-
};
|
|
707
|
-
}
|
|
708
|
-
const propertyIdsByKey = dataSource.propertyIdsByKey;
|
|
709
|
-
const propertySchemasById = dataSource.propertySchemasById;
|
|
710
|
-
const resolvedItems = queryState.items.map(entry => {
|
|
711
|
-
const propertiesByKey = {};
|
|
712
|
-
for (const [key, propertyId] of Object.entries(propertyIdsByKey)) {
|
|
713
|
-
propertiesByKey[key] =
|
|
714
|
-
propertyId === undefined ? undefined : entry.propertiesById[propertyId];
|
|
715
|
-
}
|
|
716
|
-
return {
|
|
717
|
-
id: entry.id,
|
|
718
|
-
propertiesById: entry.propertiesById,
|
|
719
|
-
propertiesByKey,
|
|
720
|
-
update: input => updateDataSourcePage({
|
|
721
|
-
dataSource,
|
|
722
|
-
pageId: entry.id,
|
|
723
|
-
input,
|
|
724
|
-
}),
|
|
725
|
-
};
|
|
726
|
-
});
|
|
727
|
-
const propertySchemasByKey = {};
|
|
728
|
-
for (const [key, propertyId] of Object.entries(propertyIdsByKey)) {
|
|
729
|
-
propertySchemasByKey[key] =
|
|
730
|
-
propertyId === undefined ? undefined : propertySchemasById[propertyId];
|
|
731
|
-
}
|
|
732
|
-
return {
|
|
733
|
-
items: resolvedItems,
|
|
734
|
-
collectionSchema: dataSource.collectionSchema,
|
|
735
|
-
propertySchemasById,
|
|
736
|
-
propertySchemasByKey,
|
|
737
|
-
isLoading: queryState.isLoading,
|
|
738
|
-
hasMore: queryState.hasMore,
|
|
739
|
-
error: queryState.error,
|
|
740
|
-
};
|
|
741
|
-
}
|
|
742
|
-
function updateDataSourcePage(args) {
|
|
743
|
-
const { dataSource, pageId, input } = args;
|
|
744
|
-
const resolvedProperties = input.properties === undefined
|
|
745
|
-
? undefined
|
|
746
|
-
: resolvePropertyWriteMapForDataSource({
|
|
747
|
-
dataSource,
|
|
748
|
-
properties: input.properties,
|
|
749
|
-
operationName: "dataSourcePage.update",
|
|
750
|
-
});
|
|
751
|
-
if (resolvedProperties?.status === "error") {
|
|
752
|
-
return Promise.resolve(resolvedProperties);
|
|
753
|
-
}
|
|
754
|
-
return bridge.updatePage({
|
|
755
|
-
pageId,
|
|
756
|
-
properties: resolvedProperties?.properties,
|
|
757
|
-
icon: input.icon,
|
|
758
|
-
cover: input.cover,
|
|
759
|
-
archived: input.archived,
|
|
760
|
-
});
|
|
761
|
-
}
|
|
762
|
-
export function createPage(input) {
|
|
763
|
-
return bridge.createPage(input);
|
|
764
|
-
}
|
|
765
|
-
export function getPage(pageId) {
|
|
766
|
-
return bridge.getPage(pageId);
|
|
767
|
-
}
|
|
768
|
-
export function updatePage(input) {
|
|
769
|
-
return bridge.updatePage(input);
|
|
770
|
-
}
|
|
771
|
-
export function postCustomBlockResize(height) {
|
|
772
|
-
bridge.postResize(height);
|
|
773
|
-
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { CustomBlockManifest } from "../manifest";
|
|
2
|
+
import type { NotionDataSource, NotionDataSourceBindings } from "./dataSource";
|
|
3
|
+
import type { NotionPropertySchema } from "./propertySchema";
|
|
4
|
+
type ResolveDataSourcesArgs = {
|
|
5
|
+
manifest: CustomBlockManifest | null;
|
|
6
|
+
dataSourceBindings: NotionDataSourceBindings;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Builds the public {@link NotionDataSource} list the SDK exposes to consumers.
|
|
10
|
+
*
|
|
11
|
+
* Combines the host-supplied bindings (collection pointers + schemas) with the
|
|
12
|
+
* manifest's declared data-source keys. When the manifest names a property
|
|
13
|
+
* that isn't in `propertyIdsByKey`, falls back to {@link findPropertyIdByManifestProperty}
|
|
14
|
+
* so renames in Notion still resolve as long as the schema name matches.
|
|
15
|
+
*/
|
|
16
|
+
export declare function resolveDataSources(args: ResolveDataSourcesArgs): NotionDataSource[];
|
|
17
|
+
export declare function findPropertyIdByManifestProperty(propertySchemasById: Record<string, NotionPropertySchema>, manifestProperty: {
|
|
18
|
+
key: string;
|
|
19
|
+
type: NotionPropertySchema["type"];
|
|
20
|
+
}): string | undefined;
|
|
21
|
+
export {};
|
|
22
|
+
//# sourceMappingURL=resolve.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolve.d.ts","sourceRoot":"","sources":["../../../bridge/dataSources/resolve.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAA;AACtD,OAAO,KAAK,EAAE,gBAAgB,EAAE,wBAAwB,EAAE,MAAM,cAAc,CAAA;AAC9E,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAA;AAE5D,KAAK,sBAAsB,GAAG;IAC7B,QAAQ,EAAE,mBAAmB,GAAG,IAAI,CAAA;IACpC,kBAAkB,EAAE,wBAAwB,CAAA;CAC5C,CAAA;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CACjC,IAAI,EAAE,sBAAsB,GAC1B,gBAAgB,EAAE,CA+CpB;AAED,wBAAgB,gCAAgC,CAC/C,mBAAmB,EAAE,MAAM,CAAC,MAAM,EAAE,oBAAoB,CAAC,EACzD,gBAAgB,EAAE;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,oBAAoB,CAAC,MAAM,CAAC,CAAA;CAAE,GACnE,MAAM,GAAG,SAAS,CAoBpB"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds the public {@link NotionDataSource} list the SDK exposes to consumers.
|
|
3
|
+
*
|
|
4
|
+
* Combines the host-supplied bindings (collection pointers + schemas) with the
|
|
5
|
+
* manifest's declared data-source keys. When the manifest names a property
|
|
6
|
+
* that isn't in `propertyIdsByKey`, falls back to {@link findPropertyIdByManifestProperty}
|
|
7
|
+
* so renames in Notion still resolve as long as the schema name matches.
|
|
8
|
+
*/
|
|
9
|
+
export function resolveDataSources(args) {
|
|
10
|
+
if (args.manifest === null) {
|
|
11
|
+
return Object.entries(args.dataSourceBindings).map(([key, binding]) => ({
|
|
12
|
+
key,
|
|
13
|
+
collectionPointer: binding.collectionPointer,
|
|
14
|
+
collectionSchema: binding.collectionSchema,
|
|
15
|
+
propertyIdsByKey: { ...(binding.propertyIdsByKey ?? {}) },
|
|
16
|
+
propertySchemasById: binding.collectionSchema?.propertiesById ?? {},
|
|
17
|
+
}));
|
|
18
|
+
}
|
|
19
|
+
return Object.entries(args.manifest.dataSources).map(([key, manifestDataSource]) => {
|
|
20
|
+
const binding = args.dataSourceBindings[key];
|
|
21
|
+
const propertySchemasById = binding?.collectionSchema?.propertiesById ?? {};
|
|
22
|
+
const bindingPropertyIdsByKey = binding?.propertyIdsByKey ?? {};
|
|
23
|
+
const propertyIdsByKey = {};
|
|
24
|
+
const manifestProperties = Object.entries(manifestDataSource.properties ?? {});
|
|
25
|
+
for (const [propertyKey, manifestProperty] of manifestProperties) {
|
|
26
|
+
const propertyId = propertyKey in bindingPropertyIdsByKey
|
|
27
|
+
? bindingPropertyIdsByKey[propertyKey]
|
|
28
|
+
: findPropertyIdByManifestProperty(propertySchemasById, {
|
|
29
|
+
key: propertyKey,
|
|
30
|
+
type: manifestProperty.type,
|
|
31
|
+
});
|
|
32
|
+
const propertySchema = propertyId === undefined ? undefined : propertySchemasById[propertyId];
|
|
33
|
+
propertyIdsByKey[propertyKey] =
|
|
34
|
+
propertySchema?.type === manifestProperty.type
|
|
35
|
+
? propertyId
|
|
36
|
+
: undefined;
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
key,
|
|
40
|
+
collectionPointer: binding?.collectionPointer,
|
|
41
|
+
collectionSchema: binding?.collectionSchema,
|
|
42
|
+
propertyIdsByKey,
|
|
43
|
+
propertySchemasById,
|
|
44
|
+
};
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
export function findPropertyIdByManifestProperty(propertySchemasById, manifestProperty) {
|
|
48
|
+
// `ManifestProperty.name` is display copy for setup UI. Binding fallback is based
|
|
49
|
+
// on the stable semantic key so copy changes don't affect property resolution.
|
|
50
|
+
const propertySchemaForKey = propertySchemasById[manifestProperty.key];
|
|
51
|
+
if (propertySchemaForKey?.type === manifestProperty.type) {
|
|
52
|
+
return manifestProperty.key;
|
|
53
|
+
}
|
|
54
|
+
const normalizedKey = normalizePropertyName(manifestProperty.key);
|
|
55
|
+
for (const [propertyId, propertySchema] of Object.entries(propertySchemasById)) {
|
|
56
|
+
if (propertySchema.type === manifestProperty.type &&
|
|
57
|
+
normalizePropertyName(propertySchema.name) === normalizedKey) {
|
|
58
|
+
return propertyId;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
function normalizePropertyName(value) {
|
|
64
|
+
return value.trim().toLowerCase();
|
|
65
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { NotionPagePropertyInputMap, NotionPagePropertyWriteMap } from "../pages/page";
|
|
2
|
+
import type { NotionDataSource } from "./dataSource";
|
|
3
|
+
export type PropertyWriteMapResolutionResult = {
|
|
4
|
+
status: "success";
|
|
5
|
+
properties: NotionPagePropertyWriteMap;
|
|
6
|
+
} | {
|
|
7
|
+
status: "error";
|
|
8
|
+
error: string;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Resolves a public SDK property write map into the ID-keyed bridge shape.
|
|
12
|
+
*
|
|
13
|
+
* Identifiers in `properties` may be raw property IDs or data-source property keys; the latter
|
|
14
|
+
* are looked up in the `dataSource`'s `propertyIdsByKey`. Each value is re-parsed through
|
|
15
|
+
* `notionPagePropertyValueSchema` with its `id` rewritten to the resolved property ID.
|
|
16
|
+
*/
|
|
17
|
+
export declare function resolvePropertyWriteMapForDataSource(args: {
|
|
18
|
+
dataSource: NotionDataSource | undefined;
|
|
19
|
+
properties: NotionPagePropertyInputMap;
|
|
20
|
+
operationName: string;
|
|
21
|
+
}): PropertyWriteMapResolutionResult;
|
|
22
|
+
/**
|
|
23
|
+
* Treats matching data-source keys as aliases for property IDs; all other identifiers are raw IDs.
|
|
24
|
+
*/
|
|
25
|
+
export declare function resolvePropertyIdentifierForDataSource(args: {
|
|
26
|
+
dataSource: NotionDataSource | undefined;
|
|
27
|
+
identifier: string;
|
|
28
|
+
operationName: string;
|
|
29
|
+
}): {
|
|
30
|
+
status: "success";
|
|
31
|
+
propertyId: string;
|
|
32
|
+
} | {
|
|
33
|
+
status: "error";
|
|
34
|
+
error: string;
|
|
35
|
+
};
|
|
36
|
+
//# sourceMappingURL=resolveProperty.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolveProperty.d.ts","sourceRoot":"","sources":["../../../bridge/dataSources/resolveProperty.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACX,0BAA0B,EAC1B,0BAA0B,EAC1B,MAAM,eAAe,CAAA;AAEtB,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAA;AAEpD,MAAM,MAAM,gCAAgC,GACzC;IAAE,MAAM,EAAE,SAAS,CAAC;IAAC,UAAU,EAAE,0BAA0B,CAAA;CAAE,GAC7D;IAAE,MAAM,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAA;AAErC;;;;;;GAMG;AACH,wBAAgB,oCAAoC,CAAC,IAAI,EAAE;IAC1D,UAAU,EAAE,gBAAgB,GAAG,SAAS,CAAA;IACxC,UAAU,EAAE,0BAA0B,CAAA;IACtC,aAAa,EAAE,MAAM,CAAA;CACrB,GAAG,gCAAgC,CA+CnC;AAED;;GAEG;AACH,wBAAgB,sCAAsC,CAAC,IAAI,EAAE;IAC5D,UAAU,EAAE,gBAAgB,GAAG,SAAS,CAAA;IACxC,UAAU,EAAE,MAAM,CAAA;IAClB,aAAa,EAAE,MAAM,CAAA;CACrB,GACE;IAAE,MAAM,EAAE,SAAS,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,GACzC;IAAE,MAAM,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAcpC"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import * as v from "valibot";
|
|
2
|
+
import { notionPagePropertyValueSchema } from "../pages/page";
|
|
3
|
+
/**
|
|
4
|
+
* Resolves a public SDK property write map into the ID-keyed bridge shape.
|
|
5
|
+
*
|
|
6
|
+
* Identifiers in `properties` may be raw property IDs or data-source property keys; the latter
|
|
7
|
+
* are looked up in the `dataSource`'s `propertyIdsByKey`. Each value is re-parsed through
|
|
8
|
+
* `notionPagePropertyValueSchema` with its `id` rewritten to the resolved property ID.
|
|
9
|
+
*/
|
|
10
|
+
export function resolvePropertyWriteMapForDataSource(args) {
|
|
11
|
+
const { dataSource, properties, operationName } = args;
|
|
12
|
+
const resolvedProperties = {};
|
|
13
|
+
for (const [identifier, value] of Object.entries(properties)) {
|
|
14
|
+
const propertyIdResult = resolvePropertyIdentifierForDataSource({
|
|
15
|
+
dataSource,
|
|
16
|
+
identifier,
|
|
17
|
+
operationName,
|
|
18
|
+
});
|
|
19
|
+
if (propertyIdResult.status === "error") {
|
|
20
|
+
return propertyIdResult;
|
|
21
|
+
}
|
|
22
|
+
const propertyId = propertyIdResult.propertyId;
|
|
23
|
+
if (value.id !== undefined &&
|
|
24
|
+
value.id !== identifier &&
|
|
25
|
+
value.id !== propertyId) {
|
|
26
|
+
return {
|
|
27
|
+
status: "error",
|
|
28
|
+
error: `Property ${identifier} resolved to ${propertyId} but value id was ${value.id}.`,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
if (resolvedProperties[propertyId] !== undefined) {
|
|
32
|
+
return {
|
|
33
|
+
status: "error",
|
|
34
|
+
error: `Cannot set property ${propertyId} more than once.`,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
const parsedValue = v.safeParse(notionPagePropertyValueSchema, {
|
|
38
|
+
...value,
|
|
39
|
+
id: propertyId,
|
|
40
|
+
});
|
|
41
|
+
if (!parsedValue.success) {
|
|
42
|
+
return {
|
|
43
|
+
status: "error",
|
|
44
|
+
error: `Invalid value for property ${identifier}.`,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
resolvedProperties[propertyId] = parsedValue.output;
|
|
48
|
+
}
|
|
49
|
+
return { status: "success", properties: resolvedProperties };
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Treats matching data-source keys as aliases for property IDs; all other identifiers are raw IDs.
|
|
53
|
+
*/
|
|
54
|
+
export function resolvePropertyIdentifierForDataSource(args) {
|
|
55
|
+
const { dataSource, identifier, operationName } = args;
|
|
56
|
+
if (dataSource !== undefined && identifier in dataSource.propertyIdsByKey) {
|
|
57
|
+
const propertyId = dataSource.propertyIdsByKey[identifier];
|
|
58
|
+
if (propertyId === undefined) {
|
|
59
|
+
return {
|
|
60
|
+
status: "error",
|
|
61
|
+
error: `${operationName} cannot resolve property key "${identifier}" because it is not bound to a Notion property.`,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
return { status: "success", propertyId };
|
|
65
|
+
}
|
|
66
|
+
return { status: "success", propertyId: identifier };
|
|
67
|
+
}
|