railcode 0.1.12 → 0.1.14
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 +3 -3
- package/dist/index.js +343 -61
- package/dist/query.js +94 -0
- package/package.json +1 -1
- package/static/sdk.js +26 -10
package/README.md
CHANGED
|
@@ -36,9 +36,9 @@ railcode db <list|query> ... List data connectors / run read-only SQL
|
|
|
36
36
|
build command when needed, then uploads the output dir. The app is
|
|
37
37
|
created-or-resolved by slug in your saved org, and the live URL is printed.
|
|
38
38
|
- **`db`** inspects the org's **data connectors** (per-org Postgres) and runs
|
|
39
|
-
ad-hoc read-only SQL.
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
ad-hoc read-only SQL. It works straight after `railcode login` — **no app and no
|
|
40
|
+
`railcode.json` required** — since connectors are org-scoped; it hits the app-less
|
|
41
|
+
`/api/organizations/{org}/data/*` plane with your login token.
|
|
42
42
|
`railcode db list` (aliases `ls`, `connections`) prints each connector's
|
|
43
43
|
`name` + `engine` (`--json` for the raw array). `railcode db query "<sql>"`
|
|
44
44
|
(alias `sql`) runs SQL against `--connection` (default `default`) and prints a
|
package/dist/index.js
CHANGED
|
@@ -12,6 +12,7 @@ import { createInterface } from "node:readline/promises";
|
|
|
12
12
|
import { fileURLToPath } from "node:url";
|
|
13
13
|
import { KvQueryError, kvCount, kvQuery, } from "./dev/kv-query.js";
|
|
14
14
|
import { DbError, formatTable, inferEngine, parseEngine, parseSqlParams, pickSqlSource, } from "./db.js";
|
|
15
|
+
import { QueryCmdError, paramSignature, parseParamDecls, parseRunParams, pickAuthoringSql, } from "./query.js";
|
|
15
16
|
import { ConnectorError, buildConnectorRequest, connectorOpenapiOutput, connectorTable, parseConnectorOption, parseMethodOption, pickConnectorBody, proxyExitCode, renderConnectorDocs, renderProxyResponse, } from "./connectors.js";
|
|
16
17
|
// ---------------------------------------------------------------------------
|
|
17
18
|
// Constants
|
|
@@ -35,6 +36,7 @@ Usage:
|
|
|
35
36
|
railcode init <app> [--template static|react]
|
|
36
37
|
railcode design-system Print your org's design-system guidance (markdown)
|
|
37
38
|
railcode db <list|query> ... List data connectors / run read-only SQL
|
|
39
|
+
railcode query <list|run|create|update|delete> ... Saved queries: invoke by name / author (admin)
|
|
38
40
|
railcode connector <list|docs|fetch> ... List connectors / show API docs / proxy a call
|
|
39
41
|
railcode --version
|
|
40
42
|
railcode --help
|
|
@@ -76,6 +78,10 @@ async function main() {
|
|
|
76
78
|
case "db":
|
|
77
79
|
await commandDb(args);
|
|
78
80
|
return;
|
|
81
|
+
case "query":
|
|
82
|
+
case "queries":
|
|
83
|
+
await commandQuery(args);
|
|
84
|
+
return;
|
|
79
85
|
case "connector":
|
|
80
86
|
case "connectors":
|
|
81
87
|
await commandConnector(args);
|
|
@@ -101,13 +107,13 @@ function parseArgs(argv) {
|
|
|
101
107
|
const withoutPrefix = item.slice(2);
|
|
102
108
|
const eq = withoutPrefix.indexOf("=");
|
|
103
109
|
if (eq !== -1) {
|
|
104
|
-
options
|
|
110
|
+
setOption(options, toCamel(withoutPrefix.slice(0, eq)), withoutPrefix.slice(eq + 1));
|
|
105
111
|
continue;
|
|
106
112
|
}
|
|
107
113
|
const key = toCamel(withoutPrefix);
|
|
108
114
|
const next = tail[i + 1];
|
|
109
115
|
if (next && !next.startsWith("--")) {
|
|
110
|
-
options
|
|
116
|
+
setOption(options, key, next);
|
|
111
117
|
i += 1;
|
|
112
118
|
}
|
|
113
119
|
else {
|
|
@@ -116,6 +122,20 @@ function parseArgs(argv) {
|
|
|
116
122
|
}
|
|
117
123
|
return { command, rest, options };
|
|
118
124
|
}
|
|
125
|
+
// A string option given twice accumulates into an array (optionString then
|
|
126
|
+
// rejects it with "can only be given once"); a repeated flag just stays true.
|
|
127
|
+
function setOption(options, key, value) {
|
|
128
|
+
const existing = options[key];
|
|
129
|
+
if (typeof existing === "string") {
|
|
130
|
+
options[key] = [existing, value];
|
|
131
|
+
}
|
|
132
|
+
else if (Array.isArray(existing)) {
|
|
133
|
+
existing.push(value);
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
options[key] = value;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
119
139
|
function toCamel(input) {
|
|
120
140
|
return input.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
121
141
|
}
|
|
@@ -174,9 +194,15 @@ async function authorizeCliWithBrowser(apiUrl) {
|
|
|
174
194
|
redirect_uri: callback.redirectUri,
|
|
175
195
|
state,
|
|
176
196
|
})}`;
|
|
177
|
-
console.log("Open this link to authorize the Railcode CLI:");
|
|
178
|
-
console.log(authorizeUrl);
|
|
179
197
|
console.log("");
|
|
198
|
+
printUrlBox("Railcode login", authorizeUrl);
|
|
199
|
+
console.log("");
|
|
200
|
+
if (openBrowser(authorizeUrl)) {
|
|
201
|
+
console.log("Opening your browser. If it does not open, paste the URL above.");
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
console.log("Paste the URL above into your browser to continue.");
|
|
205
|
+
}
|
|
180
206
|
console.log("Waiting for browser authorization...");
|
|
181
207
|
const code = await callback.code;
|
|
182
208
|
return await exchangeCliAuthCode(apiUrl, code);
|
|
@@ -267,6 +293,25 @@ async function startCliAuthCallback(expectedState) {
|
|
|
267
293
|
},
|
|
268
294
|
};
|
|
269
295
|
}
|
|
296
|
+
function openBrowser(url) {
|
|
297
|
+
const opener = process.platform === "darwin"
|
|
298
|
+
? { command: "open", args: [url] }
|
|
299
|
+
: process.platform === "win32"
|
|
300
|
+
? { command: "cmd", args: ["/c", "start", "", url] }
|
|
301
|
+
: { command: "xdg-open", args: [url] };
|
|
302
|
+
try {
|
|
303
|
+
const child = spawn(opener.command, opener.args, {
|
|
304
|
+
detached: true,
|
|
305
|
+
stdio: "ignore",
|
|
306
|
+
});
|
|
307
|
+
child.on("error", () => { });
|
|
308
|
+
child.unref();
|
|
309
|
+
return true;
|
|
310
|
+
}
|
|
311
|
+
catch {
|
|
312
|
+
return false;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
270
315
|
function sendBrowserCallback(response, status, message) {
|
|
271
316
|
response.writeHead(status, { "Content-Type": "text/html" });
|
|
272
317
|
response.end(`<!doctype html><title>Railcode CLI</title><p>${escapeHtml(message)}</p>`);
|
|
@@ -353,17 +398,6 @@ async function resolveOrCreateApp(config, slug) {
|
|
|
353
398
|
});
|
|
354
399
|
return (await readJsonOrThrow(created, "Create app"));
|
|
355
400
|
}
|
|
356
|
-
// Resolve an existing app by slug WITHOUT creating it — for read-only commands
|
|
357
|
-
// (e.g. `railcode db`) that must not spawn a phantom app as a side effect.
|
|
358
|
-
async function resolveExistingApp(config, slug) {
|
|
359
|
-
const orgPath = `/api/organizations/${config.orgUuid}/apps`;
|
|
360
|
-
const list = (await readJsonOrThrow(await authedFetch(config, orgPath), "List apps"));
|
|
361
|
-
const existing = list.find((app) => app.app_slug === slug);
|
|
362
|
-
if (!existing) {
|
|
363
|
-
throw new CliError(`App "${slug}" hasn't been created in your org yet. Run \`railcode deploy\` first.`, 1);
|
|
364
|
-
}
|
|
365
|
-
return existing;
|
|
366
|
-
}
|
|
367
401
|
async function postDeploy(config, appUuid, files) {
|
|
368
402
|
// The deploy API is multipart: one part named "files" per file, with the
|
|
369
403
|
// relative path carried as the part filename.
|
|
@@ -815,7 +849,7 @@ Query options:
|
|
|
815
849
|
--json Print the raw { columns, rows, rowcount, truncated } envelope
|
|
816
850
|
|
|
817
851
|
SQL is sent read-only with bound parameters — values are never interpolated into
|
|
818
|
-
the query.
|
|
852
|
+
the query. Works right after \`railcode login\` — no app or ${MANIFEST_NAME} required.
|
|
819
853
|
`;
|
|
820
854
|
async function commandDb(args) {
|
|
821
855
|
try {
|
|
@@ -851,8 +885,7 @@ async function commandDbList(args) {
|
|
|
851
885
|
throw new CliError(`railcode db list takes no arguments (got "${args.rest[1]}").`, 2);
|
|
852
886
|
}
|
|
853
887
|
const config = requireLogin(args);
|
|
854
|
-
const
|
|
855
|
-
const connections = await fetchDbConnections(config, app.uuid);
|
|
888
|
+
const connections = await fetchDbConnections(config);
|
|
856
889
|
if (args.options.json) {
|
|
857
890
|
console.log(JSON.stringify(connections, null, 2));
|
|
858
891
|
return;
|
|
@@ -868,16 +901,15 @@ async function commandDbQuery(args) {
|
|
|
868
901
|
if (args.rest.length > 2) {
|
|
869
902
|
throw new CliError('railcode db query takes a single SQL string — quote it: railcode db query "select 1".', 2);
|
|
870
903
|
}
|
|
871
|
-
const source = pickSqlSource(args.rest[1], args
|
|
904
|
+
const source = pickSqlSource(args.rest[1], singleOption(args, "file"));
|
|
872
905
|
const params = parseSqlParams(optionString(args, "params"));
|
|
873
|
-
const engineOption = parseEngine(args
|
|
906
|
+
const engineOption = parseEngine(singleOption(args, "engine"));
|
|
874
907
|
const connection = optionString(args, "connection") ?? "default";
|
|
875
908
|
const query = source.kind === "file" ? readSqlFile(source.path) : source.sql;
|
|
876
909
|
const config = requireLogin(args);
|
|
877
|
-
const app = await resolveExistingApp(config, requireAppSlug());
|
|
878
910
|
// --engine wins; otherwise look the engine up from the connector list.
|
|
879
|
-
const engine = engineOption ?? inferEngine(await fetchDbConnections(config
|
|
880
|
-
const result = await runDbQuery(config,
|
|
911
|
+
const engine = engineOption ?? inferEngine(await fetchDbConnections(config), connection);
|
|
912
|
+
const result = await runDbQuery(config, { engine, connection, query, params });
|
|
881
913
|
if (args.options.json) {
|
|
882
914
|
console.log(JSON.stringify(result, null, 2));
|
|
883
915
|
return;
|
|
@@ -889,22 +921,14 @@ async function commandDbQuery(args) {
|
|
|
889
921
|
console.log("Note: results were truncated by the server.");
|
|
890
922
|
}
|
|
891
923
|
}
|
|
892
|
-
function
|
|
893
|
-
const
|
|
894
|
-
if (!manifest?.app) {
|
|
895
|
-
throw new CliError(`No ${MANIFEST_NAME} with an "app" slug here. Run \`railcode init <app>\` first, or add ${MANIFEST_NAME}.`, 2);
|
|
896
|
-
}
|
|
897
|
-
assertAppName(manifest.app);
|
|
898
|
-
return manifest.app;
|
|
899
|
-
}
|
|
900
|
-
async function fetchDbConnections(config, appUuid) {
|
|
901
|
-
const resp = await authedFetch(config, `/api/organizations/${config.orgUuid}/apps/${appUuid}/connections`);
|
|
924
|
+
async function fetchDbConnections(config) {
|
|
925
|
+
const resp = await authedFetch(config, `/api/organizations/${config.orgUuid}/data/connections`);
|
|
902
926
|
if (resp.status === 401)
|
|
903
927
|
await reLoginOrThrow();
|
|
904
928
|
return (await readJsonOrThrow(resp, "List connections"));
|
|
905
929
|
}
|
|
906
|
-
async function runDbQuery(config,
|
|
907
|
-
const resp = await authedFetch(config, `/api/organizations/${config.orgUuid}/
|
|
930
|
+
async function runDbQuery(config, body) {
|
|
931
|
+
const resp = await authedFetch(config, `/api/organizations/${config.orgUuid}/data/sql`, {
|
|
908
932
|
method: "POST",
|
|
909
933
|
headers: { "Content-Type": "application/json" },
|
|
910
934
|
body: JSON.stringify(body),
|
|
@@ -926,8 +950,20 @@ function optionString(args, key) {
|
|
|
926
950
|
const value = args.options[key];
|
|
927
951
|
if (value === true)
|
|
928
952
|
throw new CliError(`--${camelToKebab(key)} requires a value.`, 2);
|
|
953
|
+
if (Array.isArray(value)) {
|
|
954
|
+
throw new CliError(`--${camelToKebab(key)} can only be given once.`, 2);
|
|
955
|
+
}
|
|
929
956
|
return typeof value === "string" ? value : undefined;
|
|
930
957
|
}
|
|
958
|
+
// A single-valued option that may legitimately be a bare flag (the db/connector
|
|
959
|
+
// parsers handle `true` themselves) — only a REPEATED option is rejected here.
|
|
960
|
+
function singleOption(args, key) {
|
|
961
|
+
const value = args.options[key];
|
|
962
|
+
if (Array.isArray(value)) {
|
|
963
|
+
throw new CliError(`--${camelToKebab(key)} can only be given once.`, 2);
|
|
964
|
+
}
|
|
965
|
+
return value;
|
|
966
|
+
}
|
|
931
967
|
function assertKnownDbOptions(args, allowed) {
|
|
932
968
|
for (const key of Object.keys(args.options)) {
|
|
933
969
|
if (!allowed.includes(key)) {
|
|
@@ -939,13 +975,260 @@ function camelToKebab(value) {
|
|
|
939
975
|
return value.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`);
|
|
940
976
|
}
|
|
941
977
|
// ---------------------------------------------------------------------------
|
|
978
|
+
// query — saved queries (`railcode query list|run|create|delete`)
|
|
979
|
+
//
|
|
980
|
+
// list/run hit the org-scoped `/data/queries` plane with the personal token
|
|
981
|
+
// (subject to `saved_query` grants; `_ctx_*` binds resolve to YOU). create/delete
|
|
982
|
+
// hit the admin management API (`/saved-queries`, connection:manage required).
|
|
983
|
+
// ---------------------------------------------------------------------------
|
|
984
|
+
const QUERY_HELP = `railcode query — saved queries
|
|
985
|
+
|
|
986
|
+
Usage:
|
|
987
|
+
railcode query list List this org's saved queries
|
|
988
|
+
railcode query run <name> [--params <json>] Invoke a saved query by name
|
|
989
|
+
railcode query create --name <n> --connection <c> (--sql <sql> | --sql-file <f>)
|
|
990
|
+
[--params <json>] [--description <d>]
|
|
991
|
+
Author a saved query (admin)
|
|
992
|
+
railcode query update <name> [--sql <sql> | --sql-file <f>] [--params <json>]
|
|
993
|
+
[--clear-params] [--connection <c>] [--description <d>]
|
|
994
|
+
Edit a saved query in place (admin;
|
|
995
|
+
omitted fields keep their value)
|
|
996
|
+
railcode query delete <name> Delete a saved query (admin)
|
|
997
|
+
|
|
998
|
+
Aliases: query = queries; list = ls; run = invoke.
|
|
999
|
+
|
|
1000
|
+
Run options:
|
|
1001
|
+
--params '<json-object>' Caller params, exactly as the API takes
|
|
1002
|
+
them: '{"region":"emea","limit":5}'.
|
|
1003
|
+
Params declared with a default may be
|
|
1004
|
+
omitted (the server binds the default).
|
|
1005
|
+
--json Print the raw { columns, rows, ... } envelope
|
|
1006
|
+
|
|
1007
|
+
Create / update options:
|
|
1008
|
+
--name <name> Invoke handle ([a-z0-9_], unique per org)
|
|
1009
|
+
--connection <name> Data connection the query runs against
|
|
1010
|
+
--sql <sql> The SQL template inline (:param style);
|
|
1011
|
+
quote it, or use --sql="..." if it starts
|
|
1012
|
+
with a -- comment
|
|
1013
|
+
--sql-file <path> File holding the SQL template
|
|
1014
|
+
--params '<json-array>' Declared params in the API shape:
|
|
1015
|
+
'[{"name":"region","type":"string","default":"emea"},
|
|
1016
|
+
{"name":"limit","type":"int"}]'
|
|
1017
|
+
Types: string, int, float, bool. A "default"
|
|
1018
|
+
makes the param optional at invoke time.
|
|
1019
|
+
On update the array REPLACES the params
|
|
1020
|
+
--clear-params (update only) declare no params
|
|
1021
|
+
--description <text> Shown to members next to the name
|
|
1022
|
+
|
|
1023
|
+
Editing the SQL or params bumps the query's version; grants naming the query are
|
|
1024
|
+
untouched (unlike delete + recreate, which removes them).
|
|
1025
|
+
|
|
1026
|
+
Templates may reference :_ctx_user_id, :_ctx_user_email and :_ctx_org — the server
|
|
1027
|
+
injects those from whoever invokes (they can never be passed as params), so a
|
|
1028
|
+
query like "where rep_email = :_ctx_user_email" is row-scoped per caller.
|
|
1029
|
+
`;
|
|
1030
|
+
async function commandQuery(args) {
|
|
1031
|
+
try {
|
|
1032
|
+
const sub = args.rest[0] ?? "";
|
|
1033
|
+
switch (sub) {
|
|
1034
|
+
case "list":
|
|
1035
|
+
case "ls":
|
|
1036
|
+
await commandQueryList(args);
|
|
1037
|
+
return;
|
|
1038
|
+
case "run":
|
|
1039
|
+
case "invoke":
|
|
1040
|
+
await commandQueryRun(args);
|
|
1041
|
+
return;
|
|
1042
|
+
case "create":
|
|
1043
|
+
await commandQueryCreate(args);
|
|
1044
|
+
return;
|
|
1045
|
+
case "update":
|
|
1046
|
+
await commandQueryUpdate(args);
|
|
1047
|
+
return;
|
|
1048
|
+
case "delete":
|
|
1049
|
+
await commandQueryDelete(args);
|
|
1050
|
+
return;
|
|
1051
|
+
case "":
|
|
1052
|
+
case "help":
|
|
1053
|
+
console.log(QUERY_HELP);
|
|
1054
|
+
return;
|
|
1055
|
+
default:
|
|
1056
|
+
throw new CliError(`Unknown query command: ${sub}\n\n${QUERY_HELP}`, 2);
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
catch (error) {
|
|
1060
|
+
if (error instanceof QueryCmdError)
|
|
1061
|
+
throw new CliError(error.message, 2);
|
|
1062
|
+
throw error;
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
async function commandQueryList(args) {
|
|
1066
|
+
assertKnownQueryOptions(args, ["json", "apiUrl"]);
|
|
1067
|
+
if (args.rest.length > 1) {
|
|
1068
|
+
throw new CliError(`railcode query list takes no arguments (got "${args.rest[1]}").`, 2);
|
|
1069
|
+
}
|
|
1070
|
+
const config = requireLogin(args);
|
|
1071
|
+
const resp = await authedFetch(config, `/api/organizations/${config.orgUuid}/data/queries`);
|
|
1072
|
+
if (resp.status === 401)
|
|
1073
|
+
await reLoginOrThrow();
|
|
1074
|
+
const queries = (await readJsonOrThrow(resp, "List saved queries"));
|
|
1075
|
+
if (args.options.json) {
|
|
1076
|
+
console.log(JSON.stringify(queries, null, 2));
|
|
1077
|
+
return;
|
|
1078
|
+
}
|
|
1079
|
+
if (queries.length === 0) {
|
|
1080
|
+
console.log("No saved queries in this organization.");
|
|
1081
|
+
return;
|
|
1082
|
+
}
|
|
1083
|
+
console.log(formatTable(["name", "params", "version", "description"], queries.map((query) => [
|
|
1084
|
+
query.name,
|
|
1085
|
+
paramSignature(query.params),
|
|
1086
|
+
`v${query.version}`,
|
|
1087
|
+
query.description || "-",
|
|
1088
|
+
])));
|
|
1089
|
+
}
|
|
1090
|
+
async function commandQueryRun(args) {
|
|
1091
|
+
assertKnownQueryOptions(args, ["params", "json", "apiUrl"]);
|
|
1092
|
+
const name = args.rest[1];
|
|
1093
|
+
if (!name || args.rest.length > 2) {
|
|
1094
|
+
throw new CliError(`Usage: railcode query run <name> [--params '{"k":"v"}']`, 2);
|
|
1095
|
+
}
|
|
1096
|
+
const params = parseRunParams(optionString(args, "params"));
|
|
1097
|
+
const config = requireLogin(args);
|
|
1098
|
+
const resp = await authedFetch(config, `/api/organizations/${config.orgUuid}/data/queries/${encodeURIComponent(name)}`, {
|
|
1099
|
+
method: "POST",
|
|
1100
|
+
headers: { "Content-Type": "application/json" },
|
|
1101
|
+
body: JSON.stringify({ params }),
|
|
1102
|
+
});
|
|
1103
|
+
if (resp.status === 401)
|
|
1104
|
+
await reLoginOrThrow();
|
|
1105
|
+
const result = (await readJsonOrThrow(resp, `Run saved query "${name}"`));
|
|
1106
|
+
if (args.options.json) {
|
|
1107
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1108
|
+
return;
|
|
1109
|
+
}
|
|
1110
|
+
console.log(formatTable(result.columns, result.rows));
|
|
1111
|
+
console.log("");
|
|
1112
|
+
console.log(`(${result.rowcount} row${result.rowcount === 1 ? "" : "s"})`);
|
|
1113
|
+
if (result.truncated) {
|
|
1114
|
+
console.log("Note: results were truncated by the server.");
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
async function commandQueryCreate(args) {
|
|
1118
|
+
assertKnownQueryOptions(args, [
|
|
1119
|
+
"name",
|
|
1120
|
+
"connection",
|
|
1121
|
+
"sql",
|
|
1122
|
+
"sqlFile",
|
|
1123
|
+
"params",
|
|
1124
|
+
"description",
|
|
1125
|
+
"apiUrl",
|
|
1126
|
+
]);
|
|
1127
|
+
const name = optionString(args, "name");
|
|
1128
|
+
const connection = optionString(args, "connection");
|
|
1129
|
+
if (!name || !connection) {
|
|
1130
|
+
throw new CliError("railcode query create requires --name and --connection.\n\n" + QUERY_HELP, 2);
|
|
1131
|
+
}
|
|
1132
|
+
// required=true: pickAuthoringSql throws rather than return null here.
|
|
1133
|
+
const source = pickAuthoringSql(optionString(args, "sql"), optionString(args, "sqlFile"), true);
|
|
1134
|
+
const params = parseParamDecls(optionString(args, "params"));
|
|
1135
|
+
const sql = source.kind === "file" ? readSqlFile(source.path) : source.sql;
|
|
1136
|
+
const config = requireLogin(args);
|
|
1137
|
+
const resp = await authedFetch(config, `/api/organizations/${config.orgUuid}/saved-queries`, {
|
|
1138
|
+
method: "POST",
|
|
1139
|
+
headers: { "Content-Type": "application/json" },
|
|
1140
|
+
body: JSON.stringify({
|
|
1141
|
+
name,
|
|
1142
|
+
connection,
|
|
1143
|
+
sql,
|
|
1144
|
+
params,
|
|
1145
|
+
description: optionString(args, "description") ?? "",
|
|
1146
|
+
}),
|
|
1147
|
+
});
|
|
1148
|
+
if (resp.status === 401)
|
|
1149
|
+
await reLoginOrThrow();
|
|
1150
|
+
const created = (await readJsonOrThrow(resp, `Create saved query "${name}"`));
|
|
1151
|
+
console.log(`Saved query "${created.name}" created (v${created.version}).`);
|
|
1152
|
+
console.log(`Invoke it: railcode query run ${created.name}`);
|
|
1153
|
+
}
|
|
1154
|
+
async function commandQueryUpdate(args) {
|
|
1155
|
+
assertKnownQueryOptions(args, [
|
|
1156
|
+
"sql",
|
|
1157
|
+
"sqlFile",
|
|
1158
|
+
"params",
|
|
1159
|
+
"clearParams",
|
|
1160
|
+
"connection",
|
|
1161
|
+
"description",
|
|
1162
|
+
"apiUrl",
|
|
1163
|
+
]);
|
|
1164
|
+
const name = args.rest[1];
|
|
1165
|
+
if (!name || args.rest.length > 2) {
|
|
1166
|
+
throw new CliError("Usage: railcode query update <name> [--sql ... | --sql-file ...]", 2);
|
|
1167
|
+
}
|
|
1168
|
+
const source = pickAuthoringSql(optionString(args, "sql"), optionString(args, "sqlFile"), false);
|
|
1169
|
+
const sql = source ? (source.kind === "file" ? readSqlFile(source.path) : source.sql) : undefined;
|
|
1170
|
+
const paramsJson = optionString(args, "params");
|
|
1171
|
+
const clearParams = args.options.clearParams === true;
|
|
1172
|
+
if (clearParams && paramsJson !== undefined) {
|
|
1173
|
+
throw new CliError("--clear-params cannot be combined with --params.", 2);
|
|
1174
|
+
}
|
|
1175
|
+
const params = clearParams ? [] : paramsJson !== undefined ? parseParamDecls(paramsJson) : undefined;
|
|
1176
|
+
const connection = optionString(args, "connection");
|
|
1177
|
+
const description = optionString(args, "description");
|
|
1178
|
+
// PATCH semantics: only the fields present in the body change; params replace
|
|
1179
|
+
// the whole declared list (the backend re-validates them against the SQL).
|
|
1180
|
+
const body = {};
|
|
1181
|
+
if (connection !== undefined)
|
|
1182
|
+
body.connection = connection;
|
|
1183
|
+
if (sql !== undefined)
|
|
1184
|
+
body.sql = sql;
|
|
1185
|
+
if (params !== undefined)
|
|
1186
|
+
body.params = params;
|
|
1187
|
+
if (description !== undefined)
|
|
1188
|
+
body.description = description;
|
|
1189
|
+
if (Object.keys(body).length === 0) {
|
|
1190
|
+
throw new CliError("Nothing to update. Pass --sql/--sql-file, --params/--clear-params, --connection or --description.", 2);
|
|
1191
|
+
}
|
|
1192
|
+
const config = requireLogin(args);
|
|
1193
|
+
const resp = await authedFetch(config, `/api/organizations/${config.orgUuid}/saved-queries/${encodeURIComponent(name)}`, {
|
|
1194
|
+
method: "PATCH",
|
|
1195
|
+
headers: { "Content-Type": "application/json" },
|
|
1196
|
+
body: JSON.stringify(body),
|
|
1197
|
+
});
|
|
1198
|
+
if (resp.status === 401)
|
|
1199
|
+
await reLoginOrThrow();
|
|
1200
|
+
const updated = (await readJsonOrThrow(resp, `Update saved query "${name}"`));
|
|
1201
|
+
console.log(`Saved query "${updated.name}" updated (v${updated.version}).`);
|
|
1202
|
+
}
|
|
1203
|
+
async function commandQueryDelete(args) {
|
|
1204
|
+
assertKnownQueryOptions(args, ["apiUrl"]);
|
|
1205
|
+
const name = args.rest[1];
|
|
1206
|
+
if (!name || args.rest.length > 2) {
|
|
1207
|
+
throw new CliError("Usage: railcode query delete <name>", 2);
|
|
1208
|
+
}
|
|
1209
|
+
const config = requireLogin(args);
|
|
1210
|
+
const resp = await authedFetch(config, `/api/organizations/${config.orgUuid}/saved-queries/${encodeURIComponent(name)}`, { method: "DELETE" });
|
|
1211
|
+
if (resp.status === 401)
|
|
1212
|
+
await reLoginOrThrow();
|
|
1213
|
+
if (!resp.ok) {
|
|
1214
|
+
throw new CliError(`Delete saved query "${name}" failed (${resp.status}): ${await errorDetail(resp)}`, 1);
|
|
1215
|
+
}
|
|
1216
|
+
console.log(`Saved query "${name}" deleted (its grants were removed too).`);
|
|
1217
|
+
}
|
|
1218
|
+
function assertKnownQueryOptions(args, allowed) {
|
|
1219
|
+
for (const key of Object.keys(args.options)) {
|
|
1220
|
+
if (!allowed.includes(key)) {
|
|
1221
|
+
throw new CliError(`Unknown option --${camelToKebab(key)} for \`railcode query\`.\n\n${QUERY_HELP}`, 2);
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
// ---------------------------------------------------------------------------
|
|
942
1226
|
// connector — list service connectors + proxy one call
|
|
943
1227
|
// (`railcode connector list|fetch`)
|
|
944
1228
|
//
|
|
945
1229
|
// Service connectors are org-configured HTTP proxies to downstream SaaS APIs.
|
|
946
|
-
//
|
|
947
|
-
//
|
|
948
|
-
// `railcode dev` forwards to.
|
|
1230
|
+
// Like `railcode db`, these one-shot commands hit the org-scoped `/data/*` plane
|
|
1231
|
+
// with the login bearer token — no app (and no railcode.json) required.
|
|
949
1232
|
// ---------------------------------------------------------------------------
|
|
950
1233
|
const CONNECTOR_HELP = `railcode connector — service connectors
|
|
951
1234
|
|
|
@@ -972,8 +1255,8 @@ Fetch options:
|
|
|
972
1255
|
--json Print the raw { status, ok, headers, body, truncated } envelope
|
|
973
1256
|
|
|
974
1257
|
The connector holds the credential; you control only method/path/body. A non-2xx
|
|
975
|
-
upstream status is still printed but exits non-zero.
|
|
976
|
-
${MANIFEST_NAME}
|
|
1258
|
+
upstream status is still printed but exits non-zero. Works right after
|
|
1259
|
+
\`railcode login\` — no app or ${MANIFEST_NAME} required.
|
|
977
1260
|
`;
|
|
978
1261
|
async function commandConnector(args) {
|
|
979
1262
|
try {
|
|
@@ -1013,8 +1296,7 @@ async function commandConnectorList(args) {
|
|
|
1013
1296
|
throw new CliError(`railcode connector list takes no arguments (got "${args.rest[1]}").`, 2);
|
|
1014
1297
|
}
|
|
1015
1298
|
const config = requireLogin(args);
|
|
1016
|
-
const
|
|
1017
|
-
const connectors = await fetchServiceConnectors(config, app.uuid);
|
|
1299
|
+
const connectors = await fetchServiceConnectors(config);
|
|
1018
1300
|
if (args.options.json) {
|
|
1019
1301
|
console.log(JSON.stringify(connectors, null, 2));
|
|
1020
1302
|
return;
|
|
@@ -1030,9 +1312,9 @@ async function commandConnectorFetch(args) {
|
|
|
1030
1312
|
if (args.rest.length > 2) {
|
|
1031
1313
|
throw new CliError('railcode connector fetch takes a single path — quote it: railcode connector fetch "/v1/charges".', 2);
|
|
1032
1314
|
}
|
|
1033
|
-
const connector = parseConnectorOption(args
|
|
1034
|
-
const method = parseMethodOption(args
|
|
1035
|
-
const bodySource = pickConnectorBody(args
|
|
1315
|
+
const connector = parseConnectorOption(singleOption(args, "connector"));
|
|
1316
|
+
const method = parseMethodOption(singleOption(args, "method"));
|
|
1317
|
+
const bodySource = pickConnectorBody(singleOption(args, "body"), singleOption(args, "file"));
|
|
1036
1318
|
const body = bodySource.kind === "file"
|
|
1037
1319
|
? readConnectorBodyFile(bodySource.path)
|
|
1038
1320
|
: bodySource.kind === "inline"
|
|
@@ -1040,8 +1322,7 @@ async function commandConnectorFetch(args) {
|
|
|
1040
1322
|
: null;
|
|
1041
1323
|
const payload = buildConnectorRequest({ connector, method, path: args.rest[1] ?? "", body });
|
|
1042
1324
|
const config = requireLogin(args);
|
|
1043
|
-
const
|
|
1044
|
-
const envelope = await runConnectorRequest(config, app.uuid, payload);
|
|
1325
|
+
const envelope = await runConnectorRequest(config, payload);
|
|
1045
1326
|
if (args.options.json) {
|
|
1046
1327
|
console.log(JSON.stringify(envelope, null, 2));
|
|
1047
1328
|
}
|
|
@@ -1065,8 +1346,7 @@ async function commandConnectorDocs(args) {
|
|
|
1065
1346
|
throw new CliError("Pass either --json or --openapi, not both.", 2);
|
|
1066
1347
|
}
|
|
1067
1348
|
const config = requireLogin(args);
|
|
1068
|
-
const
|
|
1069
|
-
const docs = await fetchServiceConnectorDocs(config, app.uuid, name);
|
|
1349
|
+
const docs = await fetchServiceConnectorDocs(config, name);
|
|
1070
1350
|
if (args.options.json) {
|
|
1071
1351
|
console.log(JSON.stringify(docs, null, 2));
|
|
1072
1352
|
return;
|
|
@@ -1081,14 +1361,14 @@ async function commandConnectorDocs(args) {
|
|
|
1081
1361
|
}
|
|
1082
1362
|
console.log(renderConnectorDocs(docs));
|
|
1083
1363
|
}
|
|
1084
|
-
async function fetchServiceConnectors(config
|
|
1085
|
-
const resp = await authedFetch(config, `/api/organizations/${config.orgUuid}/
|
|
1364
|
+
async function fetchServiceConnectors(config) {
|
|
1365
|
+
const resp = await authedFetch(config, `/api/organizations/${config.orgUuid}/data/service-connectors`);
|
|
1086
1366
|
if (resp.status === 401)
|
|
1087
1367
|
await reLoginOrThrow();
|
|
1088
1368
|
return (await readJsonOrThrow(resp, "List service connectors"));
|
|
1089
1369
|
}
|
|
1090
|
-
async function fetchServiceConnectorDocs(config,
|
|
1091
|
-
const resp = await authedFetch(config, `/api/organizations/${config.orgUuid}/
|
|
1370
|
+
async function fetchServiceConnectorDocs(config, name) {
|
|
1371
|
+
const resp = await authedFetch(config, `/api/organizations/${config.orgUuid}/data/service-connectors/${encodeURIComponent(name)}`);
|
|
1092
1372
|
if (resp.status === 401)
|
|
1093
1373
|
await reLoginOrThrow();
|
|
1094
1374
|
if (resp.status === 404) {
|
|
@@ -1096,8 +1376,8 @@ async function fetchServiceConnectorDocs(config, appUuid, name) {
|
|
|
1096
1376
|
}
|
|
1097
1377
|
return (await readJsonOrThrow(resp, "Connector docs"));
|
|
1098
1378
|
}
|
|
1099
|
-
async function runConnectorRequest(config,
|
|
1100
|
-
const resp = await authedFetch(config, `/api/organizations/${config.orgUuid}/
|
|
1379
|
+
async function runConnectorRequest(config, body) {
|
|
1380
|
+
const resp = await authedFetch(config, `/api/organizations/${config.orgUuid}/data/service-connectors/request`, {
|
|
1101
1381
|
method: "POST",
|
|
1102
1382
|
headers: { "Content-Type": "application/json" },
|
|
1103
1383
|
body: JSON.stringify(body),
|
|
@@ -1849,6 +2129,8 @@ async function handleLocalApi(ctx, req, res, url) {
|
|
|
1849
2129
|
}
|
|
1850
2130
|
// Proxied surfaces → forward to the real instance with the saved token.
|
|
1851
2131
|
if (path === "/connections" ||
|
|
2132
|
+
path === "/queries" ||
|
|
2133
|
+
path.startsWith("/queries/") ||
|
|
1852
2134
|
path === "/sql" ||
|
|
1853
2135
|
path === "/llm/generate" ||
|
|
1854
2136
|
path === "/llm/stream" ||
|
|
@@ -2093,10 +2375,10 @@ function resolveDevAppUuid(ctx, creds, allowCreate) {
|
|
|
2093
2375
|
}
|
|
2094
2376
|
async function forwardCompute(ctx, req, res, path, url) {
|
|
2095
2377
|
// Never reply 401 to the browser — the SDK reloads the page on 401 (loop).
|
|
2096
|
-
// Load-time list calls degrade to [] so dataConnectors()/
|
|
2097
|
-
// load don't break;
|
|
2098
|
-
// return 503 instead.
|
|
2099
|
-
const degradeOk = path === "/connections" || path === "/service-connectors";
|
|
2378
|
+
// Load-time list calls degrade to [] so dataConnectors()/savedQueries()/
|
|
2379
|
+
// serviceConnectors() at load don't break; actual call surfaces
|
|
2380
|
+
// (/queries/{name}, /sql, /llm, /service-connectors/request) return 503 instead.
|
|
2381
|
+
const degradeOk = path === "/connections" || path === "/queries" || path === "/service-connectors";
|
|
2100
2382
|
const creds = devCreds(ctx.config);
|
|
2101
2383
|
if (!creds) {
|
|
2102
2384
|
if (degradeOk)
|
|
@@ -2106,8 +2388,8 @@ async function forwardCompute(ctx, req, res, path, url) {
|
|
|
2106
2388
|
message: "Run `railcode login` to use LLM and connectors in dev.",
|
|
2107
2389
|
});
|
|
2108
2390
|
}
|
|
2109
|
-
// Only the actual call surfaces (llm/sql/
|
|
2110
|
-
// the app server-side;
|
|
2391
|
+
// Only the actual call surfaces (saved-query invoke / llm / sql /
|
|
2392
|
+
// service-connectors/request) may create the app server-side; load-time lists won't.
|
|
2111
2393
|
const appUuid = await resolveDevAppUuid(ctx, creds, !degradeOk);
|
|
2112
2394
|
if (!appUuid) {
|
|
2113
2395
|
if (degradeOk)
|
package/dist/query.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// Pure helpers behind `railcode query list|run|create|delete`. Kept free of any
|
|
2
|
+
// I/O or process state so they can be unit-tested directly (see
|
|
3
|
+
// test/query.test.mjs); the command layer in index.ts does the HTTP and printing.
|
|
4
|
+
export class QueryCmdError extends Error {
|
|
5
|
+
}
|
|
6
|
+
// Matches the backend's declared param types (schemas/saved_query.py).
|
|
7
|
+
export const QUERY_PARAM_TYPES = ["string", "int", "float", "bool"];
|
|
8
|
+
// `query run --params` — the caller params as ONE JSON object. The CLI is
|
|
9
|
+
// mostly driven by agents, and agents speak JSON natively: no name=value
|
|
10
|
+
// mini-grammar, the flag takes exactly what the API takes.
|
|
11
|
+
export function parseRunParams(json) {
|
|
12
|
+
if (json === undefined)
|
|
13
|
+
return {};
|
|
14
|
+
let parsed;
|
|
15
|
+
try {
|
|
16
|
+
parsed = JSON.parse(json);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
throw new QueryCmdError(`--params must be valid JSON. Example: --params '{"region":"emea","limit":5}'`);
|
|
20
|
+
}
|
|
21
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
22
|
+
throw new QueryCmdError(`--params must be a JSON object, e.g. '{"region":"emea"}'.`);
|
|
23
|
+
}
|
|
24
|
+
return parsed;
|
|
25
|
+
}
|
|
26
|
+
// `query create|update --params` — the declared params as a JSON array in the
|
|
27
|
+
// exact API shape: [{"name","type","default"?}]. A "default" makes the param
|
|
28
|
+
// optional at invoke time (the server binds it when a caller omits the param);
|
|
29
|
+
// the backend re-validates it against the declared type at save.
|
|
30
|
+
export function parseParamDecls(json) {
|
|
31
|
+
if (json === undefined)
|
|
32
|
+
return [];
|
|
33
|
+
let parsed;
|
|
34
|
+
try {
|
|
35
|
+
parsed = JSON.parse(json);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
throw new QueryCmdError(`--params must be valid JSON. Example: --params '[{"name":"region","type":"string","default":"emea"}]'`);
|
|
39
|
+
}
|
|
40
|
+
if (!Array.isArray(parsed)) {
|
|
41
|
+
throw new QueryCmdError(`--params must be a JSON array of {"name","type","default"?} objects.`);
|
|
42
|
+
}
|
|
43
|
+
return parsed.map((item, i) => {
|
|
44
|
+
if (item === null || typeof item !== "object" || Array.isArray(item)) {
|
|
45
|
+
throw new QueryCmdError(`--params[${i}] must be an object like {"name":"region","type":"string"}.`);
|
|
46
|
+
}
|
|
47
|
+
const { name, type, default: def, ...extra } = item;
|
|
48
|
+
if (typeof name !== "string" || name === "") {
|
|
49
|
+
throw new QueryCmdError(`--params[${i}] needs a non-empty string "name".`);
|
|
50
|
+
}
|
|
51
|
+
if (typeof type !== "string" || !QUERY_PARAM_TYPES.includes(type)) {
|
|
52
|
+
throw new QueryCmdError(`--params[${i}] ("${name}") needs "type": one of ${QUERY_PARAM_TYPES.join(", ")}.`);
|
|
53
|
+
}
|
|
54
|
+
const extraKeys = Object.keys(extra);
|
|
55
|
+
if (extraKeys.length > 0) {
|
|
56
|
+
throw new QueryCmdError(`--params[${i}] ("${name}") has unknown key(s): ${extraKeys.join(", ")}.`);
|
|
57
|
+
}
|
|
58
|
+
const decl = { name, type: type };
|
|
59
|
+
if (def !== undefined) {
|
|
60
|
+
if (def === null || !["string", "number", "boolean"].includes(typeof def)) {
|
|
61
|
+
throw new QueryCmdError(`--params[${i}] ("${name}") "default" must be a string, number or boolean — omit it to make the param required.`);
|
|
62
|
+
}
|
|
63
|
+
decl.default = def;
|
|
64
|
+
}
|
|
65
|
+
return decl;
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
export function pickAuthoringSql(sql, sqlFile, required) {
|
|
69
|
+
if (sql !== undefined && sqlFile !== undefined) {
|
|
70
|
+
throw new QueryCmdError("Pass the SQL template with --sql or --sql-file, not both.");
|
|
71
|
+
}
|
|
72
|
+
if (sql !== undefined) {
|
|
73
|
+
const trimmed = sql.trim();
|
|
74
|
+
if (!trimmed)
|
|
75
|
+
throw new QueryCmdError("--sql is empty.");
|
|
76
|
+
return { kind: "inline", sql: trimmed };
|
|
77
|
+
}
|
|
78
|
+
if (sqlFile !== undefined) {
|
|
79
|
+
return { kind: "file", path: sqlFile };
|
|
80
|
+
}
|
|
81
|
+
if (required) {
|
|
82
|
+
throw new QueryCmdError('Provide the SQL template: --sql "select ..." or --sql-file query.sql.');
|
|
83
|
+
}
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
// Render one saved query's signature for the list table, e.g.
|
|
87
|
+
// "region?: string = "emea", limit: int" — `?` marks optional (defaulted) params.
|
|
88
|
+
export function paramSignature(params) {
|
|
89
|
+
return (params
|
|
90
|
+
.map((p) => p.default === undefined
|
|
91
|
+
? `${p.name}: ${p.type}`
|
|
92
|
+
: `${p.name}?: ${p.type} = ${JSON.stringify(p.default)}`)
|
|
93
|
+
.join(", ") || "-");
|
|
94
|
+
}
|
package/package.json
CHANGED
package/static/sdk.js
CHANGED
|
@@ -120,10 +120,10 @@
|
|
|
120
120
|
this.name = "ApiError";
|
|
121
121
|
}
|
|
122
122
|
};
|
|
123
|
-
function buildUrl(path,
|
|
123
|
+
function buildUrl(path, query2) {
|
|
124
124
|
const url2 = new URL(BASE + path, location.origin);
|
|
125
|
-
if (
|
|
126
|
-
for (const [key, value] of Object.entries(
|
|
125
|
+
if (query2) {
|
|
126
|
+
for (const [key, value] of Object.entries(query2)) {
|
|
127
127
|
if (value !== void 0) url2.searchParams.set(key, String(value));
|
|
128
128
|
}
|
|
129
129
|
}
|
|
@@ -148,8 +148,8 @@
|
|
|
148
148
|
if (resp.status === 204) return void 0;
|
|
149
149
|
return await resp.json();
|
|
150
150
|
}
|
|
151
|
-
async function getOrNull(path,
|
|
152
|
-
const resp = await send(buildUrl(path,
|
|
151
|
+
async function getOrNull(path, query2) {
|
|
152
|
+
const resp = await send(buildUrl(path, query2), { credentials: "include" });
|
|
153
153
|
if (resp.status === 404) return null;
|
|
154
154
|
if (!resp.ok) throw new ApiError(resp.status, await resp.text());
|
|
155
155
|
return await resp.json();
|
|
@@ -232,8 +232,8 @@
|
|
|
232
232
|
rows.truncated = r.truncated;
|
|
233
233
|
return rows;
|
|
234
234
|
};
|
|
235
|
-
var runSQL = (engine, connection,
|
|
236
|
-
const trimmed = shorten(
|
|
235
|
+
var runSQL = (engine, connection, query2, params) => {
|
|
236
|
+
const trimmed = shorten(query2.replace(/\s+/g, " ").trim(), QUERY_LABEL_MAX);
|
|
237
237
|
const label2 = `${engine ?? "data"}(${q(connection)}).runSQL(${q(trimmed)}${params && params.length ? `, ${shorten(JSON.stringify(params), PARAMS_LABEL_MAX)}` : ""})`;
|
|
238
238
|
return track(
|
|
239
239
|
"sql",
|
|
@@ -241,16 +241,16 @@
|
|
|
241
241
|
() => call("POST", "/sql", {
|
|
242
242
|
// engine is advisory; omit it for the generic data() namespace so the backend
|
|
243
243
|
// dispatches on the connection's stored kind. postgres() pins engine="postgres".
|
|
244
|
-
body: { ...engine ? { engine } : {}, connection, query, params: params || [] }
|
|
244
|
+
body: { ...engine ? { engine } : {}, connection, query: query2, params: params || [] }
|
|
245
245
|
}).then(toSqlRows)
|
|
246
246
|
);
|
|
247
247
|
};
|
|
248
248
|
var namespace = (engine) => {
|
|
249
249
|
const handle = (connection) => ({
|
|
250
|
-
runSQL: (
|
|
250
|
+
runSQL: (query2, params) => runSQL(engine, connection, query2, params)
|
|
251
251
|
});
|
|
252
252
|
const ns = handle;
|
|
253
|
-
ns.runSQL = (
|
|
253
|
+
ns.runSQL = (query2, params) => runSQL(engine, "default", query2, params);
|
|
254
254
|
return ns;
|
|
255
255
|
};
|
|
256
256
|
var data = namespace();
|
|
@@ -499,6 +499,20 @@
|
|
|
499
499
|
}
|
|
500
500
|
var llm = { generate, stream };
|
|
501
501
|
|
|
502
|
+
// ../sdk/src/queries.ts
|
|
503
|
+
var PARAMS_LABEL_MAX2 = 40;
|
|
504
|
+
var query = (name, params) => {
|
|
505
|
+
const label2 = `query(${q(name)}${params && Object.keys(params).length ? `, ${shorten(JSON.stringify(params), PARAMS_LABEL_MAX2)}` : ""})`;
|
|
506
|
+
return track(
|
|
507
|
+
"sql",
|
|
508
|
+
label2,
|
|
509
|
+
() => call("POST", `/queries/${encPath(name)}`, {
|
|
510
|
+
body: { params: params || {} }
|
|
511
|
+
}).then(toSqlRows)
|
|
512
|
+
);
|
|
513
|
+
};
|
|
514
|
+
var savedQueries = () => track("queries", "savedQueries()", () => call("GET", "/queries"));
|
|
515
|
+
|
|
502
516
|
// ../sdk/src/service-connectors.ts
|
|
503
517
|
var PATH_LABEL_MAX = 60;
|
|
504
518
|
var toResponse = (env) => {
|
|
@@ -547,4 +561,6 @@
|
|
|
547
561
|
target.dataConnectors = dataConnectors;
|
|
548
562
|
target.connector = connector;
|
|
549
563
|
target.serviceConnectors = serviceConnectors;
|
|
564
|
+
target.query = query;
|
|
565
|
+
target.savedQueries = savedQueries;
|
|
550
566
|
})();
|