khotan-data 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +60 -6
- package/dist/cli.js +292 -8
- package/dist/factory.cjs +1083 -99
- package/dist/factory.cjs.map +1 -1
- package/dist/factory.d.cts +225 -38
- package/dist/factory.d.ts +225 -38
- package/dist/factory.js +1082 -101
- package/dist/factory.js.map +1 -1
- package/dist/templates/cache.example.ts +11 -0
- package/dist/templates/cache.ts +58 -0
- package/dist/templates/catch.ts +13 -1
- package/dist/templates/inflow.ts +5 -6
- package/dist/templates/khotan-config.ts +13 -4
- package/dist/templates/mapping-browser.tsx +761 -0
- package/dist/templates/mappings-page.tsx +9 -0
- package/dist/templates/outflow.ts +5 -5
- package/dist/templates/pass.ts +10 -0
- package/dist/templates/relay.example.ts +11 -1
- package/dist/templates/relay.ts +16 -7
- package/dist/templates/schema.ts +81 -0
- package/dist/templates/skill-plug.md +38 -15
- package/dist/templates/skill-setup.md +44 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,6 +4,14 @@ Data primitives for TypeScript — ETL pipelines, transforms, and Drizzle Postgr
|
|
|
4
4
|
|
|
5
5
|
Built for **Next.js + Drizzle + Postgres** projects. Think better-auth for data management.
|
|
6
6
|
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm i khotan-data
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Requires `drizzle-orm` as a peer dependency (you almost certainly already have it).
|
|
14
|
+
|
|
7
15
|
## CLI
|
|
8
16
|
|
|
9
17
|
Scaffold components into your Next.js + Drizzle project:
|
|
@@ -17,6 +25,7 @@ npx khotan init --full
|
|
|
17
25
|
|
|
18
26
|
# Add components (reusable building blocks — never create pages)
|
|
19
27
|
npx khotan add schema # Drizzle table definitions (plugs, flows, runs, resources, mappings)
|
|
28
|
+
npx khotan add cache # Durable key/value caches for workflows and relays
|
|
20
29
|
npx khotan add plug # Fetch wrapper with auth, retry, pagination
|
|
21
30
|
npx khotan add inflow # Workflow-backed flow for pulling data in
|
|
22
31
|
npx khotan add outflow # Workflow-backed flow for pushing data out
|
|
@@ -33,18 +42,22 @@ npx khotan add hub --yes # Auto-accept dependency install prompts
|
|
|
33
42
|
|
|
34
43
|
## Factory (Runtime Engine)
|
|
35
44
|
|
|
36
|
-
Register plugs, flows, and resources — the factory upserts them on boot and serves a REST API:
|
|
45
|
+
Register plugs, caches, flows, and resources — the factory upserts them on boot and serves a REST API:
|
|
37
46
|
|
|
38
47
|
```typescript
|
|
39
48
|
import { khotan, drizzleAdapter, toNextJsHandler } from "khotan-data/factory";
|
|
40
49
|
import { db } from "@/db";
|
|
41
50
|
import { shopifyPlug } from "@/lib/khotan/plugs/shopify";
|
|
42
51
|
import { shopifyProductsInflow } from "@/lib/khotan/flows/shopify-products";
|
|
52
|
+
import { shopifyProductsSnapshotCache } from "@/lib/khotan/caches/shopify-products-snapshot";
|
|
43
53
|
|
|
44
54
|
const khotanData = khotan({
|
|
45
55
|
adapter: drizzleAdapter(db),
|
|
46
56
|
resources: [
|
|
47
|
-
{ name: "products", connectField: "sku" },
|
|
57
|
+
{ name: "products", mapping: { connectField: "sku" } },
|
|
58
|
+
],
|
|
59
|
+
caches: [
|
|
60
|
+
shopifyProductsSnapshotCache,
|
|
48
61
|
],
|
|
49
62
|
plugs: [
|
|
50
63
|
{
|
|
@@ -66,13 +79,54 @@ await khotanData.flow("products-inflow", { plugName: "shopify" }).start({
|
|
|
66
79
|
});
|
|
67
80
|
```
|
|
68
81
|
|
|
69
|
-
##
|
|
82
|
+
## Caches
|
|
70
83
|
|
|
71
|
-
|
|
72
|
-
|
|
84
|
+
Use first-class caches when a flow, relay, catch, or pass needs durable state between runs.
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
import { cache } from "@/lib/khotan/caches/cache";
|
|
88
|
+
|
|
89
|
+
export const shopifyProductsSnapshotCache = cache({
|
|
90
|
+
name: "shopify-products-snapshot",
|
|
91
|
+
scope: {
|
|
92
|
+
plug: "shopify",
|
|
93
|
+
resource: "products",
|
|
94
|
+
flow: "shopify-products-inflow",
|
|
95
|
+
},
|
|
96
|
+
ttl: "6h",
|
|
97
|
+
});
|
|
73
98
|
```
|
|
74
99
|
|
|
75
|
-
|
|
100
|
+
Inside workflows, use `khotanCache(ctx, "name")` for snapshots, cursors, and dedupe markers:
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
import { khotanCache } from "khotan-data/factory";
|
|
104
|
+
|
|
105
|
+
async function shopifyProductsWorkflow(ctx: InflowContext) {
|
|
106
|
+
"use workflow";
|
|
107
|
+
|
|
108
|
+
async function syncProducts() {
|
|
109
|
+
"use step";
|
|
110
|
+
const snapshotCache = khotanCache(ctx, "shopify-products-snapshot");
|
|
111
|
+
const previous =
|
|
112
|
+
(await snapshotCache.get<Array<Record<string, unknown>>>("latest")) ?? [];
|
|
113
|
+
|
|
114
|
+
const response = await shopifyPlug.get<{ data?: Array<Record<string, unknown>> }>("/products");
|
|
115
|
+
const records = Array.isArray(response.data) ? response.data : [];
|
|
116
|
+
|
|
117
|
+
await snapshotCache.set("latest", records);
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
extracted: records.length,
|
|
121
|
+
transformed: records.length,
|
|
122
|
+
created: records.length,
|
|
123
|
+
metadata: { previousCount: previous.length },
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return syncProducts();
|
|
128
|
+
}
|
|
129
|
+
```
|
|
76
130
|
|
|
77
131
|
## Quick Start
|
|
78
132
|
|
package/dist/cli.js
CHANGED
|
@@ -168,6 +168,23 @@ var COMPONENTS = {
|
|
|
168
168
|
npmPackages: ["drizzle-orm"]
|
|
169
169
|
}
|
|
170
170
|
},
|
|
171
|
+
cache: {
|
|
172
|
+
name: "cache",
|
|
173
|
+
description: "First-class durable cache definitions for khotan sync workloads",
|
|
174
|
+
requires: ["schema"],
|
|
175
|
+
files: [
|
|
176
|
+
{
|
|
177
|
+
templatePath: path2.resolve(__dirname$1, "templates", "cache.ts"),
|
|
178
|
+
outputFile: "caches/cache.ts",
|
|
179
|
+
outputBase: "outputDir"
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
templatePath: path2.resolve(__dirname$1, "templates", "cache.example.ts"),
|
|
183
|
+
outputFile: "caches/cache.example.ts",
|
|
184
|
+
outputBase: "outputDir"
|
|
185
|
+
}
|
|
186
|
+
]
|
|
187
|
+
},
|
|
171
188
|
hub: {
|
|
172
189
|
name: "hub",
|
|
173
190
|
description: "Dashboard UI for managing plugs and flows",
|
|
@@ -231,6 +248,25 @@ var COMPONENTS = {
|
|
|
231
248
|
}
|
|
232
249
|
]
|
|
233
250
|
},
|
|
251
|
+
"mapping-browser": {
|
|
252
|
+
name: "mapping-browser",
|
|
253
|
+
description: "Searchable mappings browser for listing, creating, editing, and deleting resource mappings",
|
|
254
|
+
requiresShadcn: true,
|
|
255
|
+
dependencies: {
|
|
256
|
+
shadcnComponents: ["card", "table", "button", "input", "label"]
|
|
257
|
+
},
|
|
258
|
+
files: [
|
|
259
|
+
{
|
|
260
|
+
templatePath: path2.resolve(
|
|
261
|
+
__dirname$1,
|
|
262
|
+
"templates",
|
|
263
|
+
"mapping-browser.tsx"
|
|
264
|
+
),
|
|
265
|
+
outputFile: "mapping-browser.tsx",
|
|
266
|
+
outputBase: "components"
|
|
267
|
+
}
|
|
268
|
+
]
|
|
269
|
+
},
|
|
234
270
|
"plug-debugger": {
|
|
235
271
|
name: "plug-debugger",
|
|
236
272
|
description: "Dev-only debug panel for testing plug requests interactively",
|
|
@@ -462,6 +498,22 @@ var BLOCKS = {
|
|
|
462
498
|
}
|
|
463
499
|
]
|
|
464
500
|
},
|
|
501
|
+
"mappings-page-1": {
|
|
502
|
+
name: "mappings-page-1",
|
|
503
|
+
description: "Page route at /mappings that renders the reusable Khotan mappings browser",
|
|
504
|
+
requires: ["mapping-browser"],
|
|
505
|
+
files: [
|
|
506
|
+
{
|
|
507
|
+
templatePath: path2.resolve(
|
|
508
|
+
__dirname$1,
|
|
509
|
+
"templates",
|
|
510
|
+
"mappings-page.tsx"
|
|
511
|
+
),
|
|
512
|
+
outputFile: "mappings/page.tsx",
|
|
513
|
+
outputBase: "appRoot"
|
|
514
|
+
}
|
|
515
|
+
]
|
|
516
|
+
},
|
|
465
517
|
graph: {
|
|
466
518
|
name: "graph",
|
|
467
519
|
description: "Standalone topology graph page at /graph with filtering and run-state overlays",
|
|
@@ -1722,13 +1774,13 @@ function diffSchemas(expected, actual, basePath = "$") {
|
|
|
1722
1774
|
}
|
|
1723
1775
|
return diffObjectSchema(expected, actual, basePath);
|
|
1724
1776
|
}
|
|
1725
|
-
function diffTypedNode(expected, actual,
|
|
1777
|
+
function diffTypedNode(expected, actual, path15) {
|
|
1726
1778
|
const expectedType = expected["_type"];
|
|
1727
1779
|
if (expectedType === "array") {
|
|
1728
1780
|
if (actual.type !== "array") {
|
|
1729
1781
|
return [
|
|
1730
1782
|
{
|
|
1731
|
-
path:
|
|
1783
|
+
path: path15,
|
|
1732
1784
|
issue: "type_mismatch",
|
|
1733
1785
|
note: `expected array, got ${actual.type}`
|
|
1734
1786
|
}
|
|
@@ -1736,13 +1788,13 @@ function diffTypedNode(expected, actual, path14) {
|
|
|
1736
1788
|
}
|
|
1737
1789
|
const itemSchema = expected["items"];
|
|
1738
1790
|
if (!itemSchema || !actual.items) return [];
|
|
1739
|
-
return diffSchemas(itemSchema, actual.items, `${
|
|
1791
|
+
return diffSchemas(itemSchema, actual.items, `${path15}[]`);
|
|
1740
1792
|
}
|
|
1741
1793
|
const normalizedExpected = normalizeType(expectedType);
|
|
1742
1794
|
if (normalizedExpected !== actual.type && actual.type !== "null") {
|
|
1743
1795
|
return [
|
|
1744
1796
|
{
|
|
1745
|
-
path:
|
|
1797
|
+
path: path15,
|
|
1746
1798
|
issue: "type_mismatch",
|
|
1747
1799
|
note: `expected ${expectedType}, got ${actual.type}`
|
|
1748
1800
|
}
|
|
@@ -1750,11 +1802,11 @@ function diffTypedNode(expected, actual, path14) {
|
|
|
1750
1802
|
}
|
|
1751
1803
|
return [];
|
|
1752
1804
|
}
|
|
1753
|
-
function diffObjectSchema(expected, actual,
|
|
1805
|
+
function diffObjectSchema(expected, actual, path15) {
|
|
1754
1806
|
if (actual.type !== "object") {
|
|
1755
1807
|
return [
|
|
1756
1808
|
{
|
|
1757
|
-
path:
|
|
1809
|
+
path: path15,
|
|
1758
1810
|
issue: "type_mismatch",
|
|
1759
1811
|
note: `expected object, got ${actual.type}`
|
|
1760
1812
|
}
|
|
@@ -1763,7 +1815,7 @@ function diffObjectSchema(expected, actual, path14) {
|
|
|
1763
1815
|
const mismatches = [];
|
|
1764
1816
|
const actualProps = actual.properties;
|
|
1765
1817
|
for (const [key, typeDesc] of Object.entries(expected)) {
|
|
1766
|
-
const childPath =
|
|
1818
|
+
const childPath = path15 === "$" ? `$.${key}` : `${path15}.${key}`;
|
|
1767
1819
|
const typeStr = typeof typeDesc === "string" ? typeDesc : null;
|
|
1768
1820
|
const isOptional = typeStr?.endsWith("?") ?? false;
|
|
1769
1821
|
if (!(key in actualProps)) {
|
|
@@ -1801,7 +1853,7 @@ function diffObjectSchema(expected, actual, path14) {
|
|
|
1801
1853
|
}
|
|
1802
1854
|
for (const key of Object.keys(actualProps)) {
|
|
1803
1855
|
if (!(key in expected)) {
|
|
1804
|
-
const childPath =
|
|
1856
|
+
const childPath = path15 === "$" ? `$.${key}` : `${path15}.${key}`;
|
|
1805
1857
|
mismatches.push({ path: childPath, issue: "extra" });
|
|
1806
1858
|
}
|
|
1807
1859
|
}
|
|
@@ -2571,6 +2623,237 @@ withApiOptions(
|
|
|
2571
2623
|
);
|
|
2572
2624
|
output4({ ok: true, action: "cancel", run });
|
|
2573
2625
|
});
|
|
2626
|
+
function output5(obj) {
|
|
2627
|
+
process.stdout.write(JSON.stringify(obj, null, 2) + "\n");
|
|
2628
|
+
}
|
|
2629
|
+
function fail5(error, hint) {
|
|
2630
|
+
output5({ ok: false, error, hint });
|
|
2631
|
+
process.exit(1);
|
|
2632
|
+
}
|
|
2633
|
+
function parseEnvFile4(filePath) {
|
|
2634
|
+
try {
|
|
2635
|
+
const content = fs4.readFileSync(filePath, "utf-8");
|
|
2636
|
+
const values = {};
|
|
2637
|
+
for (const line of content.split("\n")) {
|
|
2638
|
+
const trimmed = line.trim();
|
|
2639
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
2640
|
+
const match = /^([A-Z0-9_]+)\s*=\s*["']?(.*?)["']?$/.exec(trimmed);
|
|
2641
|
+
if (match) values[match[1]] = match[2];
|
|
2642
|
+
}
|
|
2643
|
+
return values;
|
|
2644
|
+
} catch {
|
|
2645
|
+
return {};
|
|
2646
|
+
}
|
|
2647
|
+
}
|
|
2648
|
+
function parsePortFromEnvFile5(filePath) {
|
|
2649
|
+
const raw = parseEnvFile4(filePath)["PORT"];
|
|
2650
|
+
if (!raw) return null;
|
|
2651
|
+
const parsed = parseInt(raw, 10);
|
|
2652
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
2653
|
+
}
|
|
2654
|
+
function resolvePort5(portFlag) {
|
|
2655
|
+
if (portFlag) return parseInt(portFlag, 10);
|
|
2656
|
+
const cwd = process.cwd();
|
|
2657
|
+
return parsePortFromEnvFile5(path2.join(cwd, ".env.local")) ?? parsePortFromEnvFile5(path2.join(cwd, ".env")) ?? 3e3;
|
|
2658
|
+
}
|
|
2659
|
+
function resolveBaseUrl2(opts) {
|
|
2660
|
+
return `http://localhost:${resolvePort5(opts.port)}${opts.basePath}`;
|
|
2661
|
+
}
|
|
2662
|
+
async function fetchJson2(url, init) {
|
|
2663
|
+
const res = await fetch(url, init);
|
|
2664
|
+
const data = await res.json().catch(() => ({}));
|
|
2665
|
+
if (!res.ok) {
|
|
2666
|
+
fail5(
|
|
2667
|
+
"request_failed",
|
|
2668
|
+
data.error ?? `Request to ${url} failed with status ${res.status}`
|
|
2669
|
+
);
|
|
2670
|
+
}
|
|
2671
|
+
return data;
|
|
2672
|
+
}
|
|
2673
|
+
async function checkConnectivity5(baseUrl) {
|
|
2674
|
+
try {
|
|
2675
|
+
const res = await fetch(`${baseUrl}/flows`);
|
|
2676
|
+
if (!res.ok) {
|
|
2677
|
+
fail5(
|
|
2678
|
+
"api_unavailable",
|
|
2679
|
+
`Could not reach Khotan flows API at ${baseUrl}. Check your base path and dev server.`
|
|
2680
|
+
);
|
|
2681
|
+
}
|
|
2682
|
+
} catch {
|
|
2683
|
+
fail5(
|
|
2684
|
+
"connect_failed",
|
|
2685
|
+
`Could not connect to dev server at ${baseUrl.replace("http://", "")}. Is it running?`
|
|
2686
|
+
);
|
|
2687
|
+
}
|
|
2688
|
+
}
|
|
2689
|
+
function parseJsonObjectOption(value, label) {
|
|
2690
|
+
if (!value) return void 0;
|
|
2691
|
+
try {
|
|
2692
|
+
const parsed = JSON.parse(value);
|
|
2693
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
2694
|
+
fail5("invalid_json", `${label} must be a JSON object.`);
|
|
2695
|
+
}
|
|
2696
|
+
return parsed;
|
|
2697
|
+
} catch {
|
|
2698
|
+
fail5("invalid_json", `${label} must be valid JSON.`);
|
|
2699
|
+
}
|
|
2700
|
+
}
|
|
2701
|
+
function withApiOptions2(command) {
|
|
2702
|
+
return command.option("--port <port>", "Dev server port").option("--base-path <path>", "API base path", "/api/khotan");
|
|
2703
|
+
}
|
|
2704
|
+
async function listResources(baseUrl) {
|
|
2705
|
+
return fetchJson2(`${baseUrl}/resources`);
|
|
2706
|
+
}
|
|
2707
|
+
async function resolveResource(baseUrl, resourceNameOrId) {
|
|
2708
|
+
const resources = await listResources(baseUrl);
|
|
2709
|
+
const match = resources.find(
|
|
2710
|
+
(resource) => resource.id === resourceNameOrId || resource.name === resourceNameOrId
|
|
2711
|
+
);
|
|
2712
|
+
if (!match) {
|
|
2713
|
+
fail5(
|
|
2714
|
+
"resource_not_found",
|
|
2715
|
+
`Resource "${resourceNameOrId}" is not registered in the running Khotan config.`
|
|
2716
|
+
);
|
|
2717
|
+
}
|
|
2718
|
+
return match;
|
|
2719
|
+
}
|
|
2720
|
+
var mappingsCommand = new Command("mappings").description("List, lookup, and mutate mappings via the running Khotan API");
|
|
2721
|
+
withApiOptions2(
|
|
2722
|
+
mappingsCommand.command("list").description("List mappings for one resource").argument("<resource>", "Resource name or ID").option("--limit <limit>", "Page size", "20").option("--offset <offset>", "Page offset", "0").option("--search <term>", "Search connectValue, refs, and metadata")
|
|
2723
|
+
).action(
|
|
2724
|
+
async (resourceNameOrId, opts) => {
|
|
2725
|
+
const baseUrl = resolveBaseUrl2(opts);
|
|
2726
|
+
await checkConnectivity5(baseUrl);
|
|
2727
|
+
const resource = await resolveResource(baseUrl, resourceNameOrId);
|
|
2728
|
+
const limit = Math.max(parseInt(opts.limit, 10) || 20, 1);
|
|
2729
|
+
const offset = Math.max(parseInt(opts.offset, 10) || 0, 0);
|
|
2730
|
+
const url = new URL(
|
|
2731
|
+
`${baseUrl}/resources/${encodeURIComponent(resource.id)}/mappings`
|
|
2732
|
+
);
|
|
2733
|
+
url.searchParams.set("limit", String(limit));
|
|
2734
|
+
url.searchParams.set("offset", String(offset));
|
|
2735
|
+
if (opts.search) {
|
|
2736
|
+
url.searchParams.set("search", opts.search);
|
|
2737
|
+
}
|
|
2738
|
+
const data = await fetchJson2(url.toString());
|
|
2739
|
+
output5({
|
|
2740
|
+
ok: true,
|
|
2741
|
+
resource,
|
|
2742
|
+
items: data.items,
|
|
2743
|
+
page: data.page
|
|
2744
|
+
});
|
|
2745
|
+
}
|
|
2746
|
+
);
|
|
2747
|
+
withApiOptions2(
|
|
2748
|
+
mappingsCommand.command("lookup").description("Lookup a mapping by connect value or plug ref").argument("<resource>", "Resource name or ID").option("--connect-value <value>", "Lookup by canonical connect value").option("--plug <plugName>", "Lookup by plug name").option("--ref <ref>", "Lookup by plug ref")
|
|
2749
|
+
).action(
|
|
2750
|
+
async (resourceNameOrId, opts) => {
|
|
2751
|
+
const usingConnectValue = typeof opts.connectValue === "string";
|
|
2752
|
+
const usingPlugRef = typeof opts.plug === "string" || typeof opts.ref === "string";
|
|
2753
|
+
if (!usingConnectValue && !usingPlugRef) {
|
|
2754
|
+
fail5(
|
|
2755
|
+
"validation_error",
|
|
2756
|
+
"Pass either --connect-value <value> or --plug <plugName> with --ref <ref>."
|
|
2757
|
+
);
|
|
2758
|
+
}
|
|
2759
|
+
if (usingConnectValue && usingPlugRef) {
|
|
2760
|
+
fail5(
|
|
2761
|
+
"validation_error",
|
|
2762
|
+
"Choose one lookup mode: either --connect-value or --plug with --ref."
|
|
2763
|
+
);
|
|
2764
|
+
}
|
|
2765
|
+
if (opts.plug && !opts.ref || !opts.plug && opts.ref) {
|
|
2766
|
+
fail5(
|
|
2767
|
+
"validation_error",
|
|
2768
|
+
"Plug-ref lookup requires both --plug <plugName> and --ref <ref>."
|
|
2769
|
+
);
|
|
2770
|
+
}
|
|
2771
|
+
const baseUrl = resolveBaseUrl2(opts);
|
|
2772
|
+
await checkConnectivity5(baseUrl);
|
|
2773
|
+
const resource = await resolveResource(baseUrl, resourceNameOrId);
|
|
2774
|
+
const payload = usingConnectValue ? {
|
|
2775
|
+
resourceId: resource.id,
|
|
2776
|
+
connectValue: opts.connectValue
|
|
2777
|
+
} : {
|
|
2778
|
+
resourceId: resource.id,
|
|
2779
|
+
plugName: opts.plug,
|
|
2780
|
+
ref: opts.ref
|
|
2781
|
+
};
|
|
2782
|
+
const mapping = await fetchJson2(
|
|
2783
|
+
`${baseUrl}/mappings/lookup`,
|
|
2784
|
+
{
|
|
2785
|
+
method: "POST",
|
|
2786
|
+
headers: { "Content-Type": "application/json" },
|
|
2787
|
+
body: JSON.stringify(payload)
|
|
2788
|
+
}
|
|
2789
|
+
);
|
|
2790
|
+
output5({ ok: true, resource, mapping });
|
|
2791
|
+
}
|
|
2792
|
+
);
|
|
2793
|
+
withApiOptions2(
|
|
2794
|
+
mappingsCommand.command("upsert").description("Create or update one mapping by canonical connect value").argument("<resource>", "Resource name or ID").requiredOption("--connect-value <value>", "Canonical connect value").requiredOption("--refs <json>", "JSON object of refs keyed by plug name").option("--metadata <json>", "Optional JSON object of contextual metadata")
|
|
2795
|
+
).action(
|
|
2796
|
+
async (resourceNameOrId, opts) => {
|
|
2797
|
+
const baseUrl = resolveBaseUrl2(opts);
|
|
2798
|
+
await checkConnectivity5(baseUrl);
|
|
2799
|
+
const resource = await resolveResource(baseUrl, resourceNameOrId);
|
|
2800
|
+
const refs = parseJsonObjectOption(opts.refs, "--refs");
|
|
2801
|
+
const metadata = parseJsonObjectOption(opts.metadata, "--metadata");
|
|
2802
|
+
const mapping = await fetchJson2(`${baseUrl}/mappings`, {
|
|
2803
|
+
method: "POST",
|
|
2804
|
+
headers: { "Content-Type": "application/json" },
|
|
2805
|
+
body: JSON.stringify({
|
|
2806
|
+
resourceId: resource.id,
|
|
2807
|
+
connectValue: opts.connectValue,
|
|
2808
|
+
refs,
|
|
2809
|
+
metadata
|
|
2810
|
+
})
|
|
2811
|
+
});
|
|
2812
|
+
output5({ ok: true, action: "upsert", resource, mapping });
|
|
2813
|
+
}
|
|
2814
|
+
);
|
|
2815
|
+
withApiOptions2(
|
|
2816
|
+
mappingsCommand.command("update").description("Update one mapping by row ID").argument("<mappingId>", "Mapping row ID").requiredOption("--resource <resource>", "Resource name or ID").requiredOption("--connect-value <value>", "Canonical connect value").requiredOption("--refs <json>", "JSON object of refs keyed by plug name").option("--metadata <json>", "Optional JSON object of contextual metadata")
|
|
2817
|
+
).action(
|
|
2818
|
+
async (mappingId, opts) => {
|
|
2819
|
+
const baseUrl = resolveBaseUrl2(opts);
|
|
2820
|
+
await checkConnectivity5(baseUrl);
|
|
2821
|
+
const resource = await resolveResource(baseUrl, opts.resource);
|
|
2822
|
+
const refs = parseJsonObjectOption(opts.refs, "--refs");
|
|
2823
|
+
const metadata = parseJsonObjectOption(opts.metadata, "--metadata");
|
|
2824
|
+
const mapping = await fetchJson2(
|
|
2825
|
+
`${baseUrl}/mappings/${encodeURIComponent(mappingId)}`,
|
|
2826
|
+
{
|
|
2827
|
+
method: "PUT",
|
|
2828
|
+
headers: { "Content-Type": "application/json" },
|
|
2829
|
+
body: JSON.stringify({
|
|
2830
|
+
resourceId: resource.id,
|
|
2831
|
+
connectValue: opts.connectValue,
|
|
2832
|
+
refs,
|
|
2833
|
+
metadata
|
|
2834
|
+
})
|
|
2835
|
+
}
|
|
2836
|
+
);
|
|
2837
|
+
output5({ ok: true, action: "update", resource, mapping });
|
|
2838
|
+
}
|
|
2839
|
+
);
|
|
2840
|
+
withApiOptions2(
|
|
2841
|
+
mappingsCommand.command("delete").description("Delete one mapping by row ID").argument("<mappingId>", "Mapping row ID")
|
|
2842
|
+
).action(async (mappingId, opts) => {
|
|
2843
|
+
const baseUrl = resolveBaseUrl2(opts);
|
|
2844
|
+
await checkConnectivity5(baseUrl);
|
|
2845
|
+
const res = await fetch(`${baseUrl}/mappings/${encodeURIComponent(mappingId)}`, {
|
|
2846
|
+
method: "DELETE"
|
|
2847
|
+
});
|
|
2848
|
+
if (!res.ok) {
|
|
2849
|
+
const data = await res.json().catch(() => ({}));
|
|
2850
|
+
fail5(
|
|
2851
|
+
"request_failed",
|
|
2852
|
+
data.error ?? `Delete request failed for mapping "${mappingId}" with status ${res.status}.`
|
|
2853
|
+
);
|
|
2854
|
+
}
|
|
2855
|
+
output5({ ok: true, action: "delete", id: mappingId });
|
|
2856
|
+
});
|
|
2574
2857
|
|
|
2575
2858
|
// src/cli/index.ts
|
|
2576
2859
|
var program = new Command();
|
|
@@ -2582,4 +2865,5 @@ program.addCommand(migrateCommand);
|
|
|
2582
2865
|
program.addCommand(plugCommand);
|
|
2583
2866
|
program.addCommand(wireCommand);
|
|
2584
2867
|
program.addCommand(flowsCommand);
|
|
2868
|
+
program.addCommand(mappingsCommand);
|
|
2585
2869
|
program.parse();
|