@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/deploy.js
DELETED
|
@@ -1,109 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,95 +0,0 @@
|
|
|
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
|
-
}
|
package/bin/cli/pullData.js
DELETED
|
@@ -1,76 +0,0 @@
|
|
|
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
|
-
}
|
package/bin/cli/pullManifest.js
DELETED
|
@@ -1,158 +0,0 @@
|
|
|
1
|
-
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { resolve as resolvePath } from "node:path";
|
|
3
|
-
import { NOTION_BUILTIN_PROPERTY_IDS } from "../src/bridge/dataSources/propertySchema.js";
|
|
4
|
-
import { extractId, formatUuid } from "./ids.js";
|
|
5
|
-
import { loginHintCommand, ntnApi } from "./ntn.js";
|
|
6
|
-
import { mergeTarget, readTarget, writeTarget } from "./target.js";
|
|
7
|
-
const MANIFEST_FILENAME = "custom_blocks.json";
|
|
8
|
-
const BUILTIN_TYPES = new Set(NOTION_BUILTIN_PROPERTY_IDS);
|
|
9
|
-
// ── Helpers ───────────────────────────────────────────────────────
|
|
10
|
-
function deriveKey(name) {
|
|
11
|
-
return name
|
|
12
|
-
.toLowerCase()
|
|
13
|
-
.replace(/\s+/g, "_")
|
|
14
|
-
.replace(/[^a-z0-9_]/g, "");
|
|
15
|
-
}
|
|
16
|
-
function plainText(items) {
|
|
17
|
-
if (!items?.length) {
|
|
18
|
-
return "Data source";
|
|
19
|
-
}
|
|
20
|
-
return items.map(t => t.plain_text ?? "").join("") || "Data source";
|
|
21
|
-
}
|
|
22
|
-
function is404(error) {
|
|
23
|
-
return error instanceof Error && error.message.includes("404");
|
|
24
|
-
}
|
|
25
|
-
// ── Schema transform ─────────────────────────────────────────────
|
|
26
|
-
function transformProperties(props) {
|
|
27
|
-
const out = {};
|
|
28
|
-
for (const prop of Object.values(props)) {
|
|
29
|
-
if (BUILTIN_TYPES.has(prop.type)) {
|
|
30
|
-
continue;
|
|
31
|
-
}
|
|
32
|
-
const key = deriveKey(prop.name);
|
|
33
|
-
if (key) {
|
|
34
|
-
out[key] = { name: prop.name, type: prop.type };
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
return out;
|
|
38
|
-
}
|
|
39
|
-
function fetchDataSource(dataSourceId, quiet) {
|
|
40
|
-
const id = formatUuid(dataSourceId);
|
|
41
|
-
if (!quiet) {
|
|
42
|
-
console.log(` Fetching data source ${id}…`);
|
|
43
|
-
}
|
|
44
|
-
const ds = ntnApi(`/v1/data_sources/${id}`);
|
|
45
|
-
if (!ds.properties || Object.keys(ds.properties).length === 0) {
|
|
46
|
-
throw new Error(`Data source ${id} has no properties.`);
|
|
47
|
-
}
|
|
48
|
-
return {
|
|
49
|
-
dataSourceId: id,
|
|
50
|
-
name: plainText(ds.title),
|
|
51
|
-
properties: transformProperties(ds.properties),
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
/**
|
|
55
|
-
* Pull schema from a database or data source ID. Tries `/v1/databases/<id>`
|
|
56
|
-
* first (most common entry point); falls back to `/v1/data_sources/<id>` if
|
|
57
|
-
* that 404s. ID-type classification (block / view / database / wrong-workspace)
|
|
58
|
-
* happens upstream in `init.ts` — by the time we get here, the ID is expected
|
|
59
|
-
* to point at a database or data source.
|
|
60
|
-
*/
|
|
61
|
-
function fetchSchema(id, quiet) {
|
|
62
|
-
const uuid = formatUuid(id);
|
|
63
|
-
let dbError;
|
|
64
|
-
try {
|
|
65
|
-
if (!quiet) {
|
|
66
|
-
console.log(` Fetching database ${uuid}…`);
|
|
67
|
-
}
|
|
68
|
-
const db = ntnApi(`/v1/databases/${uuid}`);
|
|
69
|
-
if (db.object === "database") {
|
|
70
|
-
if (!db.data_sources?.length) {
|
|
71
|
-
throw new Error(`Database ${uuid} has no data sources. Confirm you're logged in to the right workspace with \`${loginHintCommand()}\`.`);
|
|
72
|
-
}
|
|
73
|
-
return fetchDataSource(db.data_sources[0].id, quiet);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
catch (error) {
|
|
77
|
-
if (!is404(error)) {
|
|
78
|
-
throw error;
|
|
79
|
-
}
|
|
80
|
-
dbError = error;
|
|
81
|
-
}
|
|
82
|
-
try {
|
|
83
|
-
return fetchDataSource(uuid, quiet);
|
|
84
|
-
}
|
|
85
|
-
catch (error) {
|
|
86
|
-
if (!is404(error)) {
|
|
87
|
-
throw error;
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
throw new Error(dbError instanceof Error
|
|
91
|
-
? `${uuid} isn't a database or data source. Confirm you're logged in to the right workspace with \`${loginHintCommand()}\`.`
|
|
92
|
-
: `${uuid} isn't a database or data source.`);
|
|
93
|
-
}
|
|
94
|
-
export function pullManifest(options) {
|
|
95
|
-
const { idOrUrl, key = "default", out = MANIFEST_FILENAME, dryRun = false, quiet = false, } = options;
|
|
96
|
-
const outPath = resolvePath(process.cwd(), out);
|
|
97
|
-
const id = extractId(idOrUrl);
|
|
98
|
-
if (quiet) {
|
|
99
|
-
console.log("\nPulling data source schema…");
|
|
100
|
-
}
|
|
101
|
-
else {
|
|
102
|
-
console.log(`\nResolving ${idOrUrl}…\n`);
|
|
103
|
-
}
|
|
104
|
-
const result = fetchSchema(id, quiet);
|
|
105
|
-
const entry = {
|
|
106
|
-
name: result.name,
|
|
107
|
-
properties: result.properties,
|
|
108
|
-
};
|
|
109
|
-
// Read existing manifest or start fresh. The entry for `key` is fully
|
|
110
|
-
// replaced so stale properties are automatically removed.
|
|
111
|
-
let manifest;
|
|
112
|
-
if (existsSync(outPath)) {
|
|
113
|
-
try {
|
|
114
|
-
const existing = JSON.parse(readFileSync(outPath, "utf-8"));
|
|
115
|
-
manifest = { version: 1, dataSources: existing.dataSources ?? {} };
|
|
116
|
-
}
|
|
117
|
-
catch {
|
|
118
|
-
manifest = { version: 1, dataSources: {} };
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
else {
|
|
122
|
-
manifest = { version: 1, dataSources: {} };
|
|
123
|
-
}
|
|
124
|
-
manifest.dataSources[key] = entry;
|
|
125
|
-
const json = JSON.stringify(manifest, null, "\t") + "\n";
|
|
126
|
-
if (dryRun) {
|
|
127
|
-
console.log(json);
|
|
128
|
-
return;
|
|
129
|
-
}
|
|
130
|
-
writeFileSync(outPath, json);
|
|
131
|
-
// Stash the resolved data-source ID in .notion/target.json so `connect`
|
|
132
|
-
// can wire the block without the user re-typing it. Don't touch block_id
|
|
133
|
-
// or property_ids_by_key — init seeds block_id at scaffold time, and
|
|
134
|
-
// `connect` fills in property_ids_by_key.
|
|
135
|
-
const existingTarget = readTarget();
|
|
136
|
-
const existingEntry = existingTarget?.data_sources[key];
|
|
137
|
-
const merged = mergeTarget(existingTarget, {
|
|
138
|
-
data_sources: {
|
|
139
|
-
[key]: {
|
|
140
|
-
data_source_id: result.dataSourceId,
|
|
141
|
-
property_ids_by_key: existingEntry?.property_ids_by_key ?? {},
|
|
142
|
-
},
|
|
143
|
-
},
|
|
144
|
-
});
|
|
145
|
-
writeTarget(merged);
|
|
146
|
-
const count = Object.keys(result.properties).length;
|
|
147
|
-
if (quiet) {
|
|
148
|
-
console.log(`Found database "${result.name}" (${count} properties)`);
|
|
149
|
-
return;
|
|
150
|
-
}
|
|
151
|
-
console.log(`\n✓ Updated ${out} (key: "${key}")\n`);
|
|
152
|
-
console.log(` → recorded data_source_id in .notion/target.json (${key})`);
|
|
153
|
-
console.log(` ${count} properties from "${result.name}":`);
|
|
154
|
-
for (const [k, p] of Object.entries(result.properties)) {
|
|
155
|
-
console.log(` ${k}: ${p.name} (${p.type})`);
|
|
156
|
-
}
|
|
157
|
-
console.log("");
|
|
158
|
-
}
|
package/bin/cli/target.js
DELETED
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { dirname, resolve as resolvePath } from "node:path";
|
|
3
|
-
export const TARGET_PATH = ".notion/target.json";
|
|
4
|
-
function defaultTarget() {
|
|
5
|
-
return { env: "production", block_id: [], data_sources: {} };
|
|
6
|
-
}
|
|
7
|
-
export function resolveTargetPath(cwd = process.cwd()) {
|
|
8
|
-
return resolvePath(cwd, TARGET_PATH);
|
|
9
|
-
}
|
|
10
|
-
export function readTarget(cwd = process.cwd()) {
|
|
11
|
-
const path = resolveTargetPath(cwd);
|
|
12
|
-
if (!existsSync(path)) {
|
|
13
|
-
return null;
|
|
14
|
-
}
|
|
15
|
-
let raw;
|
|
16
|
-
try {
|
|
17
|
-
raw = readFileSync(path, "utf-8");
|
|
18
|
-
}
|
|
19
|
-
catch (error) {
|
|
20
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
21
|
-
throw new Error(`Error: failed to read ${TARGET_PATH}: ${message}\n Check file permissions.`);
|
|
22
|
-
}
|
|
23
|
-
let parsed;
|
|
24
|
-
try {
|
|
25
|
-
parsed = JSON.parse(raw);
|
|
26
|
-
}
|
|
27
|
-
catch {
|
|
28
|
-
throw new Error(`Error: ${TARGET_PATH} contains invalid JSON.\n Fix the syntax or delete the file to regenerate it with 'ncblock connect'.`);
|
|
29
|
-
}
|
|
30
|
-
return normalizeTarget(parsed);
|
|
31
|
-
}
|
|
32
|
-
export function writeTarget(config, cwd = process.cwd()) {
|
|
33
|
-
const path = resolveTargetPath(cwd);
|
|
34
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
35
|
-
writeFileSync(path, JSON.stringify(config, null, "\t") + "\n");
|
|
36
|
-
}
|
|
37
|
-
function normalizeTarget(value) {
|
|
38
|
-
if (!isRecord(value)) {
|
|
39
|
-
throw new Error(`Error: ${TARGET_PATH} must be a JSON object.\n Delete the file to regenerate it with 'ncblock connect'.`);
|
|
40
|
-
}
|
|
41
|
-
const env = typeof value.env === "string" ? value.env : "production";
|
|
42
|
-
const block_id = [];
|
|
43
|
-
if (Array.isArray(value.block_id)) {
|
|
44
|
-
for (const entry of value.block_id) {
|
|
45
|
-
if (typeof entry === "string" && entry.length > 0) {
|
|
46
|
-
block_id.push(entry);
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
else if (typeof value.block_id === "string" && value.block_id.length > 0) {
|
|
51
|
-
block_id.push(value.block_id);
|
|
52
|
-
}
|
|
53
|
-
const data_sources = {};
|
|
54
|
-
if (isRecord(value.data_sources)) {
|
|
55
|
-
for (const [key, ds] of Object.entries(value.data_sources)) {
|
|
56
|
-
if (!isRecord(ds) || typeof ds.data_source_id !== "string") {
|
|
57
|
-
continue;
|
|
58
|
-
}
|
|
59
|
-
const property_ids_by_key = {};
|
|
60
|
-
if (isRecord(ds.property_ids_by_key)) {
|
|
61
|
-
for (const [k, v] of Object.entries(ds.property_ids_by_key)) {
|
|
62
|
-
if (typeof v === "string") {
|
|
63
|
-
property_ids_by_key[k] = v;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
data_sources[key] = {
|
|
68
|
-
data_source_id: ds.data_source_id,
|
|
69
|
-
property_ids_by_key,
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
return { env, block_id, data_sources };
|
|
74
|
-
}
|
|
75
|
-
/**
|
|
76
|
-
* Merge a new partial target into the existing one. Block IDs from `incoming`
|
|
77
|
-
* are appended (de-duplicated). Data source bindings replace any existing
|
|
78
|
-
* binding for matching keys; unrelated keys are preserved.
|
|
79
|
-
*/
|
|
80
|
-
export function mergeTarget(existing, incoming) {
|
|
81
|
-
const base = existing ?? defaultTarget();
|
|
82
|
-
const env = incoming.env ?? base.env;
|
|
83
|
-
const blockIds = new Set(base.block_id);
|
|
84
|
-
for (const id of incoming.block_id ?? []) {
|
|
85
|
-
blockIds.add(id);
|
|
86
|
-
}
|
|
87
|
-
const data_sources = {
|
|
88
|
-
...base.data_sources,
|
|
89
|
-
...(incoming.data_sources ?? {}),
|
|
90
|
-
};
|
|
91
|
-
return { env, block_id: [...blockIds], data_sources };
|
|
92
|
-
}
|
|
93
|
-
function isRecord(value) {
|
|
94
|
-
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
95
|
-
}
|