@notionhq/custom-blocks 0.0.78 → 0.1.0
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 +9 -2
- package/README.md +4 -2
- package/dist/bridge/SandboxBridge.d.ts +1 -1
- package/dist/bridge/SandboxBridge.js +1 -1
- package/dist/bridge/messages/connect.d.ts +1 -1
- package/dist/bridge/messages/connect.d.ts.map +1 -1
- package/dist/bridge/messages/connect.js +2 -8
- package/dist/bridge/messages/sandboxToHost.d.ts +1 -1
- package/dist/host/createCustomBlockHost.d.ts +1 -1
- package/dist/host/createCustomBlockHost.d.ts.map +1 -1
- package/dist/host/createCustomBlockHost.js +17 -4
- package/dist/host/lifecycle/{ready.d.ts → protocolVersion.d.ts} +1 -1
- package/dist/host/lifecycle/protocolVersion.d.ts.map +1 -0
- package/dist/version.js +1 -1
- package/docs/data-sources.md +7 -5
- package/docs/vite-plugin.md +46 -0
- package/package.json +1 -2
- package/src/bridge/SandboxBridge.ts +1 -1
- package/src/bridge/messages/connect.ts +2 -9
- package/src/host/createCustomBlockHost.ts +25 -5
- package/bin/cli/attach.js +0 -180
- package/bin/cli/cli.js +0 -248
- package/bin/cli/create.js +0 -106
- package/bin/cli/datasources.js +0 -255
- package/bin/cli/deploy.js +0 -109
- package/bin/cli/ids.js +0 -13
- package/bin/cli/ntn.js +0 -95
- package/bin/cli/pullData.js +0 -76
- package/bin/cli/pullManifest.js +0 -158
- package/bin/cli/target.js +0 -95
- package/bin/src/bridge/dataSources/propertySchema.js +0 -148
- package/bin/src/bridge/manifest.js +0 -40
- package/dist/host/lifecycle/ready.d.ts.map +0 -1
- package/docs/manifest.md +0 -42
- /package/dist/host/lifecycle/{ready.js → protocolVersion.js} +0 -0
- /package/src/host/lifecycle/{ready.ts → protocolVersion.ts} +0 -0
package/bin/cli/cli.js
DELETED
|
@@ -1,248 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { attach } from "./attach.js";
|
|
3
|
-
import { create } from "./create.js";
|
|
4
|
-
import { connect, getDataSources } from "./datasources.js";
|
|
5
|
-
import { deploy } from "./deploy.js";
|
|
6
|
-
import { pullData } from "./pullData.js";
|
|
7
|
-
import { pullManifest } from "./pullManifest.js";
|
|
8
|
-
const HELP = `
|
|
9
|
-
ncblock — Notion Custom Block SDK CLI
|
|
10
|
-
|
|
11
|
-
Usage:
|
|
12
|
-
ncblock connect <database-url-or-id> [options]
|
|
13
|
-
ncblock connect [--block <id>] [--bind <key>=<ds-id>] [options]
|
|
14
|
-
ncblock data_sources get <block-id-or-url>
|
|
15
|
-
ncblock deploy <dist-path> [--block <id>] [options]
|
|
16
|
-
ncblock pull_data <id-or-url> [--limit <n>] [--out <path>]
|
|
17
|
-
ncblock attach <block-id-or-url> [--capability <key>]
|
|
18
|
-
ncblock create [--parent <id-or-url>] [--capability <key>]
|
|
19
|
-
|
|
20
|
-
Commands:
|
|
21
|
-
connect Pull the data source schema and PATCH the block(s) in .notion/target.json
|
|
22
|
-
data_sources get Read data source bindings from a custom block
|
|
23
|
-
deploy Run \`ntn custom deploy\` for each block; appends --block IDs into target.json on success
|
|
24
|
-
pull_data Resolve an ID/URL to a data source; print its schema + N rows as JSON to stdout
|
|
25
|
-
attach Point a custom block at the worker's definition (from \`ntn workers capabilities\`)
|
|
26
|
-
create Create a custom block (private page parent by default) and attach it
|
|
27
|
-
|
|
28
|
-
Options:
|
|
29
|
-
--block <id> Block ID/URL (repeatable; accumulates into target.json's block_id[])
|
|
30
|
-
--bind <key>=<id> Bind a manifest key to a data source ID (repeatable)
|
|
31
|
-
--capability <key> customBlock capability key to attach (required if the worker has more than one)
|
|
32
|
-
--parent <id> Parent ID/URL for \`create\` (a page hosts the block; omit for a new private page)
|
|
33
|
-
--limit <n> Rows to pull for \`pull_data\` (default: 25, max: 100)
|
|
34
|
-
--env <name> Notion env (defaults to target.json's env, or "production")
|
|
35
|
-
--key <key> Data source key in manifest (default: "default") — for connect <id> only
|
|
36
|
-
--out <path> Output path — manifest for connect, JSON file for pull_data
|
|
37
|
-
--manifest <path> Path to custom_blocks.json (default: "./custom_blocks.json")
|
|
38
|
-
--dry-run Print the intended action without performing it
|
|
39
|
-
--json Emit machine-parseable JSON output
|
|
40
|
-
--help, -h Show this help message
|
|
41
|
-
|
|
42
|
-
Examples:
|
|
43
|
-
ncblock connect <db-url>
|
|
44
|
-
ncblock connect --block <block-id> --bind default=<db-id>
|
|
45
|
-
ncblock data_sources get <block-id>
|
|
46
|
-
ncblock deploy dist/
|
|
47
|
-
ncblock pull_data <db-url> --limit 50 --out sample.json
|
|
48
|
-
ncblock attach <block-url> --capability hello
|
|
49
|
-
ncblock create
|
|
50
|
-
`;
|
|
51
|
-
function parseArgs(argv) {
|
|
52
|
-
const flags = {};
|
|
53
|
-
const bindings = {};
|
|
54
|
-
const blocks = [];
|
|
55
|
-
const positional = [];
|
|
56
|
-
let i = 2;
|
|
57
|
-
while (i < argv.length) {
|
|
58
|
-
const arg = argv[i];
|
|
59
|
-
if (arg === "--key") {
|
|
60
|
-
flags.key = argv[++i];
|
|
61
|
-
}
|
|
62
|
-
else if (arg === "--out") {
|
|
63
|
-
flags.out = argv[++i];
|
|
64
|
-
}
|
|
65
|
-
else if (arg === "--dry-run") {
|
|
66
|
-
flags.dryRun = true;
|
|
67
|
-
}
|
|
68
|
-
else if (arg === "--json") {
|
|
69
|
-
flags.jsonOutput = true;
|
|
70
|
-
}
|
|
71
|
-
else if (arg === "--quiet") {
|
|
72
|
-
flags.quiet = true;
|
|
73
|
-
}
|
|
74
|
-
else if (arg === "--manifest") {
|
|
75
|
-
flags.manifest = argv[++i];
|
|
76
|
-
}
|
|
77
|
-
else if (arg === "--capability") {
|
|
78
|
-
flags.capability = argv[++i];
|
|
79
|
-
}
|
|
80
|
-
else if (arg === "--parent") {
|
|
81
|
-
flags.parent = argv[++i];
|
|
82
|
-
}
|
|
83
|
-
else if (arg === "--limit") {
|
|
84
|
-
flags.limit = argv[++i];
|
|
85
|
-
}
|
|
86
|
-
else if (arg === "--env") {
|
|
87
|
-
flags.env = argv[++i];
|
|
88
|
-
}
|
|
89
|
-
else if (arg === "--block") {
|
|
90
|
-
const value = argv[++i];
|
|
91
|
-
if (!value) {
|
|
92
|
-
console.error("Error: --block requires a value.\n");
|
|
93
|
-
console.error("Usage: --block <id-or-url>");
|
|
94
|
-
process.exit(1);
|
|
95
|
-
}
|
|
96
|
-
blocks.push(value);
|
|
97
|
-
}
|
|
98
|
-
else if (arg === "--bind") {
|
|
99
|
-
const pair = argv[++i];
|
|
100
|
-
const eq = pair?.indexOf("=");
|
|
101
|
-
if (!pair || eq === undefined || eq < 1) {
|
|
102
|
-
console.error(`Error: invalid --bind value: ${pair}\n`);
|
|
103
|
-
console.error("Expected format: --bind <key>=<data-source-id>");
|
|
104
|
-
process.exit(1);
|
|
105
|
-
}
|
|
106
|
-
bindings[pair.slice(0, eq)] = pair.slice(eq + 1);
|
|
107
|
-
}
|
|
108
|
-
else if (arg === "--help" || arg === "-h") {
|
|
109
|
-
flags.help = true;
|
|
110
|
-
}
|
|
111
|
-
else if (!arg.startsWith("-")) {
|
|
112
|
-
positional.push(arg);
|
|
113
|
-
}
|
|
114
|
-
i++;
|
|
115
|
-
}
|
|
116
|
-
return { flags, bindings, blocks, positional };
|
|
117
|
-
}
|
|
118
|
-
function main() {
|
|
119
|
-
const { flags, bindings, blocks, positional } = parseArgs(process.argv);
|
|
120
|
-
if (flags.help || positional.length === 0) {
|
|
121
|
-
console.log(HELP);
|
|
122
|
-
process.exit(0);
|
|
123
|
-
}
|
|
124
|
-
const [command, subcommand, ...rest] = positional;
|
|
125
|
-
// `manifest pull` is kept as an internal/advanced command — not advertised
|
|
126
|
-
// in HELP, but still callable for users who want pull without the PATCH
|
|
127
|
-
// step. Prefer `ncblock connect <id>`.
|
|
128
|
-
if (command === "manifest" && subcommand === "pull") {
|
|
129
|
-
const idOrUrl = rest[0];
|
|
130
|
-
if (!idOrUrl) {
|
|
131
|
-
console.error("Error: missing <id-or-url> argument.\n");
|
|
132
|
-
console.error("Usage: ncblock manifest pull <id-or-url> [options]");
|
|
133
|
-
process.exit(1);
|
|
134
|
-
}
|
|
135
|
-
pullManifest({
|
|
136
|
-
idOrUrl,
|
|
137
|
-
key: flags.key,
|
|
138
|
-
out: flags.out,
|
|
139
|
-
dryRun: flags.dryRun,
|
|
140
|
-
quiet: flags.quiet,
|
|
141
|
-
});
|
|
142
|
-
return;
|
|
143
|
-
}
|
|
144
|
-
if (command === "connect") {
|
|
145
|
-
// `ncblock connect <database-url-or-id>` pulls the schema first then
|
|
146
|
-
// runs the binding/PATCH flow against target.json. The positional
|
|
147
|
-
// must point at a database (or a data source); ID-kind classification
|
|
148
|
-
// (block vs view vs wrong-workspace) happens upstream in `init.ts`.
|
|
149
|
-
connect({
|
|
150
|
-
idOrUrl: subcommand,
|
|
151
|
-
blockIds: blocks,
|
|
152
|
-
bindings,
|
|
153
|
-
key: flags.key,
|
|
154
|
-
out: flags.out,
|
|
155
|
-
manifest: flags.manifest,
|
|
156
|
-
env: flags.env,
|
|
157
|
-
dryRun: flags.dryRun,
|
|
158
|
-
jsonOutput: flags.jsonOutput,
|
|
159
|
-
quiet: flags.quiet,
|
|
160
|
-
});
|
|
161
|
-
return;
|
|
162
|
-
}
|
|
163
|
-
if (command === "data_sources" && subcommand === "get") {
|
|
164
|
-
const idOrUrl = rest[0];
|
|
165
|
-
if (!idOrUrl) {
|
|
166
|
-
console.error("Error: missing <block-id-or-url> argument.\n");
|
|
167
|
-
console.error("Usage: ncblock data_sources get <block-id-or-url>");
|
|
168
|
-
process.exit(1);
|
|
169
|
-
}
|
|
170
|
-
getDataSources(idOrUrl);
|
|
171
|
-
return;
|
|
172
|
-
}
|
|
173
|
-
if (command === "deploy") {
|
|
174
|
-
const distPath = subcommand;
|
|
175
|
-
if (!distPath) {
|
|
176
|
-
console.error("Error: missing <dist-path> argument.\n");
|
|
177
|
-
console.error("Usage: ncblock deploy <dist-path> [--block <id>...] [--env <name>]");
|
|
178
|
-
process.exit(1);
|
|
179
|
-
}
|
|
180
|
-
deploy({
|
|
181
|
-
distPath,
|
|
182
|
-
blockIds: blocks,
|
|
183
|
-
env: flags.env,
|
|
184
|
-
dryRun: flags.dryRun,
|
|
185
|
-
jsonOutput: flags.jsonOutput,
|
|
186
|
-
});
|
|
187
|
-
return;
|
|
188
|
-
}
|
|
189
|
-
// The commands below route their API calls through ntn.ts, which resolves
|
|
190
|
-
// the env from `process.env.ENV` (falling back to target.json). Honor an
|
|
191
|
-
// explicit --env by setting it here so every ntn call inherits it.
|
|
192
|
-
if (flags.env) {
|
|
193
|
-
process.env.ENV = flags.env;
|
|
194
|
-
}
|
|
195
|
-
if (command === "pull_data") {
|
|
196
|
-
const idOrUrl = subcommand;
|
|
197
|
-
if (!idOrUrl) {
|
|
198
|
-
console.error("Error: missing <id-or-url> argument.\n");
|
|
199
|
-
console.error("Usage: ncblock pull_data <id-or-url> [--limit <n>] [--out <path>]");
|
|
200
|
-
process.exit(1);
|
|
201
|
-
}
|
|
202
|
-
const limitRaw = flags.limit;
|
|
203
|
-
const limit = limitRaw === undefined ? undefined : Number(limitRaw);
|
|
204
|
-
if (limit !== undefined && !Number.isFinite(limit)) {
|
|
205
|
-
console.error(`Error: --limit must be a number, got '${limitRaw}'.`);
|
|
206
|
-
process.exit(1);
|
|
207
|
-
}
|
|
208
|
-
pullData({
|
|
209
|
-
idOrUrl,
|
|
210
|
-
limit,
|
|
211
|
-
out: flags.out,
|
|
212
|
-
});
|
|
213
|
-
return;
|
|
214
|
-
}
|
|
215
|
-
if (command === "attach") {
|
|
216
|
-
const idOrUrl = subcommand;
|
|
217
|
-
if (!idOrUrl) {
|
|
218
|
-
console.error("Error: missing <block-id-or-url> argument.\n");
|
|
219
|
-
console.error("Usage: ncblock attach <block-id-or-url> [--capability <key>]");
|
|
220
|
-
process.exit(1);
|
|
221
|
-
}
|
|
222
|
-
attach({
|
|
223
|
-
idOrUrl,
|
|
224
|
-
capability: flags.capability,
|
|
225
|
-
jsonOutput: flags.jsonOutput,
|
|
226
|
-
});
|
|
227
|
-
return;
|
|
228
|
-
}
|
|
229
|
-
if (command === "create") {
|
|
230
|
-
create({
|
|
231
|
-
parent: flags.parent,
|
|
232
|
-
capability: flags.capability,
|
|
233
|
-
jsonOutput: flags.jsonOutput,
|
|
234
|
-
});
|
|
235
|
-
return;
|
|
236
|
-
}
|
|
237
|
-
console.error(`Unknown command: ${positional.join(" ")}\n`);
|
|
238
|
-
console.log(HELP);
|
|
239
|
-
process.exit(1);
|
|
240
|
-
}
|
|
241
|
-
try {
|
|
242
|
-
main();
|
|
243
|
-
}
|
|
244
|
-
catch (error) {
|
|
245
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
246
|
-
console.error(`\nError: ${message}\n`);
|
|
247
|
-
process.exit(1);
|
|
248
|
-
}
|
package/bin/cli/create.js
DELETED
|
@@ -1,106 +0,0 @@
|
|
|
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 block as a collection view. 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 block as a collection view isn't supported yet.\n`);
|
|
86
|
-
console.error(" The public API has no `custom` view type; the backend does the parent nesting. Create the custom block 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
|
-
}
|
package/bin/cli/datasources.js
DELETED
|
@@ -1,255 +0,0 @@
|
|
|
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
|
-
}
|