@notionhq/custom-blocks 0.0.58
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 +171 -0
- package/README.md +57 -0
- package/bin/cli/attach.js +180 -0
- package/bin/cli/cli.js +248 -0
- package/bin/cli/create.js +106 -0
- package/bin/cli/datasources.js +255 -0
- package/bin/cli/deploy.js +109 -0
- package/bin/cli/ids.js +13 -0
- package/bin/cli/ntn.js +95 -0
- package/bin/cli/pullData.js +76 -0
- package/bin/cli/pullManifest.js +158 -0
- package/bin/cli/target.js +95 -0
- package/bin/src/bridge/dataSources/propertySchema.js +148 -0
- package/bin/src/bridge/manifest.js +40 -0
- package/dist/autoResize.d.ts +4 -0
- package/dist/autoResize.d.ts.map +1 -0
- package/dist/autoResize.js +71 -0
- package/dist/bridge/SandboxBridge.d.ts +92 -0
- package/dist/bridge/SandboxBridge.d.ts.map +1 -0
- package/dist/bridge/SandboxBridge.js +718 -0
- package/dist/bridge/dataSources/dataSource.d.ts +732 -0
- package/dist/bridge/dataSources/dataSource.d.ts.map +1 -0
- package/dist/bridge/dataSources/dataSource.js +37 -0
- package/dist/bridge/dataSources/dataSourcePage.d.ts +118 -0
- package/dist/bridge/dataSources/dataSourcePage.d.ts.map +1 -0
- package/dist/bridge/dataSources/dataSourcePage.js +14 -0
- package/dist/bridge/dataSources/dataSourceValue.d.ts +67 -0
- package/dist/bridge/dataSources/dataSourceValue.d.ts.map +1 -0
- package/dist/bridge/dataSources/dataSourceValue.js +14 -0
- package/dist/bridge/dataSources/dateValue.d.ts +158 -0
- package/dist/bridge/dataSources/dateValue.d.ts.map +1 -0
- package/dist/bridge/dataSources/dateValue.js +59 -0
- package/dist/bridge/dataSources/errors.d.ts +6 -0
- package/dist/bridge/dataSources/errors.d.ts.map +1 -0
- package/dist/bridge/dataSources/errors.js +4 -0
- package/dist/bridge/dataSources/propertySchema.d.ts +191 -0
- package/dist/bridge/dataSources/propertySchema.d.ts.map +1 -0
- package/dist/bridge/dataSources/propertySchema.js +148 -0
- package/dist/bridge/dataSources/recordPointer.d.ts +10 -0
- package/dist/bridge/dataSources/recordPointer.d.ts.map +1 -0
- package/dist/bridge/dataSources/recordPointer.js +8 -0
- 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 +41 -0
- package/dist/bridge/dataSources/resolveProperty.d.ts.map +1 -0
- package/dist/bridge/dataSources/resolveProperty.js +73 -0
- package/dist/bridge/errors.d.ts +12 -0
- package/dist/bridge/errors.d.ts.map +1 -0
- package/dist/bridge/errors.js +6 -0
- package/dist/bridge/hostState.d.ts +65 -0
- package/dist/bridge/hostState.d.ts.map +1 -0
- package/dist/bridge/hostState.js +65 -0
- package/dist/bridge/ids.d.ts +39 -0
- package/dist/bridge/ids.d.ts.map +1 -0
- package/dist/bridge/ids.js +5 -0
- package/dist/bridge/incomingType.d.ts +10 -0
- package/dist/bridge/incomingType.d.ts.map +1 -0
- package/dist/bridge/incomingType.js +17 -0
- package/dist/bridge/loadManifest.d.ts +25 -0
- package/dist/bridge/loadManifest.d.ts.map +1 -0
- package/dist/bridge/loadManifest.js +68 -0
- package/dist/bridge/manifest.d.ts +66 -0
- package/dist/bridge/manifest.d.ts.map +1 -0
- package/dist/bridge/manifest.js +40 -0
- package/dist/bridge/messages/createPage.d.ts +225 -0
- package/dist/bridge/messages/createPage.d.ts.map +1 -0
- package/dist/bridge/messages/createPage.js +46 -0
- package/dist/bridge/messages/createPageResult.d.ts +211 -0
- package/dist/bridge/messages/createPageResult.d.ts.map +1 -0
- package/dist/bridge/messages/createPageResult.js +28 -0
- package/dist/bridge/messages/currentUserChanged.d.ts +19 -0
- package/dist/bridge/messages/currentUserChanged.d.ts.map +1 -0
- package/dist/bridge/messages/currentUserChanged.js +9 -0
- package/dist/bridge/messages/dataSourcesChanged.d.ts +158 -0
- package/dist/bridge/messages/dataSourcesChanged.d.ts.map +1 -0
- package/dist/bridge/messages/dataSourcesChanged.js +13 -0
- package/dist/bridge/messages/getPage.d.ts +216 -0
- package/dist/bridge/messages/getPage.d.ts.map +1 -0
- package/dist/bridge/messages/getPage.js +33 -0
- package/dist/bridge/messages/getUser.d.ts +45 -0
- package/dist/bridge/messages/getUser.d.ts.map +1 -0
- package/dist/bridge/messages/getUser.js +32 -0
- package/dist/bridge/messages/hostToSandbox.d.ts +1089 -0
- package/dist/bridge/messages/hostToSandbox.d.ts.map +1 -0
- package/dist/bridge/messages/hostToSandbox.js +32 -0
- package/dist/bridge/messages/init.d.ts +209 -0
- package/dist/bridge/messages/init.d.ts.map +1 -0
- package/dist/bridge/messages/init.js +47 -0
- package/dist/bridge/messages/invalidHostMessage.d.ts +15 -0
- package/dist/bridge/messages/invalidHostMessage.d.ts.map +1 -0
- package/dist/bridge/messages/invalidHostMessage.js +13 -0
- package/dist/bridge/messages/invalidSandboxMessage.d.ts +15 -0
- package/dist/bridge/messages/invalidSandboxMessage.d.ts.map +1 -0
- package/dist/bridge/messages/invalidSandboxMessage.js +13 -0
- package/dist/bridge/messages/listUsers.d.ts +53 -0
- package/dist/bridge/messages/listUsers.d.ts.map +1 -0
- package/dist/bridge/messages/listUsers.js +33 -0
- package/dist/bridge/messages/pageChanged.d.ts +12 -0
- package/dist/bridge/messages/pageChanged.d.ts.map +1 -0
- package/dist/bridge/messages/pageChanged.js +9 -0
- package/dist/bridge/messages/parentChanged.d.ts +22 -0
- package/dist/bridge/messages/parentChanged.d.ts.map +1 -0
- package/dist/bridge/messages/parentChanged.js +9 -0
- package/dist/bridge/messages/queryDataSource.d.ts +20 -0
- package/dist/bridge/messages/queryDataSource.d.ts.map +1 -0
- package/dist/bridge/messages/queryDataSource.js +19 -0
- package/dist/bridge/messages/queryDataSourceResult.d.ts +90 -0
- package/dist/bridge/messages/queryDataSourceResult.d.ts.map +1 -0
- package/dist/bridge/messages/queryDataSourceResult.js +21 -0
- package/dist/bridge/messages/ready.d.ts +45 -0
- package/dist/bridge/messages/ready.d.ts.map +1 -0
- package/dist/bridge/messages/ready.js +37 -0
- package/dist/bridge/messages/resize.d.ts +12 -0
- package/dist/bridge/messages/resize.d.ts.map +1 -0
- package/dist/bridge/messages/resize.js +10 -0
- package/dist/bridge/messages/sandboxToHost.d.ts +410 -0
- package/dist/bridge/messages/sandboxToHost.d.ts.map +1 -0
- package/dist/bridge/messages/sandboxToHost.js +25 -0
- package/dist/bridge/messages/themeChanged.d.ts +11 -0
- package/dist/bridge/messages/themeChanged.d.ts.map +1 -0
- package/dist/bridge/messages/themeChanged.js +10 -0
- package/dist/bridge/messages/updatePage.d.ts +171 -0
- package/dist/bridge/messages/updatePage.d.ts.map +1 -0
- package/dist/bridge/messages/updatePage.js +15 -0
- package/dist/bridge/messages/updatePageResult.d.ts +210 -0
- package/dist/bridge/messages/updatePageResult.d.ts.map +1 -0
- package/dist/bridge/messages/updatePageResult.js +27 -0
- package/dist/bridge/pages/page.d.ts +600 -0
- package/dist/bridge/pages/page.d.ts.map +1 -0
- package/dist/bridge/pages/page.js +217 -0
- package/dist/bridge/parent.d.ts +25 -0
- package/dist/bridge/parent.d.ts.map +1 -0
- package/dist/bridge/parent.js +11 -0
- package/dist/bridge/pendingRequests.d.ts +14 -0
- package/dist/bridge/pendingRequests.d.ts.map +1 -0
- package/dist/bridge/pendingRequests.js +27 -0
- package/dist/bridge/sandboxClient.d.ts +63 -0
- package/dist/bridge/sandboxClient.d.ts.map +1 -0
- package/dist/bridge/sandboxClient.js +92 -0
- package/dist/bridge/theme.d.ts +4 -0
- package/dist/bridge/theme.d.ts.map +1 -0
- package/dist/bridge/theme.js +2 -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/customBlock.d.ts +36 -0
- package/dist/customBlock.d.ts.map +1 -0
- package/dist/customBlock.js +65 -0
- package/dist/host/createCustomBlockHost.d.ts +34 -0
- package/dist/host/createCustomBlockHost.d.ts.map +1 -0
- package/dist/host/createCustomBlockHost.js +278 -0
- package/dist/host/lifecycle/initErrors.d.ts +10 -0
- package/dist/host/lifecycle/initErrors.d.ts.map +1 -0
- package/dist/host/lifecycle/initErrors.js +123 -0
- package/dist/host/lifecycle/ready.d.ts +8 -0
- package/dist/host/lifecycle/ready.d.ts.map +1 -0
- package/dist/host/lifecycle/ready.js +24 -0
- package/dist/host/lifecycle/types.d.ts +24 -0
- package/dist/host/lifecycle/types.d.ts.map +1 -0
- package/dist/host/lifecycle/types.js +1 -0
- package/dist/host/messages/invalidSandboxMessage.d.ts +7 -0
- package/dist/host/messages/invalidSandboxMessage.d.ts.map +1 -0
- package/dist/host/messages/invalidSandboxMessage.js +24 -0
- package/dist/host/messages/types.d.ts +60 -0
- package/dist/host/messages/types.d.ts.map +1 -0
- package/dist/host/messages/types.js +1 -0
- package/dist/host/queries/querySubscriptions.d.ts +20 -0
- package/dist/host/queries/querySubscriptions.d.ts.map +1 -0
- package/dist/host/queries/querySubscriptions.js +29 -0
- package/dist/host/queries/types.d.ts +9 -0
- package/dist/host/queries/types.d.ts.map +1 -0
- package/dist/host/queries/types.js +1 -0
- package/dist/host.d.ts +73 -0
- package/dist/host.d.ts.map +1 -0
- package/dist/host.js +42 -0
- package/dist/index.d.ts +37 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/init.d.ts +53 -0
- package/dist/init.d.ts.map +1 -0
- package/dist/init.js +87 -0
- package/dist/react/DebugMessageLog.d.ts +2 -0
- package/dist/react/DebugMessageLog.d.ts.map +1 -0
- package/dist/react/DebugMessageLog.js +22 -0
- package/dist/react/NotionCustomBlock.css +110 -0
- package/dist/react/NotionCustomBlock.d.ts +50 -0
- package/dist/react/NotionCustomBlock.d.ts.map +1 -0
- package/dist/react/NotionCustomBlock.js +81 -0
- package/dist/react/index.d.ts +14 -0
- package/dist/react/index.d.ts.map +1 -0
- package/dist/react/index.js +13 -0
- package/dist/react/standalonePreview.d.ts +2 -0
- package/dist/react/standalonePreview.d.ts.map +1 -0
- package/dist/react/standalonePreview.js +26 -0
- package/dist/react/useCustomBlockAutoResize.d.ts +30 -0
- package/dist/react/useCustomBlockAutoResize.d.ts.map +1 -0
- package/dist/react/useCustomBlockAutoResize.js +35 -0
- package/dist/react/useCustomBlockInit.d.ts +43 -0
- package/dist/react/useCustomBlockInit.d.ts.map +1 -0
- package/dist/react/useCustomBlockInit.js +60 -0
- package/dist/react/useDataSource.d.ts +13 -0
- package/dist/react/useDataSource.d.ts.map +1 -0
- package/dist/react/useDataSource.js +37 -0
- package/dist/react/useHostState.d.ts +2 -0
- package/dist/react/useHostState.d.ts.map +1 -0
- package/dist/react/useHostState.js +5 -0
- package/dist/react/useRuntimeState.d.ts +66 -0
- package/dist/react/useRuntimeState.d.ts.map +1 -0
- package/dist/react/useRuntimeState.js +73 -0
- package/dist/types.d.ts +154 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/dist/utils.d.ts +9 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +10 -0
- package/dist/version.d.ts +9 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +7 -0
- package/docs/block-location.md +83 -0
- package/docs/data-sources.md +186 -0
- package/docs/lifecycle.md +172 -0
- package/docs/manifest.md +42 -0
- package/docs/pages.md +143 -0
- package/docs/users.md +92 -0
- package/package.json +69 -0
- package/src/autoResize.ts +84 -0
- package/src/bridge/SandboxBridge.ts +921 -0
- package/src/bridge/dataSources/dataSource.ts +63 -0
- package/src/bridge/dataSources/dataSourcePage.ts +71 -0
- package/src/bridge/dataSources/dataSourceValue.ts +19 -0
- package/src/bridge/dataSources/dateValue.ts +96 -0
- package/src/bridge/dataSources/errors.ts +14 -0
- package/src/bridge/dataSources/propertySchema.ts +186 -0
- package/src/bridge/dataSources/recordPointer.ts +13 -0
- package/src/bridge/dataSources/resolve.ts +99 -0
- package/src/bridge/dataSources/resolveProperty.ts +130 -0
- package/src/bridge/errors.ts +13 -0
- package/src/bridge/hostState.ts +154 -0
- package/src/bridge/ids.ts +58 -0
- package/src/bridge/incomingType.ts +19 -0
- package/src/bridge/loadManifest.ts +89 -0
- package/src/bridge/manifest.ts +53 -0
- package/src/bridge/messages/createPage.ts +67 -0
- package/src/bridge/messages/createPageResult.ts +52 -0
- package/src/bridge/messages/currentUserChanged.ts +14 -0
- package/src/bridge/messages/dataSourcesChanged.ts +18 -0
- package/src/bridge/messages/getPage.ts +52 -0
- package/src/bridge/messages/getUser.ts +48 -0
- package/src/bridge/messages/hostToSandbox.ts +37 -0
- package/src/bridge/messages/init.ts +81 -0
- package/src/bridge/messages/invalidHostMessage.ts +16 -0
- package/src/bridge/messages/invalidSandboxMessage.ts +18 -0
- package/src/bridge/messages/listUsers.ts +49 -0
- package/src/bridge/messages/pageChanged.ts +12 -0
- package/src/bridge/messages/parentChanged.ts +14 -0
- package/src/bridge/messages/queryDataSource.ts +24 -0
- package/src/bridge/messages/queryDataSourceResult.ts +38 -0
- package/src/bridge/messages/ready.ts +41 -0
- package/src/bridge/messages/resize.ts +13 -0
- package/src/bridge/messages/sandboxToHost.ts +30 -0
- package/src/bridge/messages/themeChanged.ts +15 -0
- package/src/bridge/messages/updatePage.ts +22 -0
- package/src/bridge/messages/updatePageResult.ts +49 -0
- package/src/bridge/pages/page.ts +281 -0
- package/src/bridge/parent.ts +27 -0
- package/src/bridge/pendingRequests.ts +28 -0
- package/src/bridge/sandboxClient.ts +137 -0
- package/src/bridge/theme.ts +5 -0
- package/src/bridge/users/user.ts +31 -0
- package/src/css.d.ts +1 -0
- package/src/customBlock.ts +102 -0
- package/src/host/createCustomBlockHost.ts +392 -0
- package/src/host/lifecycle/initErrors.ts +132 -0
- package/src/host/lifecycle/ready.ts +36 -0
- package/src/host/lifecycle/types.ts +28 -0
- package/src/host/messages/invalidSandboxMessage.ts +38 -0
- package/src/host/messages/types.ts +67 -0
- package/src/host/queries/querySubscriptions.ts +52 -0
- package/src/host/queries/types.ts +16 -0
- package/src/host.ts +167 -0
- package/src/index.ts +115 -0
- package/src/init.ts +143 -0
- package/src/react/DebugMessageLog.tsx +30 -0
- package/src/react/NotionCustomBlock.css +110 -0
- package/src/react/NotionCustomBlock.tsx +151 -0
- package/src/react/index.ts +28 -0
- package/src/react/standalonePreview.ts +28 -0
- package/src/react/useCustomBlockAutoResize.ts +46 -0
- package/src/react/useCustomBlockInit.ts +91 -0
- package/src/react/useDataSource.ts +47 -0
- package/src/react/useHostState.ts +9 -0
- package/src/react/useRuntimeState.ts +85 -0
- package/src/types.ts +175 -0
- package/src/utils.ts +13 -0
- package/src/version.ts +8 -0
- package/vite-plugin/index.d.ts +46 -0
- package/vite-plugin/index.js +115 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { attach } from "./attach.js";
|
|
2
|
+
import { extractId, formatUuid } from "./ids.js";
|
|
3
|
+
import { ntnApi, ntnApiPatch, ntnApiPost } from "./ntn.js";
|
|
4
|
+
const DEFAULT_PAGE_TITLE = "Custom block";
|
|
5
|
+
function is404(error) {
|
|
6
|
+
return error instanceof Error && error.message.includes("404");
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Classify a `--parent` ID/URL. A database or data source means the user wants
|
|
10
|
+
* a custom *view* under a collection (the parental-nesting flow), which the
|
|
11
|
+
* public API can't express today — see `create` below. Anything else is
|
|
12
|
+
* treated as a page/block to host the new block.
|
|
13
|
+
*/
|
|
14
|
+
function classifyParent(uuid) {
|
|
15
|
+
try {
|
|
16
|
+
const db = ntnApi(`/v1/databases/${uuid}`);
|
|
17
|
+
if (db.object === "database") {
|
|
18
|
+
return "database";
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
if (!is404(error)) {
|
|
23
|
+
throw error;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
const ds = ntnApi(`/v1/data_sources/${uuid}`);
|
|
28
|
+
if (ds.object === "data_source") {
|
|
29
|
+
return "data_source";
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
if (!is404(error)) {
|
|
34
|
+
throw error;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return "page";
|
|
38
|
+
}
|
|
39
|
+
/** Create a private (workspace-level) page to host the new block. */
|
|
40
|
+
function createPrivatePage() {
|
|
41
|
+
console.log("\nCreating a private page to host the block…");
|
|
42
|
+
const page = ntnApiPost("/v1/pages", {
|
|
43
|
+
parent: { workspace: true },
|
|
44
|
+
properties: {
|
|
45
|
+
title: { title: [{ text: { content: DEFAULT_PAGE_TITLE } }] },
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
if (!page.id) {
|
|
49
|
+
throw new Error("Page creation returned no id.");
|
|
50
|
+
}
|
|
51
|
+
const id = formatUuid(page.id);
|
|
52
|
+
console.log(` Created page ${id}`);
|
|
53
|
+
return id;
|
|
54
|
+
}
|
|
55
|
+
/** Append an empty custom block under `pageId` and return its dashed UUID. */
|
|
56
|
+
function createCustomBlock(pageId) {
|
|
57
|
+
console.log(` Creating custom block under ${pageId}…`);
|
|
58
|
+
const result = ntnApiPatch(`/v1/blocks/${pageId}/children`, {
|
|
59
|
+
children: [{ type: "custom_block", custom_block: {} }],
|
|
60
|
+
});
|
|
61
|
+
const created = result.results?.[0];
|
|
62
|
+
if (!created?.id) {
|
|
63
|
+
throw new Error("Custom block creation returned no block id.");
|
|
64
|
+
}
|
|
65
|
+
return formatUuid(created.id);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Create a new custom block and attach it to the worker's definition.
|
|
69
|
+
*
|
|
70
|
+
* - No `--parent` → create a private page, then a block under it.
|
|
71
|
+
* - Page `--parent` → create a block under that page.
|
|
72
|
+
* - Database/data source `--parent` → a custom *view* under a collection. The
|
|
73
|
+
* public API has no `custom` view type and the parent-nesting the backend
|
|
74
|
+
* uses isn't derivable from this repo, so we error rather than guess.
|
|
75
|
+
*/
|
|
76
|
+
export function create(options) {
|
|
77
|
+
let pageId;
|
|
78
|
+
if (!options.parent) {
|
|
79
|
+
pageId = createPrivatePage();
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
const parentUuid = formatUuid(extractId(options.parent));
|
|
83
|
+
const kind = classifyParent(parentUuid);
|
|
84
|
+
if (kind === "database" || kind === "data_source") {
|
|
85
|
+
console.error(`Error: --parent ${parentUuid} is a ${kind.replace("_", " ")}. Creating a custom view under a collection isn't supported yet.\n`);
|
|
86
|
+
console.error(" The public API has no `custom` view type; the backend does the parent nesting. Create the view in Notion, then run `ncblock attach <block-id-or-url>`.");
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
pageId = parentUuid;
|
|
90
|
+
}
|
|
91
|
+
const blockId = createCustomBlock(pageId);
|
|
92
|
+
// Hand off to attach for definition resolution + the PATCH. It re-fetches
|
|
93
|
+
// the block (verifying type) and prints its own success line.
|
|
94
|
+
const attached = attach({
|
|
95
|
+
idOrUrl: blockId,
|
|
96
|
+
capability: options.capability,
|
|
97
|
+
jsonOutput: options.jsonOutput,
|
|
98
|
+
emitResult: false,
|
|
99
|
+
});
|
|
100
|
+
if (options.jsonOutput) {
|
|
101
|
+
console.log(JSON.stringify({ action: "create", page_id: pageId, block_id: attached }, null, "\t"));
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
console.log(`✓ Created custom block ${attached} on page ${pageId}\n`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { resolve as resolvePath } from "node:path";
|
|
3
|
+
import { extractId, formatUuid } from "./ids.js";
|
|
4
|
+
import { ntnApi, ntnApiPatch } from "./ntn.js";
|
|
5
|
+
import { pullManifest } from "./pullManifest.js";
|
|
6
|
+
import { mergeTarget, readTarget, writeTarget } from "./target.js";
|
|
7
|
+
// ── get ─────────────────────────────────────────────────────────
|
|
8
|
+
export function getDataSources(idOrUrl) {
|
|
9
|
+
const uuid = formatUuid(extractId(idOrUrl));
|
|
10
|
+
console.log(`\nFetching block ${uuid}…\n`);
|
|
11
|
+
const block = ntnApi(`/v1/blocks/${uuid}`);
|
|
12
|
+
if (block.type !== "custom_block") {
|
|
13
|
+
console.error(`Error: block ${uuid} is type "${block.type}", not "custom_block".`);
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
const dataSources = block.custom_block?.data_sources ?? [];
|
|
17
|
+
if (dataSources.length === 0) {
|
|
18
|
+
console.log("No data sources bound to this custom block.\n");
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
console.log(JSON.stringify(dataSources, null, "\t"));
|
|
22
|
+
console.log("");
|
|
23
|
+
}
|
|
24
|
+
function patchBlock(idOrUrl, dataSources, jsonOutput) {
|
|
25
|
+
const uuid = formatUuid(extractId(idOrUrl));
|
|
26
|
+
if (!jsonOutput) {
|
|
27
|
+
console.log(`\nUpdating block ${uuid}…\n`);
|
|
28
|
+
}
|
|
29
|
+
const result = ntnApiPatch(`/v1/blocks/${uuid}`, {
|
|
30
|
+
custom_block: { data_sources: dataSources },
|
|
31
|
+
});
|
|
32
|
+
const updated = result.custom_block?.data_sources ?? [];
|
|
33
|
+
if (jsonOutput) {
|
|
34
|
+
console.log(JSON.stringify({ block_id: uuid, data_sources: updated }, null, "\t"));
|
|
35
|
+
}
|
|
36
|
+
else if (updated.length === 0) {
|
|
37
|
+
console.log("✓ Cleared all data source bindings.\n");
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
console.log(`✓ Set ${updated.length} data source binding(s):\n`);
|
|
41
|
+
console.log(JSON.stringify(updated, null, "\t"));
|
|
42
|
+
console.log("");
|
|
43
|
+
}
|
|
44
|
+
return result;
|
|
45
|
+
}
|
|
46
|
+
function targetToMappings(dataSources) {
|
|
47
|
+
return Object.entries(dataSources).map(([key, ds]) => {
|
|
48
|
+
const mapping = {
|
|
49
|
+
key,
|
|
50
|
+
data_source_id: ds.data_source_id,
|
|
51
|
+
};
|
|
52
|
+
if (Object.keys(ds.property_ids_by_key).length > 0) {
|
|
53
|
+
mapping.property_ids_by_key = ds.property_ids_by_key;
|
|
54
|
+
}
|
|
55
|
+
return mapping;
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
function emitDryRun(jsonOutput, payload) {
|
|
59
|
+
if (jsonOutput) {
|
|
60
|
+
console.log(JSON.stringify(payload, null, "\t"));
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
console.log("\nDry run — would PATCH:\n");
|
|
64
|
+
console.log(JSON.stringify(payload, null, "\t"));
|
|
65
|
+
console.log("");
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
export function loadManifest(manifestPath) {
|
|
69
|
+
const path = resolvePath(process.cwd(), manifestPath ?? "custom_blocks.json");
|
|
70
|
+
if (!existsSync(path)) {
|
|
71
|
+
console.error(`Error: manifest not found at ${path}\n`);
|
|
72
|
+
console.error("Run 'ncblock connect <database-url-or-id>' to generate it, or pass --manifest <path>.");
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
let manifest;
|
|
76
|
+
try {
|
|
77
|
+
manifest = JSON.parse(readFileSync(path, "utf-8"));
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
console.error(`Error: failed to parse ${path}\n`);
|
|
81
|
+
console.error("custom_blocks.json must be valid JSON.");
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
return { path, manifest };
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Resolve a manifest data source entry's property IDs by querying the live
|
|
88
|
+
* data source schema and matching declared `name` fields.
|
|
89
|
+
*
|
|
90
|
+
* Notion column names are case-sensitive. Property IDs are URL-encoded in
|
|
91
|
+
* API responses (e.g. `%3EPdk` → `>Pdk`); we decode before storing them.
|
|
92
|
+
*/
|
|
93
|
+
export function resolvePropertyIdsByKey(args) {
|
|
94
|
+
const { dataSourceId, manifestKey, manifestProps } = args;
|
|
95
|
+
const uuid = formatUuid(extractId(dataSourceId));
|
|
96
|
+
const schema = ntnApi(`/v1/data_sources/${uuid}`);
|
|
97
|
+
const schemaProps = schema.properties ?? {};
|
|
98
|
+
// Build a name-keyed index. The API map is keyed by name already, but be
|
|
99
|
+
// defensive and use the `name` field on each entry so we don't depend on
|
|
100
|
+
// the map's key shape.
|
|
101
|
+
const byName = new Map();
|
|
102
|
+
for (const prop of Object.values(schemaProps)) {
|
|
103
|
+
byName.set(prop.name, prop);
|
|
104
|
+
}
|
|
105
|
+
const out = {};
|
|
106
|
+
for (const [propKey, manifestProp] of Object.entries(manifestProps)) {
|
|
107
|
+
const match = byName.get(manifestProp.name);
|
|
108
|
+
if (!match) {
|
|
109
|
+
console.error(`Error: column '${manifestProp.name}' (${manifestProp.type}) not found in data source ${uuid} (manifest key '${manifestKey}').\n` +
|
|
110
|
+
` Either rename a column in Notion to '${manifestProp.name}', or edit .notion/target.json's data_sources.${manifestKey}.property_ids_by_key manually.`);
|
|
111
|
+
process.exit(1);
|
|
112
|
+
}
|
|
113
|
+
if (match.type !== manifestProp.type) {
|
|
114
|
+
console.error(`Error: column '${manifestProp.name}' is type '${match.type}' in the data source, but the manifest declares '${manifestProp.type}'.\n` +
|
|
115
|
+
` Change the Notion column type, or update custom_blocks.json so the types line up.`);
|
|
116
|
+
process.exit(1);
|
|
117
|
+
}
|
|
118
|
+
out[propKey] = decodeURIComponent(match.id);
|
|
119
|
+
}
|
|
120
|
+
return out;
|
|
121
|
+
}
|
|
122
|
+
function buildFromManifest(manifestPath, bindings) {
|
|
123
|
+
const { manifest } = loadManifest(manifestPath);
|
|
124
|
+
const keys = Object.keys(manifest.dataSources ?? {});
|
|
125
|
+
if (keys.length === 0) {
|
|
126
|
+
console.error("Error: manifest has no data source keys.\n");
|
|
127
|
+
console.error("Run 'ncblock connect <database-url-or-id>' to populate custom_blocks.json.");
|
|
128
|
+
process.exit(1);
|
|
129
|
+
}
|
|
130
|
+
const missing = keys.filter(k => !bindings[k]);
|
|
131
|
+
if (missing.length > 0) {
|
|
132
|
+
console.error(`Error: missing --bind for manifest key(s): ${missing.join(", ")}\n`);
|
|
133
|
+
console.error("Use --bind <key>=<data-source-id> for each data source in the manifest.");
|
|
134
|
+
process.exit(1);
|
|
135
|
+
}
|
|
136
|
+
return keys.map(key => {
|
|
137
|
+
const entry = {
|
|
138
|
+
key,
|
|
139
|
+
data_source_id: bindings[key],
|
|
140
|
+
};
|
|
141
|
+
const props = manifest.dataSources[key]?.properties;
|
|
142
|
+
if (props && Object.keys(props).length > 0) {
|
|
143
|
+
entry.property_ids_by_key = resolvePropertyIdsByKey({
|
|
144
|
+
dataSourceId: bindings[key],
|
|
145
|
+
manifestKey: key,
|
|
146
|
+
manifestProps: props,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
return entry;
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
export function connect(options) {
|
|
153
|
+
// When a positional database URL/ID is supplied, pull the schema first.
|
|
154
|
+
// `pullManifest` writes both custom_blocks.json and `.notion/target.json`,
|
|
155
|
+
// so the connect step below just reads from target.json as usual.
|
|
156
|
+
if (options.idOrUrl) {
|
|
157
|
+
pullManifest({
|
|
158
|
+
idOrUrl: options.idOrUrl,
|
|
159
|
+
key: options.key,
|
|
160
|
+
out: options.out,
|
|
161
|
+
dryRun: options.dryRun,
|
|
162
|
+
quiet: options.quiet,
|
|
163
|
+
});
|
|
164
|
+
if (options.dryRun) {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
const existingTarget = readTarget();
|
|
169
|
+
// Bindings come from --bind flags OR from target.json's data_sources
|
|
170
|
+
// (where `manifest pull` stashes the resolved data_source_id). Flags win.
|
|
171
|
+
const bindings = { ...options.bindings };
|
|
172
|
+
if (Object.keys(bindings).length === 0 && existingTarget) {
|
|
173
|
+
for (const [key, ds] of Object.entries(existingTarget.data_sources)) {
|
|
174
|
+
bindings[key] = ds.data_source_id;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
if (Object.keys(bindings).length === 0) {
|
|
178
|
+
console.error("Error: no data sources to connect — pass a database URL/ID as a positional argument, --bind <key>=<data-source-id>, or wire one up with 'ncblock connect <database-url-or-id>'.\n");
|
|
179
|
+
process.exit(1);
|
|
180
|
+
}
|
|
181
|
+
// Blocks: --block flags + whatever's already in target.json. Flags don't
|
|
182
|
+
// "win" here, they accumulate — same as how deploy treats blocks.
|
|
183
|
+
const blockIds = Array.from(new Set([
|
|
184
|
+
...(existingTarget?.block_id ?? []),
|
|
185
|
+
...options.blockIds.map(id => formatUuid(extractId(id))),
|
|
186
|
+
]));
|
|
187
|
+
if (blockIds.length === 0) {
|
|
188
|
+
// When a positional was given, pull already ran and recorded the data
|
|
189
|
+
// source — that's useful on its own (e.g. for a view ID where no
|
|
190
|
+
// custom_block is associated yet). Exit cleanly so init doesn't bail
|
|
191
|
+
// the scaffold; the user wires a block later with --block.
|
|
192
|
+
if (options.idOrUrl) {
|
|
193
|
+
if (!options.jsonOutput) {
|
|
194
|
+
console.log("\nData source recorded. Wire a custom block later with `ncblock connect --block <block-id>`.\n");
|
|
195
|
+
}
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
console.error("Error: no blocks to connect — pass --block <id>, or run 'ncblock connect --block <id>' to add one to target.json.\n");
|
|
199
|
+
process.exit(1);
|
|
200
|
+
}
|
|
201
|
+
const { manifest } = loadManifest(options.manifest);
|
|
202
|
+
const manifestKeys = Object.keys(manifest.dataSources ?? {});
|
|
203
|
+
const unknown = Object.keys(bindings).filter(k => !manifestKeys.includes(k));
|
|
204
|
+
if (unknown.length > 0) {
|
|
205
|
+
console.error(`Error: binding key(s) not declared in the manifest: ${unknown.join(", ")}\n`);
|
|
206
|
+
console.error(`Manifest keys: ${manifestKeys.join(", ") || "(none)"}. Add the key to custom_blocks.json or fix the --bind key.`);
|
|
207
|
+
process.exit(1);
|
|
208
|
+
}
|
|
209
|
+
const resolvedBindings = {};
|
|
210
|
+
for (const [key, dataSourceId] of Object.entries(bindings)) {
|
|
211
|
+
const props = manifest.dataSources[key]?.properties ?? {};
|
|
212
|
+
const property_ids_by_key = Object.keys(props).length > 0
|
|
213
|
+
? resolvePropertyIdsByKey({
|
|
214
|
+
dataSourceId,
|
|
215
|
+
manifestKey: key,
|
|
216
|
+
manifestProps: props,
|
|
217
|
+
})
|
|
218
|
+
: {};
|
|
219
|
+
resolvedBindings[key] = {
|
|
220
|
+
data_source_id: formatUuid(extractId(dataSourceId)),
|
|
221
|
+
property_ids_by_key,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
const merged = mergeTarget(existingTarget, {
|
|
225
|
+
env: options.env,
|
|
226
|
+
block_id: blockIds,
|
|
227
|
+
data_sources: resolvedBindings,
|
|
228
|
+
});
|
|
229
|
+
const mappings = targetToMappings(merged.data_sources);
|
|
230
|
+
if (options.dryRun) {
|
|
231
|
+
emitDryRun(options.jsonOutput, {
|
|
232
|
+
action: "connect",
|
|
233
|
+
target: merged,
|
|
234
|
+
block_patches: blockIds.map(id => ({
|
|
235
|
+
block_id: id,
|
|
236
|
+
data_sources: mappings,
|
|
237
|
+
})),
|
|
238
|
+
});
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
writeTarget(merged);
|
|
242
|
+
if (!options.jsonOutput) {
|
|
243
|
+
console.log(`\n✓ Wrote .notion/target.json`);
|
|
244
|
+
}
|
|
245
|
+
for (const id of blockIds) {
|
|
246
|
+
patchBlock(id, mappings, options.jsonOutput);
|
|
247
|
+
}
|
|
248
|
+
if (options.jsonOutput) {
|
|
249
|
+
console.log(JSON.stringify({
|
|
250
|
+
action: "connect",
|
|
251
|
+
target: merged,
|
|
252
|
+
blocks_updated: blockIds,
|
|
253
|
+
}, null, "\t"));
|
|
254
|
+
}
|
|
255
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { existsSync, statSync } from "node:fs";
|
|
3
|
+
import { resolve as resolvePath } from "node:path";
|
|
4
|
+
import { extractId, formatUuid } from "./ids.js";
|
|
5
|
+
import { mergeTarget, readTarget, writeTarget } from "./target.js";
|
|
6
|
+
/**
|
|
7
|
+
* The `ntn custom deploy` endpoint is feature-gated. When the gate is off,
|
|
8
|
+
* the API returns a 403 with `code: "restricted_resource"` and message
|
|
9
|
+
* `"Endpoint unavailable."` — but the user sees a raw blob and no actionable
|
|
10
|
+
* hint. Match any of the wire fingerprints so we can nudge regardless of
|
|
11
|
+
* which surface ntn echoed.
|
|
12
|
+
*/
|
|
13
|
+
const FEATURE_GATE_PATTERN = /restricted_resource|Endpoint unavailable|403\s*Forbidden/i;
|
|
14
|
+
const FEATURE_GATE_HINT = "Please ensure that the feature gate custom_blocks is enabled.";
|
|
15
|
+
/**
|
|
16
|
+
* Run `ntn custom deploy --block <id> <dist>` for each block. Block IDs come
|
|
17
|
+
* from --block flags if any, otherwise from `.notion/target.json`. On
|
|
18
|
+
* success, --block IDs (and --env) are merged back into target.json so it
|
|
19
|
+
* accumulates deploy targets across runs.
|
|
20
|
+
*
|
|
21
|
+
* This wrapper does NOT build — the package manager is the user's choice.
|
|
22
|
+
*/
|
|
23
|
+
export function deploy(options) {
|
|
24
|
+
const existingTarget = readTarget();
|
|
25
|
+
const flagBlockIds = (options.blockIds ?? []).map(id => formatUuid(extractId(id)));
|
|
26
|
+
const blockIds = flagBlockIds.length > 0 ? flagBlockIds : (existingTarget?.block_id ?? []);
|
|
27
|
+
if (blockIds.length === 0) {
|
|
28
|
+
console.error("Error: no blocks to deploy to — pass --block <id>, or run 'ncblock connect --block <id>' first to populate .notion/target.json.\n");
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
const distPath = resolvePath(process.cwd(), options.distPath);
|
|
32
|
+
if (!existsSync(distPath) || !statSync(distPath).isDirectory()) {
|
|
33
|
+
console.error(`Error: <dist-path> '${options.distPath}' does not exist or is not a directory.\n`);
|
|
34
|
+
console.error("Run your project's build script first (e.g. `npm run build`), then pass the resulting directory.");
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
const env = options.env ?? existingTarget?.env ?? "production";
|
|
38
|
+
if (options.dryRun) {
|
|
39
|
+
const payload = {
|
|
40
|
+
action: "deploy",
|
|
41
|
+
env,
|
|
42
|
+
dist_path: distPath,
|
|
43
|
+
commands: blockIds.map(id => buildDeployCommand(env, id, distPath)),
|
|
44
|
+
};
|
|
45
|
+
if (options.jsonOutput) {
|
|
46
|
+
console.log(JSON.stringify(payload, null, "\t"));
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
console.log("\nDry run — would run:\n");
|
|
50
|
+
for (const cmd of payload.commands) {
|
|
51
|
+
console.log(` ${cmd}`);
|
|
52
|
+
}
|
|
53
|
+
console.log("");
|
|
54
|
+
}
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const results = [];
|
|
58
|
+
for (const id of blockIds) {
|
|
59
|
+
const cmd = buildDeployCommand(env, id, distPath);
|
|
60
|
+
if (!options.jsonOutput) {
|
|
61
|
+
console.log(`\n→ ${cmd}\n`);
|
|
62
|
+
}
|
|
63
|
+
// Capture both streams (rather than inherit) so we can scan for the
|
|
64
|
+
// feature-gate fingerprint and print an actionable hint. We still echo
|
|
65
|
+
// everything back to the user so the live output looks the same.
|
|
66
|
+
const result = spawnSync(cmd, {
|
|
67
|
+
shell: true,
|
|
68
|
+
stdio: ["inherit", "pipe", "pipe"],
|
|
69
|
+
encoding: "utf-8",
|
|
70
|
+
});
|
|
71
|
+
const childStdout = result.stdout ?? "";
|
|
72
|
+
const childStderr = result.stderr ?? "";
|
|
73
|
+
if (!options.jsonOutput) {
|
|
74
|
+
if (childStdout) {
|
|
75
|
+
process.stdout.write(childStdout);
|
|
76
|
+
}
|
|
77
|
+
if (childStderr) {
|
|
78
|
+
process.stderr.write(childStderr);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (result.status !== 0) {
|
|
82
|
+
if (FEATURE_GATE_PATTERN.test(childStdout + childStderr)) {
|
|
83
|
+
process.stderr.write(`\n${FEATURE_GATE_HINT}\n\n`);
|
|
84
|
+
}
|
|
85
|
+
process.exit(result.status ?? 1);
|
|
86
|
+
}
|
|
87
|
+
results.push({ block_id: id, command: cmd });
|
|
88
|
+
}
|
|
89
|
+
// Record the successful deploy back into target.json so subsequent
|
|
90
|
+
// `connect` / `deploy` calls remember where this got shipped.
|
|
91
|
+
const merged = mergeTarget(existingTarget, {
|
|
92
|
+
env: options.env ?? existingTarget?.env,
|
|
93
|
+
block_id: blockIds,
|
|
94
|
+
});
|
|
95
|
+
writeTarget(merged);
|
|
96
|
+
if (options.jsonOutput) {
|
|
97
|
+
console.log(JSON.stringify({ action: "deploy", env, deployed: results, target: merged }, null, "\t"));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function buildDeployCommand(env, blockId, distPath) {
|
|
101
|
+
const prefix = env && env !== "production" ? `NOTION_KEYRING=0 ntn --env ${env}` : "ntn";
|
|
102
|
+
return `${prefix} custom deploy --block ${blockId} ${shellQuote(distPath)}`;
|
|
103
|
+
}
|
|
104
|
+
function shellQuote(s) {
|
|
105
|
+
if (!/[\s"'`$\\]/.test(s)) {
|
|
106
|
+
return s;
|
|
107
|
+
}
|
|
108
|
+
return `'${s.replace(/'/g, "'\\''")}'`;
|
|
109
|
+
}
|
package/bin/cli/ids.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Extract a 32-char hex ID from a Notion URL or raw UUID. */
|
|
2
|
+
export function extractId(input) {
|
|
3
|
+
const match = input.match(/([a-f0-9]{32}|[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})/);
|
|
4
|
+
if (match) {
|
|
5
|
+
return match[1].replace(/-/g, "");
|
|
6
|
+
}
|
|
7
|
+
return input.replace(/-/g, "");
|
|
8
|
+
}
|
|
9
|
+
/** Format a 32-char hex string as a dashed UUID. */
|
|
10
|
+
export function formatUuid(raw) {
|
|
11
|
+
const h = raw.replace(/-/g, "");
|
|
12
|
+
return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20)}`;
|
|
13
|
+
}
|
package/bin/cli/ntn.js
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
import { readTarget } from "./target.js";
|
|
3
|
+
/**
|
|
4
|
+
* Resolve the active Notion env. An explicit `ENV=` always wins — that's the
|
|
5
|
+
* escape hatch for "I know what target.json says, but run this one against X".
|
|
6
|
+
* Otherwise target.json is the source of truth (every command merges into it,
|
|
7
|
+
* and it's checked in alongside the project).
|
|
8
|
+
*/
|
|
9
|
+
function resolveEnv() {
|
|
10
|
+
return process.env.ENV ?? readTarget()?.env;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Shell command we nudge users toward when an `ntn api` call fails in a way
|
|
14
|
+
* that suggests they're not logged in (404 on a known-public resource, missing
|
|
15
|
+
* data sources on a real database). Mirrors `buildCommand`'s env handling.
|
|
16
|
+
*/
|
|
17
|
+
export function loginHintCommand() {
|
|
18
|
+
const env = resolveEnv();
|
|
19
|
+
if (env && env !== "production") {
|
|
20
|
+
return `NOTION_KEYRING=0 ntn --env ${env} login`;
|
|
21
|
+
}
|
|
22
|
+
return "ntn login";
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* The `ntn` invocation prefix with correct ENV handling.
|
|
26
|
+
*
|
|
27
|
+
* - env is unset or "production" → `ntn` (no keyring override)
|
|
28
|
+
* - Any other env value → `NOTION_KEYRING=0 ntn --env <env>`
|
|
29
|
+
*/
|
|
30
|
+
function ntnPrefix() {
|
|
31
|
+
const env = resolveEnv();
|
|
32
|
+
if (env && env !== "production") {
|
|
33
|
+
return `NOTION_KEYRING=0 ntn --env ${env}`;
|
|
34
|
+
}
|
|
35
|
+
return "ntn";
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Build the `ntn api` command with correct ENV handling.
|
|
39
|
+
*
|
|
40
|
+
* - env is unset or "production" → `ntn api <path>` (no keyring override)
|
|
41
|
+
* - Any other env value → `NOTION_KEYRING=0 ntn --env <env> api <path>`
|
|
42
|
+
*/
|
|
43
|
+
function buildCommand(apiPath) {
|
|
44
|
+
return `${ntnPrefix()} api ${apiPath}`;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Call `ntn api <path>` and return the parsed JSON response.
|
|
48
|
+
* Throws on non-zero exit or invalid JSON.
|
|
49
|
+
*/
|
|
50
|
+
export function ntnApi(apiPath) {
|
|
51
|
+
const cmd = buildCommand(apiPath);
|
|
52
|
+
return run(cmd, apiPath);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Call `ntn api -X PATCH <path> -d <json>` and return the parsed JSON response.
|
|
56
|
+
* Throws on non-zero exit or invalid JSON.
|
|
57
|
+
*/
|
|
58
|
+
export function ntnApiPatch(apiPath, body) {
|
|
59
|
+
const base = buildCommand(apiPath);
|
|
60
|
+
const cmd = `${base} -X PATCH -d ${shellEscape(JSON.stringify(body))}`;
|
|
61
|
+
return run(cmd, apiPath);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Call `ntn api -X POST <path> -d <json>` and return the parsed JSON response.
|
|
65
|
+
* Throws on non-zero exit or invalid JSON.
|
|
66
|
+
*/
|
|
67
|
+
export function ntnApiPost(apiPath, body) {
|
|
68
|
+
const base = buildCommand(apiPath);
|
|
69
|
+
const cmd = `${base} -X POST -d ${shellEscape(JSON.stringify(body))}`;
|
|
70
|
+
return run(cmd, apiPath);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Call `ntn workers capabilities list --json` and return the parsed JSON.
|
|
74
|
+
* Throws on non-zero exit or invalid JSON. Reads the worker ID from the local
|
|
75
|
+
* workers.json (same as the bare `ntn` command would).
|
|
76
|
+
*/
|
|
77
|
+
export function ntnWorkersCapabilities() {
|
|
78
|
+
const cmd = `${ntnPrefix()} workers capabilities list --json`;
|
|
79
|
+
return run(cmd, "workers capabilities list");
|
|
80
|
+
}
|
|
81
|
+
function shellEscape(s) {
|
|
82
|
+
return `'${s.replace(/'/g, "'\\''")}'`;
|
|
83
|
+
}
|
|
84
|
+
function run(cmd, label) {
|
|
85
|
+
try {
|
|
86
|
+
const output = execSync(cmd, { encoding: "utf-8", stdio: "pipe" });
|
|
87
|
+
return JSON.parse(output);
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
const stderr = error instanceof Error && "stderr" in error
|
|
91
|
+
? error.stderr
|
|
92
|
+
: "";
|
|
93
|
+
throw new Error(`ntn api ${label} failed${stderr ? `:\n${stderr.toString().trim()}` : ""}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { writeFileSync } from "node:fs";
|
|
2
|
+
import { resolve as resolvePath } from "node:path";
|
|
3
|
+
import { extractId, formatUuid } from "./ids.js";
|
|
4
|
+
import { loginHintCommand, ntnApi, ntnApiPost } from "./ntn.js";
|
|
5
|
+
const DEFAULT_LIMIT = 25;
|
|
6
|
+
// The public data source query endpoint caps page_size at 100.
|
|
7
|
+
const MAX_PAGE_SIZE = 100;
|
|
8
|
+
function is404(error) {
|
|
9
|
+
return error instanceof Error && error.message.includes("404");
|
|
10
|
+
}
|
|
11
|
+
function plainText(items) {
|
|
12
|
+
if (!items?.length) {
|
|
13
|
+
return "Data source";
|
|
14
|
+
}
|
|
15
|
+
return items.map(t => t.plain_text ?? "").join("") || "Data source";
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Resolve an ID/URL to a data source ID. Tries `/v1/databases/<id>` first
|
|
19
|
+
* (walks to its first data source), falling back to treating the ID as a data
|
|
20
|
+
* source directly. Mirrors the resolution `pullManifest`/`classifyId` use.
|
|
21
|
+
*/
|
|
22
|
+
function resolveDataSourceId(uuid) {
|
|
23
|
+
try {
|
|
24
|
+
console.error(` Resolving database ${uuid}…`);
|
|
25
|
+
const db = ntnApi(`/v1/databases/${uuid}`);
|
|
26
|
+
if (db.object === "database") {
|
|
27
|
+
if (!db.data_sources?.length) {
|
|
28
|
+
throw new Error(`Database ${uuid} has no data sources. Confirm you're logged in to the right workspace with \`${loginHintCommand()}\`.`);
|
|
29
|
+
}
|
|
30
|
+
return formatUuid(db.data_sources[0].id);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
if (!is404(error)) {
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
// Not a database — assume it already points at a data source. We don't GET
|
|
39
|
+
// it here; the schema fetch below will 404 clearly if it isn't one.
|
|
40
|
+
return uuid;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Resolve an ID/URL to a data source, then emit its property schema plus the
|
|
44
|
+
* first N rows as JSON. JSON goes to stdout (pipeable) unless `--out` is set;
|
|
45
|
+
* progress chatter goes to stderr so it never pollutes the payload.
|
|
46
|
+
*/
|
|
47
|
+
export function pullData(options) {
|
|
48
|
+
const uuid = formatUuid(extractId(options.idOrUrl));
|
|
49
|
+
const limit = Math.min(Math.max(1, options.limit ?? DEFAULT_LIMIT), MAX_PAGE_SIZE);
|
|
50
|
+
console.error(`\nResolving ${options.idOrUrl}…`);
|
|
51
|
+
const dataSourceId = resolveDataSourceId(uuid);
|
|
52
|
+
console.error(` Fetching schema for data source ${dataSourceId}…`);
|
|
53
|
+
const schema = ntnApi(`/v1/data_sources/${dataSourceId}`);
|
|
54
|
+
const properties = schema.properties ?? {};
|
|
55
|
+
console.error(` Querying ${limit} row(s)…`);
|
|
56
|
+
const query = ntnApiPost(`/v1/data_sources/${dataSourceId}/query`, {
|
|
57
|
+
page_size: limit,
|
|
58
|
+
});
|
|
59
|
+
const rows = query.results ?? [];
|
|
60
|
+
const payload = {
|
|
61
|
+
data_source_id: dataSourceId,
|
|
62
|
+
name: plainText(schema.title),
|
|
63
|
+
properties,
|
|
64
|
+
row_count: rows.length,
|
|
65
|
+
has_more: query.has_more ?? false,
|
|
66
|
+
rows,
|
|
67
|
+
};
|
|
68
|
+
const json = JSON.stringify(payload, null, "\t") + "\n";
|
|
69
|
+
if (options.out) {
|
|
70
|
+
const outPath = resolvePath(process.cwd(), options.out);
|
|
71
|
+
writeFileSync(outPath, json);
|
|
72
|
+
console.error(`\n✓ Wrote ${rows.length} row(s) from "${payload.name}" to ${options.out}\n`);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
process.stdout.write(json);
|
|
76
|
+
}
|