railcode 0.1.5 → 0.1.8
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 +7 -0
- package/dist/index.js +476 -1
- package/package.json +1 -1
- package/static/sdk.js +35 -0
package/README.md
CHANGED
|
@@ -73,6 +73,13 @@ you enter a bare domain.
|
|
|
73
73
|
`/v1/config/design-system` using the saved browser-authorized API token or
|
|
74
74
|
`RAILCODE_API_TOKEN` and prints it to stdout.
|
|
75
75
|
|
|
76
|
+
`railcode db list` and `railcode db query "<sql>"` read the admin-configured data
|
|
77
|
+
connectors from the terminal — listing them (`GET /v1/connections`) or running a
|
|
78
|
+
read-only SQL query against one (`POST /v1/sql`). The engine is inferred from the
|
|
79
|
+
named connector (override with `--engine`); use `--connection <name>` to pick a
|
|
80
|
+
connector, `--params '<json-array>'` for `$1, $2, ...` placeholders, and `--json`
|
|
81
|
+
for the raw response. Authenticates with the saved token or `RAILCODE_API_TOKEN`.
|
|
82
|
+
|
|
76
83
|
`railcode deploy` runs from the current Railcode app directory, builds the app,
|
|
77
84
|
and publishes the inferred app output over HTTP to the canonical API host. It
|
|
78
85
|
uses a saved Railcode token, prints a browser authorization link when needed, or
|
package/dist/index.js
CHANGED
|
@@ -26,6 +26,11 @@ Usage:
|
|
|
26
26
|
railcode login [--api-url <url>]
|
|
27
27
|
railcode deploy [--private | --public]
|
|
28
28
|
railcode access [public|private|restricted] [--users <a,b>]
|
|
29
|
+
railcode network
|
|
30
|
+
railcode db list
|
|
31
|
+
railcode db query "<sql>" [--connection <name>] [--engine <postgres|mysql>]
|
|
32
|
+
railcode connector list
|
|
33
|
+
railcode connector fetch <name> <path> [--method <verb>] [--body <data>]
|
|
29
34
|
railcode get design-system
|
|
30
35
|
railcode init <app>
|
|
31
36
|
railcode upgrade
|
|
@@ -59,6 +64,16 @@ async function main() {
|
|
|
59
64
|
case "access":
|
|
60
65
|
await commandAccess(args);
|
|
61
66
|
return;
|
|
67
|
+
case "network":
|
|
68
|
+
await commandNetwork(args);
|
|
69
|
+
return;
|
|
70
|
+
case "db":
|
|
71
|
+
await commandDb(args);
|
|
72
|
+
return;
|
|
73
|
+
case "connector":
|
|
74
|
+
case "connectors":
|
|
75
|
+
await commandConnector(args);
|
|
76
|
+
return;
|
|
62
77
|
case "get":
|
|
63
78
|
await commandGet(args);
|
|
64
79
|
return;
|
|
@@ -620,7 +635,8 @@ async function handleLocalApi(ctx, request, response, url) {
|
|
|
620
635
|
await handleLocalFiles(ctx, request, response, path);
|
|
621
636
|
return;
|
|
622
637
|
}
|
|
623
|
-
if (path === "/connections"
|
|
638
|
+
if ((path === "/connections" || path === "/service-connectors") &&
|
|
639
|
+
(!ctx.token || !ctx.apiBase)) {
|
|
624
640
|
sendJson(response, 200, []);
|
|
625
641
|
return;
|
|
626
642
|
}
|
|
@@ -1594,6 +1610,465 @@ function appUrlFromOrigin(app, origin) {
|
|
|
1594
1610
|
url.hash = "";
|
|
1595
1611
|
return url.toString();
|
|
1596
1612
|
}
|
|
1613
|
+
const NETWORK_HELP = `Usage:
|
|
1614
|
+
railcode network Show the platform default network mode for new apps
|
|
1615
|
+
|
|
1616
|
+
Network mode is the per-app egress policy, enforced as a CSP:
|
|
1617
|
+
open No CSP — the app may fetch any origin.
|
|
1618
|
+
restricted External calls only via the Railcode proxy (connect-src 'self').
|
|
1619
|
+
sandboxed Same-origin only — no inline <script>, no external CDN/fonts.
|
|
1620
|
+
|
|
1621
|
+
Admins set the default and flip individual apps from the admin console; the CLI
|
|
1622
|
+
only reports the default so you know how to build a new app.
|
|
1623
|
+
`;
|
|
1624
|
+
const NETWORK_MODE_BUILD_HINTS = {
|
|
1625
|
+
open: "Build freely: direct fetch() to any origin is allowed.",
|
|
1626
|
+
restricted: "Don't fetch external origins directly — route them through the Railcode proxy. Inline scripts and CDN assets are fine.",
|
|
1627
|
+
sandboxed: "Same-origin only: no external fetch, no inline <script>, no external CDN/font links. Use the SDK and bundle every asset locally.",
|
|
1628
|
+
};
|
|
1629
|
+
async function commandNetwork(args) {
|
|
1630
|
+
if (booleanOption(args, "help") || args.rest[0] === "help") {
|
|
1631
|
+
console.log(NETWORK_HELP);
|
|
1632
|
+
return;
|
|
1633
|
+
}
|
|
1634
|
+
if (args.rest.length > 0) {
|
|
1635
|
+
throw new CliError("railcode network shows the platform default. Admins flip individual apps from the admin console.", 2);
|
|
1636
|
+
}
|
|
1637
|
+
rejectUnknownOptions(args, ["help", "json", "apiUrl"], "network");
|
|
1638
|
+
const config = loadConfig();
|
|
1639
|
+
const manifestApiUrl = readRailcodeManifest(process.cwd())?.manifest.deploy?.apiUrl;
|
|
1640
|
+
const authUrl = await resolveRequiredAuthOrigin({
|
|
1641
|
+
config,
|
|
1642
|
+
args,
|
|
1643
|
+
action: "Network mode",
|
|
1644
|
+
fallbackApiUrl: manifestApiUrl,
|
|
1645
|
+
missingUrlHint: "Pass --api-url <url>, set deploy.apiUrl in railcode.json, or set RAILCODE_API_URL.",
|
|
1646
|
+
});
|
|
1647
|
+
const apiUrl = canonicalApiOrigin(authUrl);
|
|
1648
|
+
let authedConfig = await ensureApiToken({ ...config, apiUrl: authUrl }, authUrl, "Network mode");
|
|
1649
|
+
let response = await fetchNetworkDefault(apiUrl, authedConfig);
|
|
1650
|
+
if (response.status === 401 && !process.env.RAILCODE_API_TOKEN && process.stdin.isTTY) {
|
|
1651
|
+
console.log("Saved login expired. Log in again to continue.");
|
|
1652
|
+
const retryConfig = { ...authedConfig, apiUrl: authUrl };
|
|
1653
|
+
delete retryConfig.apiToken;
|
|
1654
|
+
delete retryConfig.apiTokenPrefix;
|
|
1655
|
+
delete retryConfig.cookies;
|
|
1656
|
+
saveConfig(retryConfig);
|
|
1657
|
+
authedConfig = await ensureApiToken(retryConfig, authUrl, "Network mode");
|
|
1658
|
+
response = await fetchNetworkDefault(apiUrl, authedConfig);
|
|
1659
|
+
}
|
|
1660
|
+
const view = (await readDbResponse(response, "Network mode"));
|
|
1661
|
+
printNetworkDefault(view, booleanOption(args, "json"));
|
|
1662
|
+
}
|
|
1663
|
+
function fetchNetworkDefault(apiUrl, config) {
|
|
1664
|
+
return dbFetch(apiUrl, config, "/v1/network-default", { redirect: "manual" });
|
|
1665
|
+
}
|
|
1666
|
+
function printNetworkDefault(view, json) {
|
|
1667
|
+
if (json) {
|
|
1668
|
+
console.log(JSON.stringify(view, null, 2));
|
|
1669
|
+
return;
|
|
1670
|
+
}
|
|
1671
|
+
const mode = view.network_mode || "restricted";
|
|
1672
|
+
console.log(`Platform default network mode: ${mode}`);
|
|
1673
|
+
const hint = NETWORK_MODE_BUILD_HINTS[mode];
|
|
1674
|
+
if (hint)
|
|
1675
|
+
console.log(hint);
|
|
1676
|
+
}
|
|
1677
|
+
const DB_ENGINES = new Set(["postgres", "mysql"]);
|
|
1678
|
+
const DB_HELP = `Query Railcode data connectors.
|
|
1679
|
+
|
|
1680
|
+
Usage:
|
|
1681
|
+
railcode db list
|
|
1682
|
+
railcode db query "<sql>" [options]
|
|
1683
|
+
|
|
1684
|
+
Commands:
|
|
1685
|
+
list List configured data connectors (name + engine).
|
|
1686
|
+
query "<sql>" Run a read-only SQL query against a connector.
|
|
1687
|
+
|
|
1688
|
+
Query options:
|
|
1689
|
+
--connection <name> Connector to query (default: default).
|
|
1690
|
+
--engine <postgres|mysql> Override the engine. Inferred from the connector by default.
|
|
1691
|
+
--params '<json-array>' Params for $1, $2, ... placeholders, e.g. --params '[42,"ok"]'.
|
|
1692
|
+
--file <path> Read SQL from a file instead of an argument.
|
|
1693
|
+
--json Print raw JSON instead of a table.
|
|
1694
|
+
|
|
1695
|
+
Options:
|
|
1696
|
+
--api-url <url> Railcode auth/API URL. Defaults to saved config or railcode.json.
|
|
1697
|
+
|
|
1698
|
+
Examples:
|
|
1699
|
+
railcode db list
|
|
1700
|
+
railcode db query "select id, email from users limit 5"
|
|
1701
|
+
railcode db query "select * from orders where total > $1" --params '[100]'
|
|
1702
|
+
railcode db query "select 1" --connection analytics --engine mysql
|
|
1703
|
+
`;
|
|
1704
|
+
async function commandDb(args) {
|
|
1705
|
+
const subcommand = args.rest[0];
|
|
1706
|
+
const rest = { ...args, rest: args.rest.slice(1) };
|
|
1707
|
+
switch (subcommand) {
|
|
1708
|
+
case "list":
|
|
1709
|
+
case "ls":
|
|
1710
|
+
case "connections":
|
|
1711
|
+
await commandDbList(rest);
|
|
1712
|
+
return;
|
|
1713
|
+
case "query":
|
|
1714
|
+
case "sql":
|
|
1715
|
+
await commandDbQuery(rest);
|
|
1716
|
+
return;
|
|
1717
|
+
case undefined:
|
|
1718
|
+
case "help":
|
|
1719
|
+
console.log(DB_HELP);
|
|
1720
|
+
return;
|
|
1721
|
+
default:
|
|
1722
|
+
if (booleanOption(args, "help")) {
|
|
1723
|
+
console.log(DB_HELP);
|
|
1724
|
+
return;
|
|
1725
|
+
}
|
|
1726
|
+
throw new CliError(`Unknown db command: ${subcommand}\n\n${DB_HELP}`, 2);
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
async function commandDbList(args) {
|
|
1730
|
+
if (booleanOption(args, "help") || args.rest[0] === "help") {
|
|
1731
|
+
console.log(DB_HELP);
|
|
1732
|
+
return;
|
|
1733
|
+
}
|
|
1734
|
+
rejectUnknownOptions(args, ["help", "json", "apiUrl"], "db list");
|
|
1735
|
+
if (args.rest.length > 0) {
|
|
1736
|
+
throw new CliError("railcode db list does not accept positional arguments.", 2);
|
|
1737
|
+
}
|
|
1738
|
+
const response = await dbAuthedRequest(args, "Data connectors list", fetchConnections);
|
|
1739
|
+
const connectors = (await readDbResponse(response, "Data connectors list"));
|
|
1740
|
+
printConnectors(connectors, booleanOption(args, "json"));
|
|
1741
|
+
}
|
|
1742
|
+
async function commandDbQuery(args) {
|
|
1743
|
+
if (booleanOption(args, "help") || args.rest[0] === "help") {
|
|
1744
|
+
console.log(DB_HELP);
|
|
1745
|
+
return;
|
|
1746
|
+
}
|
|
1747
|
+
rejectUnknownOptions(args, ["help", "json", "apiUrl", "connection", "engine", "params", "file"], "db query");
|
|
1748
|
+
const query = resolveQuerySql(args);
|
|
1749
|
+
const connection = (stringOption(args, "connection") || "default").trim() || "default";
|
|
1750
|
+
const params = parseSqlParams(args);
|
|
1751
|
+
const engine = parseEngineOption(args) ?? (await resolveConnectionEngine(args, connection));
|
|
1752
|
+
const body = { engine, connection, query, params };
|
|
1753
|
+
const response = await dbAuthedRequest(args, "SQL query", (apiUrl, config) => postSql(apiUrl, config, body));
|
|
1754
|
+
const result = (await readDbResponse(response, "SQL query", "Run `railcode db list` to see configured connectors."));
|
|
1755
|
+
printSqlResult(result, booleanOption(args, "json"));
|
|
1756
|
+
}
|
|
1757
|
+
function resolveQuerySql(args) {
|
|
1758
|
+
const file = stringOption(args, "file");
|
|
1759
|
+
if (file && args.rest.length > 0) {
|
|
1760
|
+
throw new CliError("Pass SQL as an argument or --file, not both.", 2);
|
|
1761
|
+
}
|
|
1762
|
+
if (file) {
|
|
1763
|
+
const path = resolve(file);
|
|
1764
|
+
if (!existsSync(path))
|
|
1765
|
+
throw new CliError(`SQL file not found: ${path}`, 1);
|
|
1766
|
+
const sql = readFileSync(path, "utf8").trim();
|
|
1767
|
+
if (!sql)
|
|
1768
|
+
throw new CliError(`SQL file is empty: ${path}`, 1);
|
|
1769
|
+
return sql;
|
|
1770
|
+
}
|
|
1771
|
+
if (args.rest.length > 1) {
|
|
1772
|
+
throw new CliError('railcode db query takes a single SQL string. Quote it: railcode db query "select 1".', 2);
|
|
1773
|
+
}
|
|
1774
|
+
const positional = args.rest[0];
|
|
1775
|
+
if (!positional || !positional.trim()) {
|
|
1776
|
+
throw new CliError('Missing SQL. Usage: railcode db query "select 1" [--connection <name>].', 2);
|
|
1777
|
+
}
|
|
1778
|
+
return positional.trim();
|
|
1779
|
+
}
|
|
1780
|
+
function parseSqlParams(args) {
|
|
1781
|
+
const raw = stringOption(args, "params");
|
|
1782
|
+
if (raw === undefined)
|
|
1783
|
+
return [];
|
|
1784
|
+
let parsed;
|
|
1785
|
+
try {
|
|
1786
|
+
parsed = JSON.parse(raw);
|
|
1787
|
+
}
|
|
1788
|
+
catch {
|
|
1789
|
+
throw new CliError('--params must be a JSON array, e.g. --params \'[42, "ok"]\'.', 2);
|
|
1790
|
+
}
|
|
1791
|
+
if (!Array.isArray(parsed)) {
|
|
1792
|
+
throw new CliError('--params must be a JSON array, e.g. --params \'[42, "ok"]\'.', 2);
|
|
1793
|
+
}
|
|
1794
|
+
return parsed;
|
|
1795
|
+
}
|
|
1796
|
+
function parseEngineOption(args) {
|
|
1797
|
+
const raw = stringOption(args, "engine");
|
|
1798
|
+
if (raw === undefined)
|
|
1799
|
+
return undefined;
|
|
1800
|
+
const engine = raw.trim().toLowerCase();
|
|
1801
|
+
if (!DB_ENGINES.has(engine)) {
|
|
1802
|
+
throw new CliError(`Unknown engine '${raw}'. Use postgres or mysql.`, 2);
|
|
1803
|
+
}
|
|
1804
|
+
return engine;
|
|
1805
|
+
}
|
|
1806
|
+
// The server requires an engine and rejects a mismatch, so when the caller omits
|
|
1807
|
+
// --engine we look the connection up in the registry and use its engine.
|
|
1808
|
+
async function resolveConnectionEngine(args, connection) {
|
|
1809
|
+
const response = await dbAuthedRequest(args, "Data connectors list", fetchConnections);
|
|
1810
|
+
const connectors = (await readDbResponse(response, "Data connectors list"));
|
|
1811
|
+
const match = connectors.find((c) => c.name === connection);
|
|
1812
|
+
if (!match) {
|
|
1813
|
+
const names = connectors.map((c) => c.name).join(", ") || "none";
|
|
1814
|
+
throw new CliError(`No data connector named '${connection}'. Configured: ${names}. Pass --engine to override.`, 1);
|
|
1815
|
+
}
|
|
1816
|
+
return match.engine;
|
|
1817
|
+
}
|
|
1818
|
+
async function dbAuthedRequest(args, action, send) {
|
|
1819
|
+
const config = loadConfig();
|
|
1820
|
+
const authUrl = await resolveDbAuthOrigin(config, args);
|
|
1821
|
+
const apiUrl = canonicalApiOrigin(authUrl);
|
|
1822
|
+
let authedConfig = await ensureApiToken({ ...config, apiUrl: authUrl }, authUrl, action);
|
|
1823
|
+
let response = await send(apiUrl, authedConfig);
|
|
1824
|
+
if (response.status === 401 && !process.env.RAILCODE_API_TOKEN && process.stdin.isTTY) {
|
|
1825
|
+
console.log("Saved login expired. Log in again to continue.");
|
|
1826
|
+
const retryConfig = { ...authedConfig, apiUrl: authUrl };
|
|
1827
|
+
delete retryConfig.apiToken;
|
|
1828
|
+
delete retryConfig.apiTokenPrefix;
|
|
1829
|
+
delete retryConfig.cookies;
|
|
1830
|
+
saveConfig(retryConfig);
|
|
1831
|
+
authedConfig = await ensureApiToken(retryConfig, authUrl, action);
|
|
1832
|
+
response = await send(apiUrl, authedConfig);
|
|
1833
|
+
}
|
|
1834
|
+
return response;
|
|
1835
|
+
}
|
|
1836
|
+
async function resolveDbAuthOrigin(config, args) {
|
|
1837
|
+
const manifestApiUrl = readRailcodeManifest(process.cwd())?.manifest.deploy?.apiUrl;
|
|
1838
|
+
return resolveRequiredAuthOrigin({
|
|
1839
|
+
config,
|
|
1840
|
+
args,
|
|
1841
|
+
action: "Data connectors",
|
|
1842
|
+
fallbackApiUrl: manifestApiUrl,
|
|
1843
|
+
missingUrlHint: "Pass --api-url <url>, set deploy.apiUrl in railcode.json, or set RAILCODE_API_URL.",
|
|
1844
|
+
});
|
|
1845
|
+
}
|
|
1846
|
+
function fetchConnections(apiUrl, config) {
|
|
1847
|
+
return dbFetch(apiUrl, config, "/v1/connections", { redirect: "manual" });
|
|
1848
|
+
}
|
|
1849
|
+
function postSql(apiUrl, config, body) {
|
|
1850
|
+
return dbFetch(apiUrl, config, "/v1/sql", {
|
|
1851
|
+
method: "POST",
|
|
1852
|
+
headers: { "Content-Type": "application/json" },
|
|
1853
|
+
body: JSON.stringify(body),
|
|
1854
|
+
redirect: "manual",
|
|
1855
|
+
});
|
|
1856
|
+
}
|
|
1857
|
+
async function dbFetch(apiUrl, config, path, init) {
|
|
1858
|
+
if (!config.apiToken) {
|
|
1859
|
+
throw new CliError("Missing Railcode API token. Run from a terminal once, or set RAILCODE_API_TOKEN.", 1);
|
|
1860
|
+
}
|
|
1861
|
+
const headers = {
|
|
1862
|
+
...(init.headers ?? {}),
|
|
1863
|
+
Authorization: `Bearer ${config.apiToken}`,
|
|
1864
|
+
};
|
|
1865
|
+
try {
|
|
1866
|
+
return await fetch(`${apiUrl}${path}`, { ...init, headers });
|
|
1867
|
+
}
|
|
1868
|
+
catch (error) {
|
|
1869
|
+
throw new CliError(`Could not reach ${apiUrl}. Configure the Railcode URL in railcode.json deploy.apiUrl, RAILCODE_API_URL, or --api-url. ${error instanceof Error ? error.message : String(error)}`, 1);
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1872
|
+
async function readDbResponse(response, action, hint) {
|
|
1873
|
+
if (response.ok)
|
|
1874
|
+
return response.json();
|
|
1875
|
+
const detail = errorDetail(await response.text());
|
|
1876
|
+
if (response.status === 401) {
|
|
1877
|
+
throw new CliError(`${action} login was rejected. Run the command again to log in.`, 1);
|
|
1878
|
+
}
|
|
1879
|
+
if (response.status === 403) {
|
|
1880
|
+
throw new CliError(`${action} denied: ${detail}`, 1);
|
|
1881
|
+
}
|
|
1882
|
+
if (response.status === 502) {
|
|
1883
|
+
throw new CliError(detail || "Database unreachable.", 1);
|
|
1884
|
+
}
|
|
1885
|
+
if (response.status === 400 || response.status === 404) {
|
|
1886
|
+
throw new CliError([detail, hint].filter(Boolean).join(" ") || `${action} failed.`, 1);
|
|
1887
|
+
}
|
|
1888
|
+
throw new CliError(`${action} failed (${response.status}): ${detail}`, 1);
|
|
1889
|
+
}
|
|
1890
|
+
function printConnectors(connectors, asJson) {
|
|
1891
|
+
if (asJson) {
|
|
1892
|
+
console.log(JSON.stringify(connectors, null, 2));
|
|
1893
|
+
return;
|
|
1894
|
+
}
|
|
1895
|
+
if (!connectors || connectors.length === 0) {
|
|
1896
|
+
console.log("No data connectors configured.");
|
|
1897
|
+
return;
|
|
1898
|
+
}
|
|
1899
|
+
console.log(formatTable(["NAME", "ENGINE"], connectors.map((c) => [c.name, c.engine])));
|
|
1900
|
+
}
|
|
1901
|
+
function printSqlResult(result, asJson) {
|
|
1902
|
+
if (asJson) {
|
|
1903
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1904
|
+
return;
|
|
1905
|
+
}
|
|
1906
|
+
const columns = result.columns ?? [];
|
|
1907
|
+
const rows = result.rows ?? [];
|
|
1908
|
+
if (columns.length > 0) {
|
|
1909
|
+
console.log(formatTable(columns, rows));
|
|
1910
|
+
}
|
|
1911
|
+
const count = typeof result.rowcount === "number" ? result.rowcount : rows.length;
|
|
1912
|
+
console.log(`${columns.length > 0 ? "\n" : ""}${count} ${count === 1 ? "row" : "rows"}${result.truncated ? " (truncated to first 1000)" : ""}`);
|
|
1913
|
+
}
|
|
1914
|
+
const CONNECTOR_HELP = `Call admin-configured HTTP service connectors (Stripe, PostHog, …).
|
|
1915
|
+
|
|
1916
|
+
Usage:
|
|
1917
|
+
railcode connector list
|
|
1918
|
+
railcode connector fetch <name> <path> [options]
|
|
1919
|
+
|
|
1920
|
+
Commands:
|
|
1921
|
+
list List service connectors you can call (name + auth type).
|
|
1922
|
+
fetch <name> <path> Proxy an HTTP request through a connector.
|
|
1923
|
+
|
|
1924
|
+
Fetch options:
|
|
1925
|
+
--method <verb> HTTP method (default GET). Must be allowed by the connector.
|
|
1926
|
+
--body <data> Raw request body (the connector sets the Content-Type).
|
|
1927
|
+
--file <path> Read the request body from a file instead.
|
|
1928
|
+
--json Print the raw JSON envelope instead of the body.
|
|
1929
|
+
|
|
1930
|
+
Options:
|
|
1931
|
+
--api-url <url> Railcode auth/API URL. Defaults to saved config or railcode.json.
|
|
1932
|
+
|
|
1933
|
+
Examples:
|
|
1934
|
+
railcode connector list
|
|
1935
|
+
railcode connector fetch stripe /v1/charges
|
|
1936
|
+
railcode connector fetch stripe /v1/charges --method POST --body "amount=100¤cy=usd"
|
|
1937
|
+
`;
|
|
1938
|
+
async function commandConnector(args) {
|
|
1939
|
+
const subcommand = args.rest[0];
|
|
1940
|
+
const rest = { ...args, rest: args.rest.slice(1) };
|
|
1941
|
+
switch (subcommand) {
|
|
1942
|
+
case "list":
|
|
1943
|
+
case "ls":
|
|
1944
|
+
await commandConnectorList(rest);
|
|
1945
|
+
return;
|
|
1946
|
+
case "fetch":
|
|
1947
|
+
case "request":
|
|
1948
|
+
await commandConnectorFetch(rest);
|
|
1949
|
+
return;
|
|
1950
|
+
case undefined:
|
|
1951
|
+
case "help":
|
|
1952
|
+
console.log(CONNECTOR_HELP);
|
|
1953
|
+
return;
|
|
1954
|
+
default:
|
|
1955
|
+
if (booleanOption(args, "help")) {
|
|
1956
|
+
console.log(CONNECTOR_HELP);
|
|
1957
|
+
return;
|
|
1958
|
+
}
|
|
1959
|
+
throw new CliError(`Unknown connector command: ${subcommand}\n\n${CONNECTOR_HELP}`, 2);
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
async function commandConnectorList(args) {
|
|
1963
|
+
if (booleanOption(args, "help") || args.rest[0] === "help") {
|
|
1964
|
+
console.log(CONNECTOR_HELP);
|
|
1965
|
+
return;
|
|
1966
|
+
}
|
|
1967
|
+
rejectUnknownOptions(args, ["help", "json", "apiUrl"], "connector list");
|
|
1968
|
+
if (args.rest.length > 0) {
|
|
1969
|
+
throw new CliError("railcode connector list does not accept positional arguments.", 2);
|
|
1970
|
+
}
|
|
1971
|
+
const response = await dbAuthedRequest(args, "Service connectors list", fetchServiceConnectors);
|
|
1972
|
+
const connectors = (await readDbResponse(response, "Service connectors list"));
|
|
1973
|
+
printServiceConnectors(connectors, booleanOption(args, "json"));
|
|
1974
|
+
}
|
|
1975
|
+
async function commandConnectorFetch(args) {
|
|
1976
|
+
if (booleanOption(args, "help") || args.rest[0] === "help") {
|
|
1977
|
+
console.log(CONNECTOR_HELP);
|
|
1978
|
+
return;
|
|
1979
|
+
}
|
|
1980
|
+
rejectUnknownOptions(args, ["help", "json", "apiUrl", "method", "body", "file"], "connector fetch");
|
|
1981
|
+
const name = args.rest[0];
|
|
1982
|
+
const path = args.rest[1];
|
|
1983
|
+
if (!name || !path) {
|
|
1984
|
+
throw new CliError("Usage: railcode connector fetch <name> <path> [--method <verb>] [--body <data>].", 2);
|
|
1985
|
+
}
|
|
1986
|
+
if (args.rest.length > 2) {
|
|
1987
|
+
throw new CliError("railcode connector fetch takes a single connector name and path.", 2);
|
|
1988
|
+
}
|
|
1989
|
+
if (!path.startsWith("/")) {
|
|
1990
|
+
throw new CliError("The connector path must start with '/'.", 2);
|
|
1991
|
+
}
|
|
1992
|
+
const method = (stringOption(args, "method") || "GET").trim().toUpperCase();
|
|
1993
|
+
const body = resolveConnectorBody(args);
|
|
1994
|
+
const requestBody = { connector: name, method, path, body };
|
|
1995
|
+
const response = await dbAuthedRequest(args, "Connector request", (apiUrl, config) => postConnectorRequest(apiUrl, config, requestBody));
|
|
1996
|
+
const result = (await readDbResponse(response, "Connector request", "Run `railcode connector list` to see connectors you can call."));
|
|
1997
|
+
printConnectorResponse(result, booleanOption(args, "json"));
|
|
1998
|
+
}
|
|
1999
|
+
function resolveConnectorBody(args) {
|
|
2000
|
+
const file = stringOption(args, "file");
|
|
2001
|
+
const inline = stringOption(args, "body");
|
|
2002
|
+
if (file && inline !== undefined) {
|
|
2003
|
+
throw new CliError("Pass --body or --file, not both.", 2);
|
|
2004
|
+
}
|
|
2005
|
+
if (file) {
|
|
2006
|
+
const path = resolve(file);
|
|
2007
|
+
if (!existsSync(path))
|
|
2008
|
+
throw new CliError(`Body file not found: ${path}`, 1);
|
|
2009
|
+
return readFileSync(path, "utf8");
|
|
2010
|
+
}
|
|
2011
|
+
return inline;
|
|
2012
|
+
}
|
|
2013
|
+
function fetchServiceConnectors(apiUrl, config) {
|
|
2014
|
+
return dbFetch(apiUrl, config, "/v1/service-connectors", { redirect: "manual" });
|
|
2015
|
+
}
|
|
2016
|
+
function postConnectorRequest(apiUrl, config, body) {
|
|
2017
|
+
return dbFetch(apiUrl, config, "/v1/service-connectors/request", {
|
|
2018
|
+
method: "POST",
|
|
2019
|
+
headers: { "Content-Type": "application/json" },
|
|
2020
|
+
body: JSON.stringify(body),
|
|
2021
|
+
redirect: "manual",
|
|
2022
|
+
});
|
|
2023
|
+
}
|
|
2024
|
+
function printServiceConnectors(connectors, asJson) {
|
|
2025
|
+
if (asJson) {
|
|
2026
|
+
console.log(JSON.stringify(connectors, null, 2));
|
|
2027
|
+
return;
|
|
2028
|
+
}
|
|
2029
|
+
if (!connectors || connectors.length === 0) {
|
|
2030
|
+
console.log("No service connectors available.");
|
|
2031
|
+
return;
|
|
2032
|
+
}
|
|
2033
|
+
console.log(formatTable(["NAME", "AUTH", "METHODS"], connectors.map((c) => [c.name, c.auth_type ?? "none", (c.allowed_methods ?? []).join(",")])));
|
|
2034
|
+
}
|
|
2035
|
+
function printConnectorResponse(result, asJson) {
|
|
2036
|
+
if (asJson) {
|
|
2037
|
+
console.log(JSON.stringify(result, null, 2));
|
|
2038
|
+
return;
|
|
2039
|
+
}
|
|
2040
|
+
const status = result.status ?? 0;
|
|
2041
|
+
console.log(`${status} ${result.ok ? "OK" : "ERROR"}${result.truncated ? " (truncated)" : ""}`);
|
|
2042
|
+
if (result.body)
|
|
2043
|
+
console.log(result.body);
|
|
2044
|
+
}
|
|
2045
|
+
function rejectUnknownOptions(args, allowed, label) {
|
|
2046
|
+
const set = new Set(allowed);
|
|
2047
|
+
const unknown = Object.keys(args.options).filter((option) => !set.has(option));
|
|
2048
|
+
if (unknown.length > 0) {
|
|
2049
|
+
throw new CliError(`Unknown option for ${label}: --${unknown[0]}`, 2);
|
|
2050
|
+
}
|
|
2051
|
+
}
|
|
2052
|
+
function formatTable(columns, rows) {
|
|
2053
|
+
const header = columns.map((c) => c ?? "");
|
|
2054
|
+
const body = rows.map((row) => header.map((_, i) => formatCell(row?.[i])));
|
|
2055
|
+
const widths = header.map((h, i) => Math.max(h.length, ...body.map((r) => r[i].length), 0));
|
|
2056
|
+
const renderRow = (cells) => cells.map((cell, i) => cell.padEnd(widths[i])).join(" ").replace(/\s+$/, "");
|
|
2057
|
+
const lines = [renderRow(header), renderRow(widths.map((w) => "-".repeat(w)))];
|
|
2058
|
+
for (const row of body)
|
|
2059
|
+
lines.push(renderRow(row));
|
|
2060
|
+
return lines.join("\n");
|
|
2061
|
+
}
|
|
2062
|
+
function formatCell(value) {
|
|
2063
|
+
if (value === null || value === undefined)
|
|
2064
|
+
return "";
|
|
2065
|
+
const text = typeof value === "string"
|
|
2066
|
+
? value
|
|
2067
|
+
: typeof value === "object"
|
|
2068
|
+
? JSON.stringify(value)
|
|
2069
|
+
: String(value);
|
|
2070
|
+
return text.replace(/[\r\n\t]+/g, " ");
|
|
2071
|
+
}
|
|
1597
2072
|
const DESIGN_SYSTEM_HELP = `Usage:
|
|
1598
2073
|
railcode get design-system
|
|
1599
2074
|
|
package/package.json
CHANGED
package/static/sdk.js
CHANGED
|
@@ -371,6 +371,39 @@
|
|
|
371
371
|
var dataConnectors = () => track("connections", "dataConnectors()", () => call("GET", "/connections"));
|
|
372
372
|
var databaseConnectors = dataConnectors;
|
|
373
373
|
|
|
374
|
+
// src/service-connectors.ts
|
|
375
|
+
var PATH_LABEL_MAX = 60;
|
|
376
|
+
var toResponse = (env) => {
|
|
377
|
+
const body = env.body ?? "";
|
|
378
|
+
return {
|
|
379
|
+
status: env.status,
|
|
380
|
+
ok: env.ok,
|
|
381
|
+
headers: env.headers || {},
|
|
382
|
+
truncated: Boolean(env.truncated),
|
|
383
|
+
text: () => Promise.resolve(body),
|
|
384
|
+
json: () => Promise.resolve(JSON.parse(body))
|
|
385
|
+
};
|
|
386
|
+
};
|
|
387
|
+
var request = (name, path, opts) => {
|
|
388
|
+
const method = (opts?.method || "GET").toUpperCase();
|
|
389
|
+
const label = `connector(${q(name)}).fetch(${q(method)} ${q(shorten(path, PATH_LABEL_MAX))})`;
|
|
390
|
+
return track(
|
|
391
|
+
"connector",
|
|
392
|
+
label,
|
|
393
|
+
() => call("POST", "/service-connectors/request", {
|
|
394
|
+
json: { connector: name, method, path, body: opts?.body }
|
|
395
|
+
}).then(toResponse)
|
|
396
|
+
);
|
|
397
|
+
};
|
|
398
|
+
var connector = (name) => ({
|
|
399
|
+
fetch: (path, opts) => request(name, path, opts)
|
|
400
|
+
});
|
|
401
|
+
var serviceConnectors = () => track(
|
|
402
|
+
"service-connectors",
|
|
403
|
+
"serviceConnectors()",
|
|
404
|
+
() => call("GET", "/service-connectors")
|
|
405
|
+
);
|
|
406
|
+
|
|
374
407
|
// src/design-system.ts
|
|
375
408
|
var designSystem = () => track("design-system", "designSystem()", async () => {
|
|
376
409
|
const response = await call("GET", "/config/design-system");
|
|
@@ -437,6 +470,8 @@
|
|
|
437
470
|
window.mysql = mysql;
|
|
438
471
|
window.dataConnectors = dataConnectors;
|
|
439
472
|
window.databaseConnectors = databaseConnectors;
|
|
473
|
+
window.connector = connector;
|
|
474
|
+
window.serviceConnectors = serviceConnectors;
|
|
440
475
|
window.designSystem = designSystem;
|
|
441
476
|
window.me = me;
|
|
442
477
|
window.appUsers = appUsers;
|