@softr/cli 0.1.3 → 0.1.6-se21219.17
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 +103 -37
- package/dist/cli.mjs +1758 -606
- package/package.json +4 -3
package/dist/cli.mjs
CHANGED
|
@@ -1745,7 +1745,7 @@ var require_table = __commonJS({
|
|
|
1745
1745
|
var debug = require_debug();
|
|
1746
1746
|
var utils = require_utils();
|
|
1747
1747
|
var tableLayout = require_layout_manager();
|
|
1748
|
-
var
|
|
1748
|
+
var Table9 = class extends Array {
|
|
1749
1749
|
constructor(opts) {
|
|
1750
1750
|
super();
|
|
1751
1751
|
const options = utils.mergeOptions(opts);
|
|
@@ -1820,7 +1820,7 @@ var require_table = __commonJS({
|
|
|
1820
1820
|
return str[0].length;
|
|
1821
1821
|
}
|
|
1822
1822
|
};
|
|
1823
|
-
|
|
1823
|
+
Table9.reset = () => debug.reset();
|
|
1824
1824
|
function doDraw(row, lineNum, result) {
|
|
1825
1825
|
let line = [];
|
|
1826
1826
|
row.forEach(function(cell) {
|
|
@@ -1829,7 +1829,7 @@ var require_table = __commonJS({
|
|
|
1829
1829
|
let str = line.join("");
|
|
1830
1830
|
if (str.length) result.push(str);
|
|
1831
1831
|
}
|
|
1832
|
-
module.exports =
|
|
1832
|
+
module.exports = Table9;
|
|
1833
1833
|
}
|
|
1834
1834
|
});
|
|
1835
1835
|
|
|
@@ -2598,14 +2598,14 @@ var Option = class {
|
|
|
2598
2598
|
* @param {string} flags
|
|
2599
2599
|
* @param {string} [description]
|
|
2600
2600
|
*/
|
|
2601
|
-
constructor(
|
|
2602
|
-
this.flags =
|
|
2601
|
+
constructor(flags2, description) {
|
|
2602
|
+
this.flags = flags2;
|
|
2603
2603
|
this.description = description || "";
|
|
2604
|
-
this.required =
|
|
2605
|
-
this.optional =
|
|
2606
|
-
this.variadic = /\w\.\.\.[>\]]$/.test(
|
|
2604
|
+
this.required = flags2.includes("<");
|
|
2605
|
+
this.optional = flags2.includes("[");
|
|
2606
|
+
this.variadic = /\w\.\.\.[>\]]$/.test(flags2);
|
|
2607
2607
|
this.mandatory = false;
|
|
2608
|
-
const optionFlags = splitOptionFlags(
|
|
2608
|
+
const optionFlags = splitOptionFlags(flags2);
|
|
2609
2609
|
this.short = optionFlags.shortFlag;
|
|
2610
2610
|
this.long = optionFlags.longFlag;
|
|
2611
2611
|
this.negate = false;
|
|
@@ -2661,8 +2661,8 @@ var Option = class {
|
|
|
2661
2661
|
* @param {(string | string[])} names
|
|
2662
2662
|
* @return {Option}
|
|
2663
2663
|
*/
|
|
2664
|
-
conflicts(
|
|
2665
|
-
this.conflictsWith = this.conflictsWith.concat(
|
|
2664
|
+
conflicts(names2) {
|
|
2665
|
+
this.conflictsWith = this.conflictsWith.concat(names2);
|
|
2666
2666
|
return this;
|
|
2667
2667
|
}
|
|
2668
2668
|
/**
|
|
@@ -2856,12 +2856,12 @@ function camelcase(str) {
|
|
|
2856
2856
|
return str2 + word[0].toUpperCase() + word.slice(1);
|
|
2857
2857
|
});
|
|
2858
2858
|
}
|
|
2859
|
-
function splitOptionFlags(
|
|
2859
|
+
function splitOptionFlags(flags2) {
|
|
2860
2860
|
let shortFlag;
|
|
2861
2861
|
let longFlag;
|
|
2862
2862
|
const shortFlagExp = /^-[^-]$/;
|
|
2863
2863
|
const longFlagExp = /^--[^-]/;
|
|
2864
|
-
const flagParts =
|
|
2864
|
+
const flagParts = flags2.split(/[ |,]+/).concat("guard");
|
|
2865
2865
|
if (shortFlagExp.test(flagParts[0])) shortFlag = flagParts.shift();
|
|
2866
2866
|
if (longFlagExp.test(flagParts[0])) longFlag = flagParts.shift();
|
|
2867
2867
|
if (!shortFlag && shortFlagExp.test(flagParts[0]))
|
|
@@ -2872,7 +2872,7 @@ function splitOptionFlags(flags) {
|
|
|
2872
2872
|
}
|
|
2873
2873
|
if (flagParts[0].startsWith("-")) {
|
|
2874
2874
|
const unsupportedFlag = flagParts[0];
|
|
2875
|
-
const baseError = `option creation failed due to '${unsupportedFlag}' in option flags '${
|
|
2875
|
+
const baseError = `option creation failed due to '${unsupportedFlag}' in option flags '${flags2}'`;
|
|
2876
2876
|
if (/^-[^-][^-]/.test(unsupportedFlag))
|
|
2877
2877
|
throw new Error(
|
|
2878
2878
|
`${baseError}
|
|
@@ -2891,7 +2891,7 @@ function splitOptionFlags(flags) {
|
|
|
2891
2891
|
}
|
|
2892
2892
|
if (shortFlag === void 0 && longFlag === void 0)
|
|
2893
2893
|
throw new Error(
|
|
2894
|
-
`option creation failed due to no flags found in '${
|
|
2894
|
+
`option creation failed due to no flags found in '${flags2}'.`
|
|
2895
2895
|
);
|
|
2896
2896
|
return { shortFlag, longFlag };
|
|
2897
2897
|
}
|
|
@@ -3268,8 +3268,8 @@ var Command = class _Command extends EventEmitter {
|
|
|
3268
3268
|
* @param {string} names
|
|
3269
3269
|
* @return {Command} `this` command for chaining
|
|
3270
3270
|
*/
|
|
3271
|
-
arguments(
|
|
3272
|
-
|
|
3271
|
+
arguments(names2) {
|
|
3272
|
+
names2.trim().split(/ +/).forEach((detail) => {
|
|
3273
3273
|
this.argument(detail);
|
|
3274
3274
|
});
|
|
3275
3275
|
return this;
|
|
@@ -3454,8 +3454,8 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
3454
3454
|
* @param {string} [description]
|
|
3455
3455
|
* @return {Option} new option
|
|
3456
3456
|
*/
|
|
3457
|
-
createOption(
|
|
3458
|
-
return new Option(
|
|
3457
|
+
createOption(flags2, description) {
|
|
3458
|
+
return new Option(flags2, description);
|
|
3459
3459
|
}
|
|
3460
3460
|
/**
|
|
3461
3461
|
* Wrap parseArgs to catch 'commander.invalidArgument'.
|
|
@@ -3570,13 +3570,13 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
3570
3570
|
* @return {Command} `this` command for chaining
|
|
3571
3571
|
* @private
|
|
3572
3572
|
*/
|
|
3573
|
-
_optionEx(config,
|
|
3574
|
-
if (typeof
|
|
3573
|
+
_optionEx(config, flags2, description, fn, defaultValue) {
|
|
3574
|
+
if (typeof flags2 === "object" && flags2 instanceof Option) {
|
|
3575
3575
|
throw new Error(
|
|
3576
3576
|
"To add an Option object use addOption() instead of option() or requiredOption()"
|
|
3577
3577
|
);
|
|
3578
3578
|
}
|
|
3579
|
-
const option = this.createOption(
|
|
3579
|
+
const option = this.createOption(flags2, description);
|
|
3580
3580
|
option.makeOptionMandatory(!!config.mandatory);
|
|
3581
3581
|
if (typeof fn === "function") {
|
|
3582
3582
|
option.default(defaultValue).argParser(fn);
|
|
@@ -3613,8 +3613,8 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
3613
3613
|
* @param {*} [defaultValue]
|
|
3614
3614
|
* @return {Command} `this` command for chaining
|
|
3615
3615
|
*/
|
|
3616
|
-
option(
|
|
3617
|
-
return this._optionEx({},
|
|
3616
|
+
option(flags2, description, parseArg, defaultValue) {
|
|
3617
|
+
return this._optionEx({}, flags2, description, parseArg, defaultValue);
|
|
3618
3618
|
}
|
|
3619
3619
|
/**
|
|
3620
3620
|
* Add a required option which must have a value after parsing. This usually means
|
|
@@ -3628,10 +3628,10 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
3628
3628
|
* @param {*} [defaultValue]
|
|
3629
3629
|
* @return {Command} `this` command for chaining
|
|
3630
3630
|
*/
|
|
3631
|
-
requiredOption(
|
|
3631
|
+
requiredOption(flags2, description, parseArg, defaultValue) {
|
|
3632
3632
|
return this._optionEx(
|
|
3633
3633
|
{ mandatory: true },
|
|
3634
|
-
|
|
3634
|
+
flags2,
|
|
3635
3635
|
description,
|
|
3636
3636
|
parseArg,
|
|
3637
3637
|
defaultValue
|
|
@@ -3798,18 +3798,18 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
3798
3798
|
*
|
|
3799
3799
|
* @private
|
|
3800
3800
|
*/
|
|
3801
|
-
_prepareUserArgs(argv,
|
|
3801
|
+
_prepareUserArgs(argv, parseOptions2) {
|
|
3802
3802
|
if (argv !== void 0 && !Array.isArray(argv)) {
|
|
3803
3803
|
throw new Error("first parameter to parse must be array or undefined");
|
|
3804
3804
|
}
|
|
3805
|
-
|
|
3806
|
-
if (argv === void 0 &&
|
|
3805
|
+
parseOptions2 = parseOptions2 || {};
|
|
3806
|
+
if (argv === void 0 && parseOptions2.from === void 0) {
|
|
3807
3807
|
if (process2.versions?.electron) {
|
|
3808
|
-
|
|
3808
|
+
parseOptions2.from = "electron";
|
|
3809
3809
|
}
|
|
3810
3810
|
const execArgv = process2.execArgv ?? [];
|
|
3811
3811
|
if (execArgv.includes("-e") || execArgv.includes("--eval") || execArgv.includes("-p") || execArgv.includes("--print")) {
|
|
3812
|
-
|
|
3812
|
+
parseOptions2.from = "eval";
|
|
3813
3813
|
}
|
|
3814
3814
|
}
|
|
3815
3815
|
if (argv === void 0) {
|
|
@@ -3817,7 +3817,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
3817
3817
|
}
|
|
3818
3818
|
this.rawArgs = argv.slice();
|
|
3819
3819
|
let userArgs;
|
|
3820
|
-
switch (
|
|
3820
|
+
switch (parseOptions2.from) {
|
|
3821
3821
|
case void 0:
|
|
3822
3822
|
case "node":
|
|
3823
3823
|
this._scriptPath = argv[1];
|
|
@@ -3839,7 +3839,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
3839
3839
|
break;
|
|
3840
3840
|
default:
|
|
3841
3841
|
throw new Error(
|
|
3842
|
-
`unexpected parse option { from: '${
|
|
3842
|
+
`unexpected parse option { from: '${parseOptions2.from}' }`
|
|
3843
3843
|
);
|
|
3844
3844
|
}
|
|
3845
3845
|
if (!this._name && this._scriptPath)
|
|
@@ -3869,9 +3869,9 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
3869
3869
|
* @param {string} [parseOptions.from] - where the args are from: 'node', 'user', 'electron'
|
|
3870
3870
|
* @return {Command} `this` command for chaining
|
|
3871
3871
|
*/
|
|
3872
|
-
parse(argv,
|
|
3872
|
+
parse(argv, parseOptions2) {
|
|
3873
3873
|
this._prepareForParse();
|
|
3874
|
-
const userArgs = this._prepareUserArgs(argv,
|
|
3874
|
+
const userArgs = this._prepareUserArgs(argv, parseOptions2);
|
|
3875
3875
|
this._parseCommand([], userArgs);
|
|
3876
3876
|
return this;
|
|
3877
3877
|
}
|
|
@@ -3895,9 +3895,9 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
3895
3895
|
* @param {string} parseOptions.from - where the args are from: 'node', 'user', 'electron'
|
|
3896
3896
|
* @return {Promise}
|
|
3897
3897
|
*/
|
|
3898
|
-
async parseAsync(argv,
|
|
3898
|
+
async parseAsync(argv, parseOptions2) {
|
|
3899
3899
|
this._prepareForParse();
|
|
3900
|
-
const userArgs = this._prepareUserArgs(argv,
|
|
3900
|
+
const userArgs = this._prepareUserArgs(argv, parseOptions2);
|
|
3901
3901
|
await this._parseCommand([], userArgs);
|
|
3902
3902
|
return this;
|
|
3903
3903
|
}
|
|
@@ -4748,12 +4748,12 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
4748
4748
|
* @param {string} [description]
|
|
4749
4749
|
* @return {(this | string | undefined)} `this` command for chaining, or version string if no arguments
|
|
4750
4750
|
*/
|
|
4751
|
-
version(str,
|
|
4751
|
+
version(str, flags2, description) {
|
|
4752
4752
|
if (str === void 0) return this._version;
|
|
4753
4753
|
this._version = str;
|
|
4754
|
-
|
|
4754
|
+
flags2 = flags2 || "-V, --version";
|
|
4755
4755
|
description = description || "output the version number";
|
|
4756
|
-
const versionOption = this.createOption(
|
|
4756
|
+
const versionOption = this.createOption(flags2, description);
|
|
4757
4757
|
this._versionOptionName = versionOption.attributeName();
|
|
4758
4758
|
this._registerOption(versionOption);
|
|
4759
4759
|
this.on("option:" + versionOption.name(), () => {
|
|
@@ -4951,9 +4951,9 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
4951
4951
|
* @param {string} [path]
|
|
4952
4952
|
* @return {(string|null|Command)}
|
|
4953
4953
|
*/
|
|
4954
|
-
executableDir(
|
|
4955
|
-
if (
|
|
4956
|
-
this._executableDir =
|
|
4954
|
+
executableDir(path16) {
|
|
4955
|
+
if (path16 === void 0) return this._executableDir;
|
|
4956
|
+
this._executableDir = path16;
|
|
4957
4957
|
return this;
|
|
4958
4958
|
}
|
|
4959
4959
|
/**
|
|
@@ -5055,9 +5055,9 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
5055
5055
|
* @param {string} [description]
|
|
5056
5056
|
* @return {Command} `this` command for chaining
|
|
5057
5057
|
*/
|
|
5058
|
-
helpOption(
|
|
5059
|
-
if (typeof
|
|
5060
|
-
if (
|
|
5058
|
+
helpOption(flags2, description) {
|
|
5059
|
+
if (typeof flags2 === "boolean") {
|
|
5060
|
+
if (flags2) {
|
|
5061
5061
|
if (this._helpOption === null) this._helpOption = void 0;
|
|
5062
5062
|
if (this._defaultOptionGroup) {
|
|
5063
5063
|
this._initOptionGroup(this._getHelpOption());
|
|
@@ -5068,10 +5068,10 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
5068
5068
|
return this;
|
|
5069
5069
|
}
|
|
5070
5070
|
this._helpOption = this.createOption(
|
|
5071
|
-
|
|
5071
|
+
flags2 ?? "-h, --help",
|
|
5072
5072
|
description ?? "display help for command"
|
|
5073
5073
|
);
|
|
5074
|
-
if (
|
|
5074
|
+
if (flags2 || description) this._initOptionGroup(this._helpOption);
|
|
5075
5075
|
return this;
|
|
5076
5076
|
}
|
|
5077
5077
|
/**
|
|
@@ -5209,7 +5209,7 @@ function useColor() {
|
|
|
5209
5209
|
var program = new Command();
|
|
5210
5210
|
|
|
5211
5211
|
// src/commands/add.ts
|
|
5212
|
-
import
|
|
5212
|
+
import path6 from "node:path";
|
|
5213
5213
|
|
|
5214
5214
|
// src/errors.ts
|
|
5215
5215
|
var CliError = class extends Error {
|
|
@@ -5310,12 +5310,12 @@ function executionOrder(config) {
|
|
|
5310
5310
|
collect(config.actions, []);
|
|
5311
5311
|
const outgoing = new Map(appearance.map((id) => [id, []]));
|
|
5312
5312
|
const indegree = new Map(appearance.map((id) => [id, 0]));
|
|
5313
|
-
for (const
|
|
5314
|
-
const to =
|
|
5315
|
-
if (!to || !nodes.has(
|
|
5313
|
+
for (const path16 of pathSets.flat()) {
|
|
5314
|
+
const to = path16.toActionId;
|
|
5315
|
+
if (!to || !nodes.has(path16.fromActionId) || !nodes.has(to) || outgoing.get(path16.fromActionId).includes(to)) {
|
|
5316
5316
|
continue;
|
|
5317
5317
|
}
|
|
5318
|
-
outgoing.get(
|
|
5318
|
+
outgoing.get(path16.fromActionId).push(to);
|
|
5319
5319
|
indegree.set(to, indegree.get(to) + 1);
|
|
5320
5320
|
}
|
|
5321
5321
|
const queue = [...nodes.values()].filter((node) => node.kind === "trigger").map((node) => node.id);
|
|
@@ -6098,7 +6098,10 @@ var SoftrApiError = class extends Error {
|
|
|
6098
6098
|
body;
|
|
6099
6099
|
hint;
|
|
6100
6100
|
};
|
|
6101
|
-
function hintFor(status, client) {
|
|
6101
|
+
function hintFor(status, client, service) {
|
|
6102
|
+
if (service === "tables") {
|
|
6103
|
+
return tablesHintFor(status, client);
|
|
6104
|
+
}
|
|
6102
6105
|
if (status === 401 || status === 403) {
|
|
6103
6106
|
return client.kind === "proxy" ? "The target refused the request: no access to this workflow, or its credential is gone." : "The token was not accepted: revoked, from another environment, missing access to this workflow, or its statements do not grant this action. Run: softr login";
|
|
6104
6107
|
}
|
|
@@ -6107,40 +6110,65 @@ function hintFor(status, client) {
|
|
|
6107
6110
|
}
|
|
6108
6111
|
return void 0;
|
|
6109
6112
|
}
|
|
6110
|
-
function
|
|
6111
|
-
|
|
6113
|
+
function tablesHintFor(status, client) {
|
|
6114
|
+
if (status === 401 || status === 403) {
|
|
6115
|
+
return client.kind === "proxy" ? "The target refused the request: no access to this database, or its credential is gone." : "The token was not accepted: revoked, from another environment, or its statements do not grant this databases action (databases.records:read, databases.records:write, databases.schema:write). Run: softr login";
|
|
6116
|
+
}
|
|
6117
|
+
if (status === 429) {
|
|
6118
|
+
return "Too many requests for this token. Wait a moment and try again.";
|
|
6119
|
+
}
|
|
6120
|
+
return void 0;
|
|
6121
|
+
}
|
|
6122
|
+
function authHeaders(client, service) {
|
|
6123
|
+
if (client.kind === "proxy") {
|
|
6124
|
+
return {};
|
|
6125
|
+
}
|
|
6126
|
+
return service === "tables" ? { "Softr-Api-Key": client.token } : { Authorization: `Bearer ${client.token}` };
|
|
6112
6127
|
}
|
|
6113
|
-
|
|
6128
|
+
var BASE_URLS = {
|
|
6129
|
+
workflows: (target) => target.apiUrl,
|
|
6130
|
+
studio: (target) => target.studioApiUrl,
|
|
6131
|
+
tables: (target) => target.tablesApiUrl
|
|
6132
|
+
};
|
|
6133
|
+
async function send(client, service, method, path16, body) {
|
|
6114
6134
|
const init = {
|
|
6115
6135
|
method,
|
|
6116
|
-
headers: { ...authHeaders(client), Accept: "application/json" }
|
|
6136
|
+
headers: { ...authHeaders(client, service), Accept: "application/json" }
|
|
6117
6137
|
};
|
|
6118
|
-
if (body
|
|
6138
|
+
if (body instanceof FormData) {
|
|
6139
|
+
init.body = body;
|
|
6140
|
+
} else if (body !== void 0) {
|
|
6119
6141
|
init.headers = { ...init.headers, "Content-Type": "application/json" };
|
|
6120
6142
|
init.body = JSON.stringify(body);
|
|
6121
6143
|
}
|
|
6122
|
-
const
|
|
6123
|
-
const response = await fetch(`${baseUrl}${path13}`, init);
|
|
6144
|
+
const response = await fetch(`${BASE_URLS[service](client.target)}${path16}`, init);
|
|
6124
6145
|
if (response.ok) {
|
|
6125
6146
|
return response;
|
|
6126
6147
|
}
|
|
6127
6148
|
const text = await response.text();
|
|
6128
6149
|
throw new SoftrApiError(
|
|
6129
|
-
`${method} ${
|
|
6150
|
+
`${method} ${path16} failed with status ${response.status}${text ? `: ${text}` : ""}`,
|
|
6130
6151
|
response.status,
|
|
6131
6152
|
text,
|
|
6132
|
-
hintFor(response.status, client)
|
|
6153
|
+
hintFor(response.status, client, service)
|
|
6133
6154
|
);
|
|
6134
6155
|
}
|
|
6135
|
-
async function apiRequest(client, method,
|
|
6136
|
-
const response = await send(client, "workflows", method,
|
|
6156
|
+
async function apiRequest(client, method, path16, body) {
|
|
6157
|
+
const response = await send(client, "workflows", method, path16, body);
|
|
6158
|
+
return await response.json();
|
|
6159
|
+
}
|
|
6160
|
+
async function apiRequestVoid(client, method, path16, body) {
|
|
6161
|
+
await send(client, "workflows", method, path16, body);
|
|
6162
|
+
}
|
|
6163
|
+
async function tablesRequest(client, method, path16, body) {
|
|
6164
|
+
const response = await send(client, "tables", method, path16, body);
|
|
6137
6165
|
return await response.json();
|
|
6138
6166
|
}
|
|
6139
|
-
async function
|
|
6140
|
-
await send(client, "
|
|
6167
|
+
async function tablesRequestVoid(client, method, path16, body) {
|
|
6168
|
+
await send(client, "tables", method, path16, body);
|
|
6141
6169
|
}
|
|
6142
|
-
async function studioRequest(client, method,
|
|
6143
|
-
const response = await send(client, "studio", method,
|
|
6170
|
+
async function studioRequest(client, method, path16, body) {
|
|
6171
|
+
const response = await send(client, "studio", method, path16, body);
|
|
6144
6172
|
return await response.json();
|
|
6145
6173
|
}
|
|
6146
6174
|
|
|
@@ -6151,6 +6179,9 @@ function globalOptions(command) {
|
|
|
6151
6179
|
function emit(options, human, data2) {
|
|
6152
6180
|
process.stdout.write(options.json ? JSON.stringify(data2, null, 2) + "\n" : human + "\n");
|
|
6153
6181
|
}
|
|
6182
|
+
function plainTable(head, colWidths) {
|
|
6183
|
+
return { head, colWidths, wordWrap: true, style: { head: [], border: [], "padding-left": 1, "padding-right": 1 } };
|
|
6184
|
+
}
|
|
6154
6185
|
function reportError(error) {
|
|
6155
6186
|
if (error instanceof CliError || error instanceof SoftrApiError) {
|
|
6156
6187
|
process.stderr.write(`Error: ${error.message}
|
|
@@ -6963,12 +6994,12 @@ function parseTree(text, errors = [], options = ParseOptions.DEFAULT) {
|
|
|
6963
6994
|
}
|
|
6964
6995
|
return result;
|
|
6965
6996
|
}
|
|
6966
|
-
function findNodeAtLocation(root,
|
|
6997
|
+
function findNodeAtLocation(root, path16) {
|
|
6967
6998
|
if (!root) {
|
|
6968
6999
|
return void 0;
|
|
6969
7000
|
}
|
|
6970
7001
|
let node = root;
|
|
6971
|
-
for (let segment of
|
|
7002
|
+
for (let segment of path16) {
|
|
6972
7003
|
if (typeof segment === "string") {
|
|
6973
7004
|
if (node.type !== "object" || !Array.isArray(node.children)) {
|
|
6974
7005
|
return void 0;
|
|
@@ -7322,14 +7353,14 @@ function getNodeType(value) {
|
|
|
7322
7353
|
|
|
7323
7354
|
// node_modules/jsonc-parser/lib/esm/impl/edit.js
|
|
7324
7355
|
function setProperty(text, originalPath, value, options) {
|
|
7325
|
-
const
|
|
7356
|
+
const path16 = originalPath.slice();
|
|
7326
7357
|
const errors = [];
|
|
7327
7358
|
const root = parseTree(text, errors);
|
|
7328
7359
|
let parent = void 0;
|
|
7329
7360
|
let lastSegment = void 0;
|
|
7330
|
-
while (
|
|
7331
|
-
lastSegment =
|
|
7332
|
-
parent = findNodeAtLocation(root,
|
|
7361
|
+
while (path16.length > 0) {
|
|
7362
|
+
lastSegment = path16.pop();
|
|
7363
|
+
parent = findNodeAtLocation(root, path16);
|
|
7333
7364
|
if (parent === void 0 && value !== void 0) {
|
|
7334
7365
|
if (typeof lastSegment === "string") {
|
|
7335
7366
|
value = { [lastSegment]: value };
|
|
@@ -7552,8 +7583,8 @@ function printParseErrorCode(code) {
|
|
|
7552
7583
|
}
|
|
7553
7584
|
return "<unknown ParseErrorCode>";
|
|
7554
7585
|
}
|
|
7555
|
-
function modify(text,
|
|
7556
|
-
return setProperty(text,
|
|
7586
|
+
function modify(text, path16, value, options) {
|
|
7587
|
+
return setProperty(text, path16, value, options);
|
|
7557
7588
|
}
|
|
7558
7589
|
function applyEdits(text, edits) {
|
|
7559
7590
|
let sortedEdits = edits.slice(0).sort((a, b) => {
|
|
@@ -7583,14 +7614,36 @@ var CONFIG_FILE_NAME = "softr-workflows.jsonc";
|
|
|
7583
7614
|
function configPath(dir) {
|
|
7584
7615
|
return path2.join(dir, CONFIG_FILE_NAME);
|
|
7585
7616
|
}
|
|
7586
|
-
async function
|
|
7617
|
+
async function fileExists(file) {
|
|
7587
7618
|
try {
|
|
7588
|
-
await access(
|
|
7619
|
+
await access(file);
|
|
7589
7620
|
return true;
|
|
7590
7621
|
} catch {
|
|
7591
7622
|
return false;
|
|
7592
7623
|
}
|
|
7593
7624
|
}
|
|
7625
|
+
async function configExists(dir) {
|
|
7626
|
+
return fileExists(configPath(dir));
|
|
7627
|
+
}
|
|
7628
|
+
function parseJsoncFile(file, text) {
|
|
7629
|
+
const errors = [];
|
|
7630
|
+
const value = parse2(text, errors, { allowTrailingComma: true });
|
|
7631
|
+
if (errors.length) {
|
|
7632
|
+
const first = errors[0];
|
|
7633
|
+
const line = text.slice(0, first.offset).split("\n").length;
|
|
7634
|
+
throw new CliError(`${file}:${line}: ${printParseErrorCode(first.error)}`);
|
|
7635
|
+
}
|
|
7636
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
7637
|
+
throw new CliError(`${file}: the top level must be a JSON object.`);
|
|
7638
|
+
}
|
|
7639
|
+
return value;
|
|
7640
|
+
}
|
|
7641
|
+
async function listSubdirsWith(cwd, holds) {
|
|
7642
|
+
const entries = await readdir(cwd, { withFileTypes: true });
|
|
7643
|
+
const dirs = entries.filter((entry) => entry.isDirectory()).map((entry) => path2.join(cwd, entry.name));
|
|
7644
|
+
const flags2 = await Promise.all(dirs.map((dir) => holds(dir)));
|
|
7645
|
+
return dirs.filter((_, index) => flags2[index]);
|
|
7646
|
+
}
|
|
7594
7647
|
async function readConfig(dir) {
|
|
7595
7648
|
const file = configPath(dir);
|
|
7596
7649
|
let text;
|
|
@@ -7605,17 +7658,7 @@ async function readConfig(dir) {
|
|
|
7605
7658
|
}
|
|
7606
7659
|
throw error;
|
|
7607
7660
|
}
|
|
7608
|
-
|
|
7609
|
-
const value = parse2(text, errors, { allowTrailingComma: true });
|
|
7610
|
-
if (errors.length) {
|
|
7611
|
-
const first = errors[0];
|
|
7612
|
-
const line = text.slice(0, first.offset).split("\n").length;
|
|
7613
|
-
throw new CliError(`${file}:${line}: ${printParseErrorCode(first.error)}`);
|
|
7614
|
-
}
|
|
7615
|
-
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
7616
|
-
throw new CliError(`${file}: the top level must be a JSON object.`);
|
|
7617
|
-
}
|
|
7618
|
-
return value;
|
|
7661
|
+
return parseJsoncFile(file, text);
|
|
7619
7662
|
}
|
|
7620
7663
|
async function writeConfig(dir, config) {
|
|
7621
7664
|
const file = configPath(dir);
|
|
@@ -7635,10 +7678,7 @@ async function findWorkflowDir(cwd, explicitDir) {
|
|
|
7635
7678
|
throw new CliError(`No ${CONFIG_FILE_NAME} in ${cwd}.`, hint);
|
|
7636
7679
|
}
|
|
7637
7680
|
async function listWorkflowDirs(cwd) {
|
|
7638
|
-
|
|
7639
|
-
const dirs = entries.filter((entry) => entry.isDirectory()).map((entry) => path2.join(cwd, entry.name));
|
|
7640
|
-
const flags = await Promise.all(dirs.map((dir) => configExists(dir)));
|
|
7641
|
-
return dirs.filter((_, index) => flags[index]);
|
|
7681
|
+
return listSubdirsWith(cwd, configExists);
|
|
7642
7682
|
}
|
|
7643
7683
|
|
|
7644
7684
|
// src/project/config-edit.ts
|
|
@@ -8249,12 +8289,12 @@ prototypeAccessors.inAsync.get = function() {
|
|
|
8249
8289
|
prototypeAccessors.canAwait.get = function() {
|
|
8250
8290
|
for (var i2 = this.scopeStack.length - 1; i2 >= 0; i2--) {
|
|
8251
8291
|
var ref2 = this.scopeStack[i2];
|
|
8252
|
-
var
|
|
8253
|
-
if (
|
|
8292
|
+
var flags2 = ref2.flags;
|
|
8293
|
+
if (flags2 & (SCOPE_CLASS_STATIC_BLOCK | SCOPE_CLASS_FIELD_INIT)) {
|
|
8254
8294
|
return false;
|
|
8255
8295
|
}
|
|
8256
|
-
if (
|
|
8257
|
-
return (
|
|
8296
|
+
if (flags2 & SCOPE_FUNCTION) {
|
|
8297
|
+
return (flags2 & SCOPE_ASYNC) > 0;
|
|
8258
8298
|
}
|
|
8259
8299
|
}
|
|
8260
8300
|
return this.inModule && this.options.ecmaVersion >= 13 || this.options.allowAwaitOutsideFunction;
|
|
@@ -8270,8 +8310,8 @@ prototypeAccessors.allowReturn.get = function() {
|
|
|
8270
8310
|
};
|
|
8271
8311
|
prototypeAccessors.allowSuper.get = function() {
|
|
8272
8312
|
var ref2 = this.currentThisScope();
|
|
8273
|
-
var
|
|
8274
|
-
return (
|
|
8313
|
+
var flags2 = ref2.flags;
|
|
8314
|
+
return (flags2 & SCOPE_SUPER) > 0 || this.options.allowSuperOutsideMethod;
|
|
8275
8315
|
};
|
|
8276
8316
|
prototypeAccessors.allowDirectSuper.get = function() {
|
|
8277
8317
|
return (this.currentThisScope().flags & SCOPE_DIRECT_SUPER) > 0;
|
|
@@ -8282,8 +8322,8 @@ prototypeAccessors.treatFunctionsAsVar.get = function() {
|
|
|
8282
8322
|
prototypeAccessors.allowNewDotTarget.get = function() {
|
|
8283
8323
|
for (var i2 = this.scopeStack.length - 1; i2 >= 0; i2--) {
|
|
8284
8324
|
var ref2 = this.scopeStack[i2];
|
|
8285
|
-
var
|
|
8286
|
-
if (
|
|
8325
|
+
var flags2 = ref2.flags;
|
|
8326
|
+
if (flags2 & (SCOPE_CLASS_STATIC_BLOCK | SCOPE_CLASS_FIELD_INIT) || flags2 & SCOPE_FUNCTION && !(flags2 & SCOPE_ARROW)) {
|
|
8287
8327
|
return true;
|
|
8288
8328
|
}
|
|
8289
8329
|
}
|
|
@@ -8291,11 +8331,11 @@ prototypeAccessors.allowNewDotTarget.get = function() {
|
|
|
8291
8331
|
};
|
|
8292
8332
|
prototypeAccessors.allowUsing.get = function() {
|
|
8293
8333
|
var ref2 = this.currentScope();
|
|
8294
|
-
var
|
|
8295
|
-
if (
|
|
8334
|
+
var flags2 = ref2.flags;
|
|
8335
|
+
if (flags2 & SCOPE_SWITCH) {
|
|
8296
8336
|
return false;
|
|
8297
8337
|
}
|
|
8298
|
-
if (!this.inModule &&
|
|
8338
|
+
if (!this.inModule && flags2 & SCOPE_TOP) {
|
|
8299
8339
|
return false;
|
|
8300
8340
|
}
|
|
8301
8341
|
return true;
|
|
@@ -11076,14 +11116,14 @@ pp$4.curPosition = function() {
|
|
|
11076
11116
|
}
|
|
11077
11117
|
};
|
|
11078
11118
|
var pp$3 = Parser.prototype;
|
|
11079
|
-
var Scope = function Scope2(
|
|
11080
|
-
this.flags =
|
|
11119
|
+
var Scope = function Scope2(flags2) {
|
|
11120
|
+
this.flags = flags2;
|
|
11081
11121
|
this.var = [];
|
|
11082
11122
|
this.lexical = [];
|
|
11083
11123
|
this.functions = [];
|
|
11084
11124
|
};
|
|
11085
|
-
pp$3.enterScope = function(
|
|
11086
|
-
this.scopeStack.push(new Scope(
|
|
11125
|
+
pp$3.enterScope = function(flags2) {
|
|
11126
|
+
this.scopeStack.push(new Scope(flags2));
|
|
11087
11127
|
};
|
|
11088
11128
|
pp$3.exitScope = function() {
|
|
11089
11129
|
this.scopeStack.pop();
|
|
@@ -11299,12 +11339,12 @@ var RegExpValidationState = function RegExpValidationState2(parser) {
|
|
|
11299
11339
|
this.backReferenceNames = [];
|
|
11300
11340
|
this.branchID = null;
|
|
11301
11341
|
};
|
|
11302
|
-
RegExpValidationState.prototype.reset = function reset(start, pattern,
|
|
11303
|
-
var unicodeSets =
|
|
11304
|
-
var unicode =
|
|
11342
|
+
RegExpValidationState.prototype.reset = function reset(start, pattern, flags2) {
|
|
11343
|
+
var unicodeSets = flags2.indexOf("v") !== -1;
|
|
11344
|
+
var unicode = flags2.indexOf("u") !== -1;
|
|
11305
11345
|
this.start = start | 0;
|
|
11306
11346
|
this.source = pattern + "";
|
|
11307
|
-
this.flags =
|
|
11347
|
+
this.flags = flags2;
|
|
11308
11348
|
if (unicodeSets && this.parser.options.ecmaVersion >= 15) {
|
|
11309
11349
|
this.switchU = true;
|
|
11310
11350
|
this.switchV = true;
|
|
@@ -11381,15 +11421,15 @@ RegExpValidationState.prototype.eatChars = function eatChars(chs, forceU) {
|
|
|
11381
11421
|
};
|
|
11382
11422
|
pp$1.validateRegExpFlags = function(state) {
|
|
11383
11423
|
var validFlags = state.validFlags;
|
|
11384
|
-
var
|
|
11424
|
+
var flags2 = state.flags;
|
|
11385
11425
|
var u = false;
|
|
11386
11426
|
var v = false;
|
|
11387
|
-
for (var i2 = 0; i2 <
|
|
11388
|
-
var flag =
|
|
11427
|
+
for (var i2 = 0; i2 < flags2.length; i2++) {
|
|
11428
|
+
var flag = flags2.charAt(i2);
|
|
11389
11429
|
if (validFlags.indexOf(flag) === -1) {
|
|
11390
11430
|
this.raise(state.start, "Invalid regular expression flag");
|
|
11391
11431
|
}
|
|
11392
|
-
if (
|
|
11432
|
+
if (flags2.indexOf(flag, i2 + 1) > -1) {
|
|
11393
11433
|
this.raise(state.start, "Duplicate regular expression flag");
|
|
11394
11434
|
}
|
|
11395
11435
|
if (flag === "u") {
|
|
@@ -13001,20 +13041,20 @@ pp.readRegexp = function() {
|
|
|
13001
13041
|
var pattern = this.input.slice(start, this.pos);
|
|
13002
13042
|
++this.pos;
|
|
13003
13043
|
var flagsStart = this.pos;
|
|
13004
|
-
var
|
|
13044
|
+
var flags2 = this.readWord1();
|
|
13005
13045
|
if (this.containsEsc) {
|
|
13006
13046
|
this.unexpected(flagsStart);
|
|
13007
13047
|
}
|
|
13008
13048
|
var state = this.regexpState || (this.regexpState = new RegExpValidationState(this));
|
|
13009
|
-
state.reset(start, pattern,
|
|
13049
|
+
state.reset(start, pattern, flags2);
|
|
13010
13050
|
this.validateRegExpFlags(state);
|
|
13011
13051
|
this.validateRegExpPattern(state);
|
|
13012
13052
|
var value = null;
|
|
13013
13053
|
try {
|
|
13014
|
-
value = new RegExp(pattern,
|
|
13054
|
+
value = new RegExp(pattern, flags2);
|
|
13015
13055
|
} catch (e) {
|
|
13016
13056
|
}
|
|
13017
|
-
return this.finishToken(types$1.regexp, { pattern, flags, value });
|
|
13057
|
+
return this.finishToken(types$1.regexp, { pattern, flags: flags2, value });
|
|
13018
13058
|
};
|
|
13019
13059
|
pp.readInt = function(radix, len, maybeLegacyOctalNumericLiteral) {
|
|
13020
13060
|
var allowSeparators = this.options.ecmaVersion >= 12 && len === void 0;
|
|
@@ -13555,8 +13595,8 @@ function typeOfField(field) {
|
|
|
13555
13595
|
return field.allowMultipleEntries ? arrayOf(base) : base;
|
|
13556
13596
|
}
|
|
13557
13597
|
var SEGMENT = /^(?:\.?\[\s*'((?:[^'\\]|\\.)*)'\s*\]|\.?\[\s*"((?:[^"\\]|\\.)*)"\s*\]|\.?\[\s*(\d+)\s*\]|\.?\[\s*\*\s*\]|\.([^.[\]]+))/;
|
|
13558
|
-
function parsePath(
|
|
13559
|
-
let rest =
|
|
13598
|
+
function parsePath(path16) {
|
|
13599
|
+
let rest = path16.trim();
|
|
13560
13600
|
if (rest.startsWith("$")) {
|
|
13561
13601
|
rest = rest.slice(1);
|
|
13562
13602
|
}
|
|
@@ -14056,10 +14096,10 @@ function describePlaceholder(placeholder, raw, config, outputs, depth) {
|
|
|
14056
14096
|
function untested(nodeId) {
|
|
14057
14097
|
return `node "${nodeId}" has no test output yet (run: softr workflows test ${nodeId})`;
|
|
14058
14098
|
}
|
|
14059
|
-
function describeOutputRef(nodeId,
|
|
14060
|
-
const segments = parsePath(
|
|
14099
|
+
function describeOutputRef(nodeId, path16, source, outputs) {
|
|
14100
|
+
const segments = parsePath(path16);
|
|
14061
14101
|
if (!segments) {
|
|
14062
|
-
return { type: UNKNOWN, source, verified: false, note: `unsupported path "${
|
|
14102
|
+
return { type: UNKNOWN, source, verified: false, note: `unsupported path "${path16}"` };
|
|
14063
14103
|
}
|
|
14064
14104
|
const row = outputs[nodeId];
|
|
14065
14105
|
if (!row) {
|
|
@@ -14384,65 +14424,264 @@ ${notes.join("\n")}` : ""}`,
|
|
|
14384
14424
|
});
|
|
14385
14425
|
}
|
|
14386
14426
|
|
|
14387
|
-
// src/
|
|
14388
|
-
|
|
14427
|
+
// src/project/slug.ts
|
|
14428
|
+
function slug(title, fallback = "workflow") {
|
|
14429
|
+
return title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || fallback;
|
|
14430
|
+
}
|
|
14389
14431
|
|
|
14390
|
-
// src/
|
|
14391
|
-
function
|
|
14392
|
-
|
|
14432
|
+
// src/commands/add.ts
|
|
14433
|
+
function uniqueId(base, taken) {
|
|
14434
|
+
if (!taken.has(base)) {
|
|
14435
|
+
return base;
|
|
14436
|
+
}
|
|
14437
|
+
for (let n = 2; ; n++) {
|
|
14438
|
+
if (!taken.has(`${base}-${n}`)) {
|
|
14439
|
+
return `${base}-${n}`;
|
|
14440
|
+
}
|
|
14441
|
+
}
|
|
14393
14442
|
}
|
|
14394
|
-
function
|
|
14395
|
-
|
|
14443
|
+
function defaultAfter(config, loopId) {
|
|
14444
|
+
const order = executionOrder(config);
|
|
14445
|
+
const container = loopId ? order.filter((node) => node.loopId === loopId) : order.filter((node) => node.loopId === void 0 && node.reachable);
|
|
14446
|
+
if (!container.length) {
|
|
14447
|
+
if (loopId) {
|
|
14448
|
+
return loopId;
|
|
14449
|
+
}
|
|
14450
|
+
const trigger = config.triggers?.[0];
|
|
14451
|
+
if (!trigger) {
|
|
14452
|
+
throw new CliError("The workflow has no trigger to attach the first action to.");
|
|
14453
|
+
}
|
|
14454
|
+
return trigger.id;
|
|
14455
|
+
}
|
|
14456
|
+
const paths = loopId ? [...allActions(config.actions)].find((node) => node.id === loopId)?.inputs?.paths ?? [] : config.paths ?? [];
|
|
14457
|
+
const withOutgoing = new Set(paths.map((p) => p.fromActionId));
|
|
14458
|
+
const leaves = container.filter((node) => !withOutgoing.has(node.id));
|
|
14459
|
+
if (leaves.length === 1 && leaves[0]) {
|
|
14460
|
+
return leaves[0].id;
|
|
14461
|
+
}
|
|
14462
|
+
throw new CliError(
|
|
14463
|
+
leaves.length ? `Several nodes have no successor: ${leaves.map((node) => node.id).join(", ")}.` : "Every node already has a successor.",
|
|
14464
|
+
"Pass --after <nodeId> to say where the new node goes."
|
|
14465
|
+
);
|
|
14396
14466
|
}
|
|
14397
|
-
function
|
|
14398
|
-
|
|
14467
|
+
function registerAdd(program3) {
|
|
14468
|
+
program3.command("add").description("Add an action after an existing node, or a trigger; custom code gets its own file under actions/").argument("<type>", 'node type from "spec", e.g. CUSTOM_CODE or WEBHOOK').option("--id <id>", "node id, readable and unique (default: derived from the type)").option("--title <title>", "node title shown in the studio (default: the type label)").option("--after <nodeId>", "actions: the node the new one follows (default: the only node without a successor)").option("--to <nodeId>", "triggers: an action the new trigger leads to").option("--in-loop <loopId>", "put the node inside this loop body").option("--lang <language>", "for CUSTOM_CODE: JAVASCRIPT or PYTHON (default JAVASCRIPT)").option("--dir <path>", "workflow directory (default: the current directory)").action(async (type, opts, command) => {
|
|
14469
|
+
const options = globalOptions(command);
|
|
14470
|
+
const dir = await findWorkflowDir(process.cwd(), opts.dir);
|
|
14471
|
+
const config = await readConfig(dir);
|
|
14472
|
+
const entry = findCatalogEntry(type);
|
|
14473
|
+
const nodes = [...config.triggers ?? [], ...allActions(config.actions)];
|
|
14474
|
+
const taken = new Set(nodes.map((node2) => node2.id));
|
|
14475
|
+
const id = opts.id ?? uniqueId(entry.kind === "trigger" ? "trigger" : slug(entry.label), taken);
|
|
14476
|
+
if (taken.has(id)) {
|
|
14477
|
+
throw new CliError(`Node id "${id}" is already used.`);
|
|
14478
|
+
}
|
|
14479
|
+
if (id.includes(":::") || /[\s/?#]/.test(id)) {
|
|
14480
|
+
throw new CliError(`Node id "${id}" must not contain ":::", spaces, "/", "?" or "#".`);
|
|
14481
|
+
}
|
|
14482
|
+
const title = opts.title ?? entry.label;
|
|
14483
|
+
const { inputs, codeFile } = await newNodeInputs(entry, id, title, opts.lang, dir);
|
|
14484
|
+
const node = { id, title, type: entry.type, version: entry.version, inputs };
|
|
14485
|
+
if (entry.kind === "trigger") {
|
|
14486
|
+
if (opts.after || opts.inLoop) {
|
|
14487
|
+
throw new CliError(
|
|
14488
|
+
"Triggers start the workflow: use --to <actionId> to connect one, not --after or --in-loop."
|
|
14489
|
+
);
|
|
14490
|
+
}
|
|
14491
|
+
if (opts.to && !taken.has(opts.to)) {
|
|
14492
|
+
throw new CliError(`No node "${opts.to}" to connect to.`, `Known nodes: ${[...taken].join(", ")}`);
|
|
14493
|
+
}
|
|
14494
|
+
await editConfig(dir, [
|
|
14495
|
+
{ path: ["triggers", -1], value: node },
|
|
14496
|
+
...opts.to ? [{ path: ["paths", -1], value: { fromActionId: id, toActionId: opts.to } }] : []
|
|
14497
|
+
]);
|
|
14498
|
+
const notes = [
|
|
14499
|
+
(config.triggers?.length ?? 0) >= 1 ? "The service accepts one trigger per published workflow for now; multi-trigger support is coming." : "",
|
|
14500
|
+
opts.to ? "" : `Connect it with a path to the first action (or pass --to next time).`,
|
|
14501
|
+
`Then: softr workflows push, softr workflows test ${id}.`
|
|
14502
|
+
].filter(Boolean);
|
|
14503
|
+
emitWithVersionNote(
|
|
14504
|
+
options,
|
|
14505
|
+
config,
|
|
14506
|
+
`Added ${entry.type} trigger "${id}"${opts.to ? ` leading to "${opts.to}"` : ""}.
|
|
14507
|
+
${notes.join("\n")}`,
|
|
14508
|
+
{
|
|
14509
|
+
id,
|
|
14510
|
+
kind: "trigger",
|
|
14511
|
+
type: entry.type,
|
|
14512
|
+
to: opts.to ?? null
|
|
14513
|
+
}
|
|
14514
|
+
);
|
|
14515
|
+
return;
|
|
14516
|
+
}
|
|
14517
|
+
if (opts.inLoop) {
|
|
14518
|
+
const loop = [...allActions(config.actions)].find((candidate) => candidate.id === opts.inLoop);
|
|
14519
|
+
if (!loop || loop.type !== LOOP_TYPE) {
|
|
14520
|
+
throw new CliError(`"${opts.inLoop}" is not a loop in this workflow.`);
|
|
14521
|
+
}
|
|
14522
|
+
}
|
|
14523
|
+
const after = opts.after ?? defaultAfter(config, opts.inLoop);
|
|
14524
|
+
if (!taken.has(after)) {
|
|
14525
|
+
throw new CliError(`No node "${after}" to attach to.`, `Known nodes: ${[...taken].join(", ")}`);
|
|
14526
|
+
}
|
|
14527
|
+
const loopIndex = opts.inLoop ? (config.actions ?? []).findIndex((action) => action.id === opts.inLoop) : -1;
|
|
14528
|
+
const base = opts.inLoop && loopIndex >= 0 ? ["actions", loopIndex, "inputs"] : [];
|
|
14529
|
+
await editConfig(dir, [
|
|
14530
|
+
{ path: [...base, "actions", -1], value: node },
|
|
14531
|
+
{ path: [...base, "paths", -1], value: { fromActionId: after, toActionId: id } }
|
|
14532
|
+
]);
|
|
14533
|
+
const next = [
|
|
14534
|
+
codeFile ? `Write the function in ${path6.relative(process.cwd(), codeFile)} and map its inputs under inputs.inputData in softr-workflows.jsonc.` : "",
|
|
14535
|
+
entry.type === "BRANCH" || entry.type === "FILTER" ? `Add the condition(s) on the path(s) leaving it in softr-workflows.jsonc (see "spec ${entry.type}").` : "",
|
|
14536
|
+
`Then: softr workflows push, softr workflows test ${id}.`
|
|
14537
|
+
].filter(Boolean);
|
|
14538
|
+
emitWithVersionNote(
|
|
14539
|
+
options,
|
|
14540
|
+
config,
|
|
14541
|
+
`Added ${entry.type} "${id}" after "${after}"${opts.inLoop ? ` inside loop "${opts.inLoop}"` : ""}.
|
|
14542
|
+
${next.join("\n")}`,
|
|
14543
|
+
{
|
|
14544
|
+
id,
|
|
14545
|
+
kind: "action",
|
|
14546
|
+
type: entry.type,
|
|
14547
|
+
after,
|
|
14548
|
+
loopId: opts.inLoop ?? null,
|
|
14549
|
+
codeFile: codeFile ? path6.relative(process.cwd(), codeFile) : null
|
|
14550
|
+
}
|
|
14551
|
+
);
|
|
14552
|
+
});
|
|
14399
14553
|
}
|
|
14400
|
-
|
|
14401
|
-
|
|
14402
|
-
|
|
14554
|
+
|
|
14555
|
+
// src/commands/db/databases.ts
|
|
14556
|
+
var import_cli_table3 = __toESM(require_cli_table3(), 1);
|
|
14557
|
+
import path11 from "node:path";
|
|
14558
|
+
|
|
14559
|
+
// src/api/tables.ts
|
|
14560
|
+
var DATABASES = "/api/v1/databases";
|
|
14561
|
+
async function listDatabases(client) {
|
|
14562
|
+
return (await tablesRequest(client, "GET", DATABASES)).data;
|
|
14403
14563
|
}
|
|
14404
|
-
function
|
|
14405
|
-
return
|
|
14564
|
+
async function getDatabase(client, databaseId) {
|
|
14565
|
+
return (await tablesRequest(client, "GET", `${DATABASES}/${encodeURIComponent(databaseId)}`)).data;
|
|
14406
14566
|
}
|
|
14407
|
-
function
|
|
14408
|
-
return
|
|
14567
|
+
async function createDatabase(client, database) {
|
|
14568
|
+
return (await tablesRequest(client, "POST", DATABASES, database)).data;
|
|
14409
14569
|
}
|
|
14410
|
-
function
|
|
14411
|
-
return
|
|
14570
|
+
async function updateDatabase(client, databaseId, changes) {
|
|
14571
|
+
return (await tablesRequest(client, "PUT", `${DATABASES}/${encodeURIComponent(databaseId)}`, changes)).data;
|
|
14412
14572
|
}
|
|
14413
|
-
function
|
|
14414
|
-
|
|
14573
|
+
async function deleteDatabase(client, databaseId, force) {
|
|
14574
|
+
await tablesRequestVoid(
|
|
14575
|
+
client,
|
|
14576
|
+
"DELETE",
|
|
14577
|
+
`${DATABASES}/${encodeURIComponent(databaseId)}${force ? "?force=true" : ""}`
|
|
14578
|
+
);
|
|
14415
14579
|
}
|
|
14416
|
-
function
|
|
14417
|
-
return
|
|
14580
|
+
async function listTables(client, databaseId) {
|
|
14581
|
+
return (await tablesRequest(client, "GET", `${DATABASES}/${encodeURIComponent(databaseId)}/tables`)).data;
|
|
14418
14582
|
}
|
|
14419
|
-
function
|
|
14420
|
-
|
|
14583
|
+
function tablePath(databaseId, tableId) {
|
|
14584
|
+
const tables = `${DATABASES}/${encodeURIComponent(databaseId)}/tables`;
|
|
14585
|
+
return tableId ? `${tables}/${encodeURIComponent(tableId)}` : tables;
|
|
14421
14586
|
}
|
|
14422
|
-
function
|
|
14423
|
-
return
|
|
14587
|
+
async function createTable(client, databaseId, table) {
|
|
14588
|
+
return (await tablesRequest(client, "POST", tablePath(databaseId), table)).data;
|
|
14424
14589
|
}
|
|
14425
|
-
function
|
|
14426
|
-
return
|
|
14590
|
+
async function updateTable(client, databaseId, tableId, table) {
|
|
14591
|
+
return (await tablesRequest(client, "PUT", tablePath(databaseId, tableId), table)).data;
|
|
14427
14592
|
}
|
|
14428
|
-
async function
|
|
14429
|
-
|
|
14430
|
-
return await apiRequest(client, "GET", `/v1/workflows/${workflowId}/enabled`);
|
|
14431
|
-
} catch (error) {
|
|
14432
|
-
if (error instanceof SoftrApiError && error.status === 404) {
|
|
14433
|
-
return null;
|
|
14434
|
-
}
|
|
14435
|
-
throw error;
|
|
14436
|
-
}
|
|
14593
|
+
async function deleteTable(client, databaseId, tableId, force) {
|
|
14594
|
+
await tablesRequestVoid(client, "DELETE", `${tablePath(databaseId, tableId)}${force ? "?force=true" : ""}`);
|
|
14437
14595
|
}
|
|
14438
|
-
function
|
|
14439
|
-
return
|
|
14596
|
+
async function addField(client, databaseId, tableId, field) {
|
|
14597
|
+
return (await tablesRequest(client, "POST", `${tablePath(databaseId, tableId)}/fields`, field)).data;
|
|
14598
|
+
}
|
|
14599
|
+
async function updateField(client, databaseId, tableId, fieldId, field) {
|
|
14600
|
+
return (await tablesRequest(
|
|
14601
|
+
client,
|
|
14602
|
+
"PUT",
|
|
14603
|
+
`${tablePath(databaseId, tableId)}/fields/${encodeURIComponent(fieldId)}`,
|
|
14604
|
+
field
|
|
14605
|
+
)).data;
|
|
14606
|
+
}
|
|
14607
|
+
async function deleteField(client, databaseId, tableId, fieldId) {
|
|
14608
|
+
await tablesRequestVoid(client, "DELETE", `${tablePath(databaseId, tableId)}/fields/${encodeURIComponent(fieldId)}`);
|
|
14609
|
+
}
|
|
14610
|
+
async function listViews(client, databaseId, tableId) {
|
|
14611
|
+
return (await tablesRequest(client, "GET", `${tablePath(databaseId, tableId)}/views`)).data;
|
|
14612
|
+
}
|
|
14613
|
+
function recordsPath(databaseId, tableId, recordId) {
|
|
14614
|
+
const records = `${tablePath(databaseId, tableId)}/records`;
|
|
14615
|
+
return recordId ? `${records}/${encodeURIComponent(recordId)}` : records;
|
|
14616
|
+
}
|
|
14617
|
+
function names(fieldNames) {
|
|
14618
|
+
return fieldNames ? "fieldNames=true" : "fieldNames=false";
|
|
14619
|
+
}
|
|
14620
|
+
async function listRecords(client, databaseId, tableId, query) {
|
|
14621
|
+
const params = new URLSearchParams({ offset: String(query.offset), limit: String(query.limit) });
|
|
14622
|
+
if (query.viewId) {
|
|
14623
|
+
params.set("viewId", query.viewId);
|
|
14624
|
+
}
|
|
14625
|
+
const response = await tablesRequest(
|
|
14626
|
+
client,
|
|
14627
|
+
"GET",
|
|
14628
|
+
`${recordsPath(databaseId, tableId)}?${params.toString()}&${names(query.fieldNames)}`
|
|
14629
|
+
);
|
|
14630
|
+
return { records: response.data, metadata: response.metadata };
|
|
14631
|
+
}
|
|
14632
|
+
async function searchRecords(client, databaseId, tableId, search, fieldNames) {
|
|
14633
|
+
const response = await tablesRequest(
|
|
14634
|
+
client,
|
|
14635
|
+
"POST",
|
|
14636
|
+
`${recordsPath(databaseId, tableId)}/search?${names(fieldNames)}`,
|
|
14637
|
+
search
|
|
14638
|
+
);
|
|
14639
|
+
return { records: response.data, metadata: response.metadata };
|
|
14640
|
+
}
|
|
14641
|
+
async function getRecord(client, databaseId, tableId, recordId, fieldNames) {
|
|
14642
|
+
return (await tablesRequest(
|
|
14643
|
+
client,
|
|
14644
|
+
"GET",
|
|
14645
|
+
`${recordsPath(databaseId, tableId, recordId)}?${names(fieldNames)}`
|
|
14646
|
+
)).data;
|
|
14647
|
+
}
|
|
14648
|
+
async function createRecord(client, databaseId, tableId, fields, fieldNames) {
|
|
14649
|
+
return (await tablesRequest(
|
|
14650
|
+
client,
|
|
14651
|
+
"POST",
|
|
14652
|
+
`${recordsPath(databaseId, tableId)}?${names(fieldNames)}`,
|
|
14653
|
+
{
|
|
14654
|
+
fields
|
|
14655
|
+
}
|
|
14656
|
+
)).data;
|
|
14657
|
+
}
|
|
14658
|
+
async function updateRecord(client, databaseId, tableId, recordId, fields, fieldNames) {
|
|
14659
|
+
return (await tablesRequest(
|
|
14660
|
+
client,
|
|
14661
|
+
"PATCH",
|
|
14662
|
+
`${recordsPath(databaseId, tableId, recordId)}?${names(fieldNames)}`,
|
|
14663
|
+
{ fields }
|
|
14664
|
+
)).data;
|
|
14665
|
+
}
|
|
14666
|
+
async function deleteRecord(client, databaseId, tableId, recordId) {
|
|
14667
|
+
await tablesRequestVoid(client, "DELETE", recordsPath(databaseId, tableId, recordId));
|
|
14668
|
+
}
|
|
14669
|
+
async function uploadAttachment(client, databaseId, tableId, recordId, fieldId, file) {
|
|
14670
|
+
const form = new FormData();
|
|
14671
|
+
form.set("file", new Blob([file.content]), file.name);
|
|
14672
|
+
form.set("fileName", file.name);
|
|
14673
|
+
return (await tablesRequest(
|
|
14674
|
+
client,
|
|
14675
|
+
"PUT",
|
|
14676
|
+
`${recordsPath(databaseId, tableId, recordId)}/fields/${encodeURIComponent(fieldId)}/upload-attachment`,
|
|
14677
|
+
form
|
|
14678
|
+
)).data;
|
|
14440
14679
|
}
|
|
14441
14680
|
|
|
14442
14681
|
// src/config/credentials.ts
|
|
14443
14682
|
import { chmod, mkdir as mkdir4, readFile as readFile5, writeFile as writeFile5 } from "node:fs/promises";
|
|
14444
14683
|
import { homedir } from "node:os";
|
|
14445
|
-
import
|
|
14684
|
+
import path7 from "node:path";
|
|
14446
14685
|
|
|
14447
14686
|
// src/api/oauth.ts
|
|
14448
14687
|
import { createHash as createHash2, randomBytes } from "node:crypto";
|
|
@@ -14475,8 +14714,8 @@ function refreshFailureHint(status) {
|
|
|
14475
14714
|
return status === 401 ? "The OAuth grant is gone (revoked or older than 30 days). Run: softr login" : "The studio could not renew the login right now; try again in a moment.";
|
|
14476
14715
|
}
|
|
14477
14716
|
async function tokenRequest(target, form) {
|
|
14478
|
-
const
|
|
14479
|
-
const response = await fetch(`${target.studioApiUrl}${
|
|
14717
|
+
const path16 = "/v1/oauth/token";
|
|
14718
|
+
const response = await fetch(`${target.studioApiUrl}${path16}`, {
|
|
14480
14719
|
method: "POST",
|
|
14481
14720
|
headers: { "Content-Type": "application/x-www-form-urlencoded", Accept: "application/json" },
|
|
14482
14721
|
body: new URLSearchParams({ ...form, client_id: oauthClientId() }).toString()
|
|
@@ -14484,7 +14723,7 @@ async function tokenRequest(target, form) {
|
|
|
14484
14723
|
if (!response.ok) {
|
|
14485
14724
|
const text = await response.text();
|
|
14486
14725
|
throw new SoftrApiError(
|
|
14487
|
-
`POST ${
|
|
14726
|
+
`POST ${path16} failed with status ${response.status}${text ? `: ${text}` : ""}`,
|
|
14488
14727
|
response.status,
|
|
14489
14728
|
text,
|
|
14490
14729
|
form["grant_type"] === "refresh_token" ? refreshFailureHint(response.status) : void 0
|
|
@@ -14513,9 +14752,11 @@ function refreshTokens(target, refreshToken) {
|
|
|
14513
14752
|
var API_URL_ENV_VAR = "SOFTR_WORKFLOWS_API_URL";
|
|
14514
14753
|
var STUDIO_API_URL_ENV_VAR = "SOFTR_STUDIO_API_URL";
|
|
14515
14754
|
var STUDIO_URL_ENV_VAR = "SOFTR_STUDIO_URL";
|
|
14755
|
+
var TABLES_API_URL_ENV_VAR = "SOFTR_TABLES_API_URL";
|
|
14516
14756
|
var DEFAULT_API_URL = "https://workflows-api.softr.io";
|
|
14517
14757
|
var DEFAULT_STUDIO_API_URL = "https://studio-api.softr.io";
|
|
14518
14758
|
var DEFAULT_STUDIO_URL = "https://studio.softr.io";
|
|
14759
|
+
var DEFAULT_TABLES_API_URL = "https://tables-api.softr.io";
|
|
14519
14760
|
function normalizeUrl(value, source) {
|
|
14520
14761
|
let url;
|
|
14521
14762
|
try {
|
|
@@ -14533,7 +14774,8 @@ function resolveTarget(remembered) {
|
|
|
14533
14774
|
return {
|
|
14534
14775
|
apiUrl: pick(API_URL_ENV_VAR, remembered?.apiUrl, DEFAULT_API_URL),
|
|
14535
14776
|
studioApiUrl: pick(STUDIO_API_URL_ENV_VAR, remembered?.studioApiUrl, DEFAULT_STUDIO_API_URL),
|
|
14536
|
-
studioUrl: pick(STUDIO_URL_ENV_VAR, remembered?.studioUrl, DEFAULT_STUDIO_URL)
|
|
14777
|
+
studioUrl: pick(STUDIO_URL_ENV_VAR, remembered?.studioUrl, DEFAULT_STUDIO_URL),
|
|
14778
|
+
tablesApiUrl: pick(TABLES_API_URL_ENV_VAR, remembered?.tablesApiUrl, DEFAULT_TABLES_API_URL)
|
|
14537
14779
|
};
|
|
14538
14780
|
}
|
|
14539
14781
|
function targetHost(target) {
|
|
@@ -14563,7 +14805,7 @@ function isProxyAuth() {
|
|
|
14563
14805
|
}
|
|
14564
14806
|
var REFRESH_MARGIN_MS = 5 * 60 * 1e3;
|
|
14565
14807
|
function credentialsPath() {
|
|
14566
|
-
return
|
|
14808
|
+
return path7.join(homedir(), ".softr", "credentials.json");
|
|
14567
14809
|
}
|
|
14568
14810
|
async function readCredentials() {
|
|
14569
14811
|
try {
|
|
@@ -14577,7 +14819,7 @@ async function readCredentials() {
|
|
|
14577
14819
|
}
|
|
14578
14820
|
async function writeCredentials(credentials) {
|
|
14579
14821
|
const file = credentialsPath();
|
|
14580
|
-
await mkdir4(
|
|
14822
|
+
await mkdir4(path7.dirname(file), { recursive: true, mode: 448 });
|
|
14581
14823
|
await writeFile5(file, JSON.stringify(credentials, null, 2) + "\n", { mode: 384 });
|
|
14582
14824
|
await chmod(file, 384);
|
|
14583
14825
|
return file;
|
|
@@ -14650,51 +14892,64 @@ async function authenticatedClient(target) {
|
|
|
14650
14892
|
return { target, token: stored.token, kind };
|
|
14651
14893
|
}
|
|
14652
14894
|
|
|
14653
|
-
// src/
|
|
14654
|
-
|
|
14895
|
+
// src/project/db-dir.ts
|
|
14896
|
+
import { mkdir as mkdir5, readdir as readdir2, readFile as readFile6, rm, writeFile as writeFile6 } from "node:fs/promises";
|
|
14897
|
+
import path8 from "node:path";
|
|
14655
14898
|
|
|
14656
|
-
// src/
|
|
14657
|
-
|
|
14899
|
+
// src/project/lookup.ts
|
|
14900
|
+
function byIdOrName(items, reference, kind, describe2 = (item) => item.id) {
|
|
14901
|
+
const byId = items.find((item) => item.id === reference);
|
|
14902
|
+
if (byId) {
|
|
14903
|
+
return byId;
|
|
14904
|
+
}
|
|
14905
|
+
const byName = items.filter((item) => item.name === reference);
|
|
14906
|
+
if (byName.length > 1) {
|
|
14907
|
+
throw new CliError(
|
|
14908
|
+
`Several ${kind}s are named "${reference}".`,
|
|
14909
|
+
`Pass the id of one: ${byName.map(describe2).join(", ")}`
|
|
14910
|
+
);
|
|
14911
|
+
}
|
|
14912
|
+
return byName[0];
|
|
14913
|
+
}
|
|
14914
|
+
|
|
14915
|
+
// src/project/db-dir.ts
|
|
14916
|
+
var DB_CONFIG_FILE_NAME = "softr-db.jsonc";
|
|
14917
|
+
var DB_CACHE_DIR_NAME = ".softr-db";
|
|
14918
|
+
var DATABASE_ID_ENV_VAR = "SOFTR_DATABASE_ID";
|
|
14919
|
+
function configFile(dir) {
|
|
14920
|
+
return path8.join(dir, DB_CONFIG_FILE_NAME);
|
|
14921
|
+
}
|
|
14922
|
+
function cacheFile(dir) {
|
|
14923
|
+
return path8.join(dir, DB_CACHE_DIR_NAME, "schema.json");
|
|
14924
|
+
}
|
|
14925
|
+
async function dbConfigExists(dir) {
|
|
14926
|
+
return fileExists(configFile(dir));
|
|
14927
|
+
}
|
|
14928
|
+
async function readDbConfig(dir) {
|
|
14929
|
+
const file = configFile(dir);
|
|
14930
|
+
let text;
|
|
14658
14931
|
try {
|
|
14659
|
-
|
|
14932
|
+
text = await readFile6(file, "utf8");
|
|
14660
14933
|
} catch (error) {
|
|
14661
|
-
if (error
|
|
14662
|
-
|
|
14663
|
-
"The studio does not know this token: revoked, expired, or from another environment.",
|
|
14664
|
-
'Create a new personal API token in the studio, or run "softr login" for the browser login.'
|
|
14665
|
-
);
|
|
14934
|
+
if (error.code === "ENOENT") {
|
|
14935
|
+
return void 0;
|
|
14666
14936
|
}
|
|
14667
14937
|
throw error;
|
|
14668
14938
|
}
|
|
14939
|
+
const value = parseJsoncFile(file, text);
|
|
14940
|
+
if (typeof value["databaseId"] !== "string" || !value["databaseId"]) {
|
|
14941
|
+
throw new CliError(`${file}: "databaseId" must be a non-empty string.`);
|
|
14942
|
+
}
|
|
14943
|
+
return { databaseId: value["databaseId"] };
|
|
14669
14944
|
}
|
|
14670
|
-
function
|
|
14671
|
-
|
|
14672
|
-
|
|
14673
|
-
);
|
|
14674
|
-
}
|
|
14675
|
-
async function listWorkspaces(client) {
|
|
14676
|
-
const workspaces = await studioRequest(client, "GET", "/v1/api/me/workspaces");
|
|
14677
|
-
return workspaces.map((workspace) => ({
|
|
14678
|
-
id: workspace.id,
|
|
14679
|
-
name: workspace.name,
|
|
14680
|
-
...workspace.role ? { userRole: workspace.role } : {},
|
|
14681
|
-
isDefault: workspace.isDefault
|
|
14682
|
-
}));
|
|
14683
|
-
}
|
|
14684
|
-
function listWorkspaceIntegrations(client, workspaceId) {
|
|
14685
|
-
return studioRequest(client, "GET", `/v1/api/me/workspaces/${workspaceId}/integrations`);
|
|
14945
|
+
async function writeDbConfig(dir, config) {
|
|
14946
|
+
await mkdir5(dir, { recursive: true });
|
|
14947
|
+
await writeFile6(configFile(dir), JSON.stringify(config, null, 2) + "\n");
|
|
14948
|
+
return configFile(dir);
|
|
14686
14949
|
}
|
|
14687
|
-
|
|
14688
|
-
// src/project/state.ts
|
|
14689
|
-
import { mkdir as mkdir5, readFile as readFile6, writeFile as writeFile6 } from "node:fs/promises";
|
|
14690
|
-
import path7 from "node:path";
|
|
14691
|
-
var STATE_DIR_NAME = ".softr-workflows";
|
|
14692
|
-
function stateDir(dir) {
|
|
14693
|
-
return path7.join(dir, STATE_DIR_NAME);
|
|
14694
|
-
}
|
|
14695
|
-
async function readState(dir) {
|
|
14950
|
+
async function readSchemaCache(dir) {
|
|
14696
14951
|
try {
|
|
14697
|
-
return JSON.parse(await readFile6(
|
|
14952
|
+
return JSON.parse(await readFile6(cacheFile(dir), "utf8"));
|
|
14698
14953
|
} catch (error) {
|
|
14699
14954
|
if (error.code === "ENOENT") {
|
|
14700
14955
|
return void 0;
|
|
@@ -14702,14 +14957,19 @@ async function readState(dir) {
|
|
|
14702
14957
|
throw error;
|
|
14703
14958
|
}
|
|
14704
14959
|
}
|
|
14705
|
-
async function
|
|
14706
|
-
|
|
14707
|
-
|
|
14708
|
-
await writeFile6(
|
|
14960
|
+
async function writeSchemaCache(dir, database, tables) {
|
|
14961
|
+
const cache = { database, tables, refreshedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
14962
|
+
await mkdir5(path8.dirname(cacheFile(dir)), { recursive: true });
|
|
14963
|
+
await writeFile6(cacheFile(dir), JSON.stringify(cache, null, 2) + "\n");
|
|
14964
|
+
await pruneViewsCache(dir, new Set(tables.map((table) => table.id)));
|
|
14965
|
+
return cache;
|
|
14709
14966
|
}
|
|
14710
|
-
|
|
14967
|
+
function viewsDir(dir) {
|
|
14968
|
+
return path8.join(dir, DB_CACHE_DIR_NAME, "views");
|
|
14969
|
+
}
|
|
14970
|
+
async function readViewsCache(dir, tableId) {
|
|
14711
14971
|
try {
|
|
14712
|
-
return JSON.parse(await readFile6(
|
|
14972
|
+
return JSON.parse(await readFile6(path8.join(viewsDir(dir), `${tableId}.json`), "utf8"));
|
|
14713
14973
|
} catch (error) {
|
|
14714
14974
|
if (error.code === "ENOENT") {
|
|
14715
14975
|
return void 0;
|
|
@@ -14717,112 +14977,261 @@ async function readStateFile(dir, name) {
|
|
|
14717
14977
|
throw error;
|
|
14718
14978
|
}
|
|
14719
14979
|
}
|
|
14720
|
-
async function
|
|
14721
|
-
|
|
14722
|
-
|
|
14723
|
-
await writeFile6(
|
|
14724
|
-
return file;
|
|
14980
|
+
async function writeViewsCache(dir, tableId, views) {
|
|
14981
|
+
const value = { tableId, views, refreshedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
14982
|
+
await mkdir5(viewsDir(dir), { recursive: true });
|
|
14983
|
+
await writeFile6(path8.join(viewsDir(dir), `${tableId}.json`), JSON.stringify(value, null, 2) + "\n");
|
|
14725
14984
|
}
|
|
14726
|
-
|
|
14727
|
-
|
|
14728
|
-
|
|
14729
|
-
|
|
14730
|
-
|
|
14731
|
-
|
|
14732
|
-
|
|
14733
|
-
|
|
14734
|
-
|
|
14735
|
-
|
|
14736
|
-
|
|
14737
|
-
|
|
14738
|
-
|
|
14739
|
-
};
|
|
14985
|
+
async function pruneViewsCache(dir, tableIds) {
|
|
14986
|
+
let names2;
|
|
14987
|
+
try {
|
|
14988
|
+
names2 = await readdir2(viewsDir(dir));
|
|
14989
|
+
} catch (error) {
|
|
14990
|
+
if (error.code === "ENOENT") {
|
|
14991
|
+
return;
|
|
14992
|
+
}
|
|
14993
|
+
throw error;
|
|
14994
|
+
}
|
|
14995
|
+
await Promise.all(
|
|
14996
|
+
names2.filter((name) => name.endsWith(".json") && !tableIds.has(name.slice(0, -".json".length))).map((name) => rm(path8.join(viewsDir(dir), name), { force: true }))
|
|
14997
|
+
);
|
|
14740
14998
|
}
|
|
14741
|
-
async function
|
|
14742
|
-
const
|
|
14743
|
-
|
|
14744
|
-
|
|
14999
|
+
async function refreshSchemaCache(client, dir, databaseId) {
|
|
15000
|
+
const [database, tables] = await Promise.all([getDatabase(client, databaseId), listTables(client, databaseId)]);
|
|
15001
|
+
return writeSchemaCache(dir, database, tables);
|
|
15002
|
+
}
|
|
15003
|
+
async function removeSchemaCache(dir) {
|
|
15004
|
+
await rm(path8.join(dir, DB_CACHE_DIR_NAME), { recursive: true, force: true });
|
|
15005
|
+
}
|
|
15006
|
+
async function resolveDatabase(cwd, databaseId, explicitDir) {
|
|
15007
|
+
const dir = path8.resolve(cwd, explicitDir ?? ".");
|
|
15008
|
+
const config = await readDbConfig(dir);
|
|
15009
|
+
if (explicitDir && !config) {
|
|
15010
|
+
throw new CliError(`No ${DB_CONFIG_FILE_NAME} in ${dir}.`, 'Run "softr db get <database>" to create one.');
|
|
15011
|
+
}
|
|
15012
|
+
const given = databaseId ?? (process.env[DATABASE_ID_ENV_VAR] || void 0);
|
|
15013
|
+
if (given) {
|
|
15014
|
+
return config?.databaseId === given ? { databaseId: given, dir } : { databaseId: given };
|
|
15015
|
+
}
|
|
15016
|
+
if (config) {
|
|
15017
|
+
return { databaseId: config.databaseId, dir };
|
|
15018
|
+
}
|
|
15019
|
+
const candidates = await listSubdirsWith(cwd, dbConfigExists);
|
|
15020
|
+
throw new CliError(
|
|
15021
|
+
`Which database? There is no ${DB_CONFIG_FILE_NAME} in ${cwd}.`,
|
|
15022
|
+
candidates.length ? `Databases found in: ${candidates.map((candidate) => path8.relative(cwd, candidate)).join(", ")}. Pass --dir <name> or cd into one.` : `Pass the database id, set ${DATABASE_ID_ENV_VAR}, or run "softr db get <database>".`
|
|
15023
|
+
);
|
|
15024
|
+
}
|
|
15025
|
+
function belongsToAnother(dir, existingId, wantedId, hint) {
|
|
15026
|
+
return new CliError(
|
|
15027
|
+
`${path8.join(dir, DB_CONFIG_FILE_NAME)} belongs to database ${existingId}${wantedId ? `, not ${wantedId}` : ""}.`,
|
|
15028
|
+
hint
|
|
15029
|
+
);
|
|
15030
|
+
}
|
|
15031
|
+
async function databaseDirFor(cwd, explicitDir, databaseId, dirName) {
|
|
15032
|
+
const here = path8.resolve(cwd, explicitDir ?? ".");
|
|
15033
|
+
const inHere = await readDbConfig(here);
|
|
15034
|
+
if (inHere) {
|
|
15035
|
+
if (databaseId && inHere.databaseId === databaseId) {
|
|
15036
|
+
return { dir: here, existing: true };
|
|
15037
|
+
}
|
|
15038
|
+
throw belongsToAnother(
|
|
15039
|
+
here,
|
|
15040
|
+
inHere.databaseId,
|
|
15041
|
+
databaseId,
|
|
15042
|
+
explicitDir ? "Pick another --dir." : "Run it from the parent directory (a new subdirectory is created) or pass --dir."
|
|
15043
|
+
);
|
|
15044
|
+
}
|
|
15045
|
+
if (explicitDir) {
|
|
15046
|
+
return { dir: here, existing: false };
|
|
15047
|
+
}
|
|
15048
|
+
const sibling2 = path8.join(cwd, dirName);
|
|
15049
|
+
const inSibling = await readDbConfig(sibling2);
|
|
15050
|
+
if (inSibling) {
|
|
15051
|
+
if (inSibling.databaseId === databaseId) {
|
|
15052
|
+
return { dir: sibling2, existing: true };
|
|
15053
|
+
}
|
|
15054
|
+
throw belongsToAnother(sibling2, inSibling.databaseId, databaseId, "Pass --dir <path>.");
|
|
15055
|
+
}
|
|
15056
|
+
return { dir: sibling2, existing: false };
|
|
15057
|
+
}
|
|
15058
|
+
async function findReachableDatabase(client, reference) {
|
|
15059
|
+
const database = byIdOrName(
|
|
15060
|
+
await listDatabases(client),
|
|
15061
|
+
reference,
|
|
15062
|
+
"database",
|
|
15063
|
+
(item) => `${item.id} (workspace ${item.workspaceId})`
|
|
15064
|
+
);
|
|
15065
|
+
if (!database) {
|
|
14745
15066
|
throw new CliError(
|
|
14746
|
-
`
|
|
14747
|
-
|
|
15067
|
+
`The token reaches no database with the id or name "${reference}".`,
|
|
15068
|
+
'"softr db list" shows the ones it reaches.'
|
|
14748
15069
|
);
|
|
14749
15070
|
}
|
|
14750
|
-
|
|
14751
|
-
|
|
14752
|
-
|
|
14753
|
-
|
|
14754
|
-
|
|
14755
|
-
|
|
14756
|
-
|
|
14757
|
-
|
|
14758
|
-
|
|
14759
|
-
|
|
14760
|
-
|
|
14761
|
-
|
|
14762
|
-
workspaceName: workspace.name,
|
|
14763
|
-
status: error.status,
|
|
14764
|
-
message: error.body
|
|
14765
|
-
});
|
|
15071
|
+
return database;
|
|
15072
|
+
}
|
|
15073
|
+
async function bindDatabaseDir(client, cwd, reference, explicitDir) {
|
|
15074
|
+
const here = path8.resolve(cwd, explicitDir ?? ".");
|
|
15075
|
+
const config = await readDbConfig(here);
|
|
15076
|
+
if (!reference || reference === config?.databaseId) {
|
|
15077
|
+
if (!config) {
|
|
15078
|
+
const candidates = await listSubdirsWith(cwd, dbConfigExists);
|
|
15079
|
+
throw new CliError(
|
|
15080
|
+
`Which database? There is no ${DB_CONFIG_FILE_NAME} in ${here}.`,
|
|
15081
|
+
candidates.length ? `Databases found in: ${candidates.map((candidate) => path8.relative(cwd, candidate)).join(", ")}. Pass --dir <name> or cd into one.` : 'Pass its id or name: softr db get <database>. "softr db list" shows the ones the token reaches.'
|
|
15082
|
+
);
|
|
14766
15083
|
}
|
|
15084
|
+
return { databaseId: config.databaseId, dir: here, wrote: false };
|
|
14767
15085
|
}
|
|
14768
|
-
|
|
15086
|
+
const database = await findReachableDatabase(client, reference);
|
|
15087
|
+
const { dir, existing } = await databaseDirFor(cwd, explicitDir, database.id, slug(database.name, "database"));
|
|
15088
|
+
if (!existing) {
|
|
15089
|
+
await writeDbConfig(dir, { databaseId: database.id });
|
|
15090
|
+
}
|
|
15091
|
+
return { databaseId: database.id, dir, wrote: !existing };
|
|
14769
15092
|
}
|
|
14770
|
-
|
|
14771
|
-
|
|
14772
|
-
|
|
14773
|
-
|
|
14774
|
-
|
|
14775
|
-
|
|
14776
|
-
|
|
14777
|
-
|
|
14778
|
-
|
|
14779
|
-
|
|
14780
|
-
|
|
14781
|
-
|
|
14782
|
-
|
|
14783
|
-
|
|
14784
|
-
|
|
14785
|
-
|
|
14786
|
-
|
|
14787
|
-
]);
|
|
15093
|
+
|
|
15094
|
+
// src/commands/db/help.ts
|
|
15095
|
+
var DATABASE_ID_HELP = `the database id (default: $SOFTR_DATABASE_ID, else the one in ${DB_CONFIG_FILE_NAME})`;
|
|
15096
|
+
var DATABASE_DIR_HELP = `database directory, the one holding ${DB_CONFIG_FILE_NAME} (default: the current directory)`;
|
|
15097
|
+
|
|
15098
|
+
// src/api/studio.ts
|
|
15099
|
+
async function resolveTokenOwner(client) {
|
|
15100
|
+
try {
|
|
15101
|
+
return await studioRequest(client, "POST", "/v1/users/me/token", { token: client.token });
|
|
15102
|
+
} catch (error) {
|
|
15103
|
+
if (error instanceof SoftrApiError && error.status === 404) {
|
|
15104
|
+
throw new CliError(
|
|
15105
|
+
"The studio does not know this token: revoked, expired, or from another environment.",
|
|
15106
|
+
'Create a new personal API token in the studio, or run "softr login" for the browser login.'
|
|
15107
|
+
);
|
|
15108
|
+
}
|
|
15109
|
+
throw error;
|
|
14788
15110
|
}
|
|
14789
|
-
return table.toString();
|
|
14790
15111
|
}
|
|
14791
|
-
|
|
14792
|
-
|
|
14793
|
-
|
|
14794
|
-
|
|
14795
|
-
|
|
14796
|
-
|
|
14797
|
-
|
|
14798
|
-
|
|
15112
|
+
var WORKFLOWS_READ_SCOPE = "workflows:read";
|
|
15113
|
+
var DATABASES_SCHEMA_WRITE_SCOPE = "databases.schema:write";
|
|
15114
|
+
function grantsActionOf(statements, prefix) {
|
|
15115
|
+
return (statements ?? []).some(
|
|
15116
|
+
(statement) => statement.actions.some((action) => action === "*" || action.startsWith(prefix))
|
|
15117
|
+
);
|
|
15118
|
+
}
|
|
15119
|
+
function grantsWorkflows(statements) {
|
|
15120
|
+
return grantsActionOf(statements, "workflows:");
|
|
15121
|
+
}
|
|
15122
|
+
function grantsDatabases(statements) {
|
|
15123
|
+
return grantsActionOf(statements, "databases.");
|
|
15124
|
+
}
|
|
15125
|
+
async function listWorkspacesWith(client, scope) {
|
|
15126
|
+
const reachable = await listWorkspaces(client);
|
|
15127
|
+
return { reachable, granted: reachable.filter((workspace) => !workspace.scopes || workspace.scopes.includes(scope)) };
|
|
15128
|
+
}
|
|
15129
|
+
async function listWorkspaces(client) {
|
|
15130
|
+
const workspaces = await studioRequest(client, "GET", "/v1/api/me/workspaces");
|
|
15131
|
+
return workspaces.map((workspace) => ({
|
|
15132
|
+
id: workspace.id,
|
|
15133
|
+
name: workspace.name,
|
|
15134
|
+
...workspace.role ? { userRole: workspace.role } : {},
|
|
15135
|
+
isDefault: workspace.isDefault,
|
|
15136
|
+
...workspace.scopes ? { scopes: workspace.scopes } : {}
|
|
15137
|
+
}));
|
|
15138
|
+
}
|
|
15139
|
+
function listWorkspaceIntegrations(client, workspaceId) {
|
|
15140
|
+
return studioRequest(client, "GET", `/v1/api/me/workspaces/${workspaceId}/integrations`);
|
|
15141
|
+
}
|
|
15142
|
+
|
|
15143
|
+
// src/commands/init.ts
|
|
15144
|
+
import path10 from "node:path";
|
|
15145
|
+
|
|
15146
|
+
// src/api/workflows.ts
|
|
15147
|
+
function getWorkflow(client, workflowId) {
|
|
15148
|
+
return apiRequest(client, "GET", `/v1/workflows/${workflowId}`);
|
|
15149
|
+
}
|
|
15150
|
+
function updateWorkflow(client, workflowId, body) {
|
|
15151
|
+
return apiRequest(client, "PUT", `/v1/workflows/${workflowId}`, body);
|
|
15152
|
+
}
|
|
15153
|
+
function getWorkflowOutputs(client, workflowId) {
|
|
15154
|
+
return apiRequest(client, "GET", `/v1/workflows/${workflowId}/outputs`);
|
|
15155
|
+
}
|
|
15156
|
+
function listWorkflows(client, workspaceId, { applicationId } = {}) {
|
|
15157
|
+
const query = new URLSearchParams({ workspaceId, ...applicationId ? { applicationId } : {}, limit: "500" });
|
|
15158
|
+
return apiRequest(client, "GET", `/v1/workflows?${query}`);
|
|
15159
|
+
}
|
|
15160
|
+
function pinWorkflow(client, workflowId, applicationId) {
|
|
15161
|
+
return apiRequestVoid(client, "POST", `/v1/workflows/${workflowId}/pinned-applications/${applicationId}`);
|
|
15162
|
+
}
|
|
15163
|
+
function unpinWorkflow(client, workflowId, applicationId) {
|
|
15164
|
+
return apiRequestVoid(client, "DELETE", `/v1/workflows/${workflowId}/pinned-applications/${applicationId}`);
|
|
15165
|
+
}
|
|
15166
|
+
function testAction(client, workflowId, hierarchyPath2, mode) {
|
|
15167
|
+
return apiRequest(client, "POST", `/v1/workflows/${workflowId}/outputs/${mode}/actions/${hierarchyPath2}`, {});
|
|
15168
|
+
}
|
|
15169
|
+
function testTrigger(client, workflowId, triggerId) {
|
|
15170
|
+
return apiRequest(client, "POST", `/v1/workflows/${workflowId}/outputs/triggers/${triggerId}/test`, {});
|
|
15171
|
+
}
|
|
15172
|
+
function saveTriggerSample(client, workflowId, triggerId, sample) {
|
|
15173
|
+
return apiRequestVoid(client, "POST", `/v1/workflows/${workflowId}/sample-output/triggers/${triggerId}`, sample);
|
|
15174
|
+
}
|
|
15175
|
+
function createWorkflow(client, body) {
|
|
15176
|
+
return apiRequest(client, "POST", "/v1/workflows", body);
|
|
15177
|
+
}
|
|
15178
|
+
function enableWorkflow(client, workflowId) {
|
|
15179
|
+
return apiRequestVoid(client, "PUT", `/v1/workflows/${workflowId}/enabled`);
|
|
15180
|
+
}
|
|
15181
|
+
function disableWorkflow(client, workflowId) {
|
|
15182
|
+
return apiRequestVoid(client, "DELETE", `/v1/workflows/${workflowId}/enabled`);
|
|
15183
|
+
}
|
|
15184
|
+
async function getEnabledVersion(client, workflowId) {
|
|
15185
|
+
try {
|
|
15186
|
+
return await apiRequest(client, "GET", `/v1/workflows/${workflowId}/enabled`);
|
|
15187
|
+
} catch (error) {
|
|
15188
|
+
if (error instanceof SoftrApiError && error.status === 404) {
|
|
15189
|
+
return null;
|
|
15190
|
+
}
|
|
15191
|
+
throw error;
|
|
14799
15192
|
}
|
|
14800
|
-
|
|
14801
|
-
|
|
14802
|
-
|
|
14803
|
-
|
|
14804
|
-
|
|
14805
|
-
|
|
14806
|
-
|
|
14807
|
-
|
|
14808
|
-
|
|
15193
|
+
}
|
|
15194
|
+
function getWorkflowReadiness(client, workflowId) {
|
|
15195
|
+
return apiRequest(client, "GET", `/v1/workflows/${workflowId}/readiness`);
|
|
15196
|
+
}
|
|
15197
|
+
|
|
15198
|
+
// src/project/state.ts
|
|
15199
|
+
import { mkdir as mkdir6, readFile as readFile7, writeFile as writeFile7 } from "node:fs/promises";
|
|
15200
|
+
import path9 from "node:path";
|
|
15201
|
+
var STATE_DIR_NAME = ".softr-workflows";
|
|
15202
|
+
function stateDir(dir) {
|
|
15203
|
+
return path9.join(dir, STATE_DIR_NAME);
|
|
15204
|
+
}
|
|
15205
|
+
async function readState(dir) {
|
|
15206
|
+
try {
|
|
15207
|
+
return JSON.parse(await readFile7(path9.join(stateDir(dir), "state.json"), "utf8"));
|
|
15208
|
+
} catch (error) {
|
|
15209
|
+
if (error.code === "ENOENT") {
|
|
15210
|
+
return void 0;
|
|
15211
|
+
}
|
|
15212
|
+
throw error;
|
|
14809
15213
|
}
|
|
14810
|
-
return blocks.join("\n\n");
|
|
14811
15214
|
}
|
|
14812
|
-
async function
|
|
14813
|
-
|
|
14814
|
-
|
|
15215
|
+
async function writeState(dir, state) {
|
|
15216
|
+
await mkdir6(stateDir(dir), { recursive: true });
|
|
15217
|
+
const value = { ...state, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
15218
|
+
await writeFile7(path9.join(stateDir(dir), "state.json"), JSON.stringify(value, null, 2) + "\n");
|
|
14815
15219
|
}
|
|
14816
|
-
function
|
|
14817
|
-
|
|
14818
|
-
|
|
14819
|
-
|
|
14820
|
-
|
|
14821
|
-
|
|
14822
|
-
|
|
14823
|
-
|
|
14824
|
-
|
|
14825
|
-
|
|
15220
|
+
async function readStateFile(dir, name) {
|
|
15221
|
+
try {
|
|
15222
|
+
return JSON.parse(await readFile7(path9.join(stateDir(dir), name), "utf8"));
|
|
15223
|
+
} catch (error) {
|
|
15224
|
+
if (error.code === "ENOENT") {
|
|
15225
|
+
return void 0;
|
|
15226
|
+
}
|
|
15227
|
+
throw error;
|
|
15228
|
+
}
|
|
15229
|
+
}
|
|
15230
|
+
async function writeStateFile(dir, name, value) {
|
|
15231
|
+
await mkdir6(stateDir(dir), { recursive: true });
|
|
15232
|
+
const file = path9.join(stateDir(dir), name);
|
|
15233
|
+
await writeFile7(file, JSON.stringify(value, null, 2) + "\n");
|
|
15234
|
+
return file;
|
|
14826
15235
|
}
|
|
14827
15236
|
|
|
14828
15237
|
// src/catalog/schema.ts
|
|
@@ -15056,302 +15465,855 @@ function registerSchema(program3) {
|
|
|
15056
15465
|
});
|
|
15057
15466
|
}
|
|
15058
15467
|
|
|
15059
|
-
// src/commands/
|
|
15060
|
-
|
|
15061
|
-
|
|
15062
|
-
|
|
15063
|
-
|
|
15064
|
-
if (explicitDir) {
|
|
15065
|
-
return path8.resolve(cwd, explicitDir);
|
|
15468
|
+
// src/commands/init.ts
|
|
15469
|
+
var TRIGGER_ID = "trigger";
|
|
15470
|
+
function requireGrantedWorkspace({ reachable, granted }, requested, scope) {
|
|
15471
|
+
if (granted.some((workspace) => workspace.id === requested)) {
|
|
15472
|
+
return;
|
|
15066
15473
|
}
|
|
15067
|
-
if (
|
|
15068
|
-
|
|
15069
|
-
|
|
15070
|
-
|
|
15071
|
-
|
|
15072
|
-
"Run pull from the parent directory (a new subdirectory is created) or pass --dir."
|
|
15073
|
-
);
|
|
15074
|
-
}
|
|
15075
|
-
return cwd;
|
|
15474
|
+
if (reachable.some((workspace) => workspace.id === requested)) {
|
|
15475
|
+
throw new CliError(
|
|
15476
|
+
`The token does not grant ${scope} on workspace ${requested}.`,
|
|
15477
|
+
`Check the token's grants with "softr whoami", or run "softr login" again.`
|
|
15478
|
+
);
|
|
15076
15479
|
}
|
|
15077
|
-
|
|
15480
|
+
throw new CliError(
|
|
15481
|
+
`You have no access to workspace ${requested}.`,
|
|
15482
|
+
`Known workspaces: ${granted.map((w) => `${w.name} (${w.id})`).join(", ")}`
|
|
15483
|
+
);
|
|
15078
15484
|
}
|
|
15079
|
-
function
|
|
15080
|
-
|
|
15081
|
-
|
|
15082
|
-
|
|
15083
|
-
|
|
15084
|
-
|
|
15085
|
-
|
|
15086
|
-
|
|
15087
|
-
|
|
15088
|
-
|
|
15485
|
+
async function resolveWorkspace(client, requested, scope = WORKFLOWS_READ_SCOPE) {
|
|
15486
|
+
const listing = await listWorkspacesWith(client, scope);
|
|
15487
|
+
const workspaces = listing.granted;
|
|
15488
|
+
if (requested) {
|
|
15489
|
+
requireGrantedWorkspace(listing, requested, scope);
|
|
15490
|
+
return requested;
|
|
15491
|
+
}
|
|
15492
|
+
if (workspaces.length === 1 && workspaces[0]) {
|
|
15493
|
+
return workspaces[0].id;
|
|
15494
|
+
}
|
|
15495
|
+
if (!workspaces.length) {
|
|
15496
|
+
throw new CliError(
|
|
15497
|
+
`The token grants ${scope} on no workspace.`,
|
|
15498
|
+
`Check the token's grants with "softr whoami", or run "softr login" again.`
|
|
15499
|
+
);
|
|
15500
|
+
}
|
|
15501
|
+
const preferred = workspaces.find((workspace) => workspace.isDefault);
|
|
15502
|
+
if (preferred) {
|
|
15503
|
+
return preferred.id;
|
|
15504
|
+
}
|
|
15505
|
+
throw new CliError(
|
|
15506
|
+
"Pass --workspace <id>: you have access to several workspaces.",
|
|
15507
|
+
`Choose one of: ${workspaces.map((w) => `${w.name} (${w.id})`).join(", ")}`
|
|
15508
|
+
);
|
|
15509
|
+
}
|
|
15510
|
+
function registerInit(program3) {
|
|
15511
|
+
program3.command("init").description("Create a new workflow with one trigger, on the server and as a local directory").argument("<title>", "the workflow title").option(
|
|
15512
|
+
"--workspace <id>",
|
|
15513
|
+
"workspace to create it in (default: $SOFTR_WORKSPACE_ID, else your only or default workspace)"
|
|
15514
|
+
).option("--pin-to-app <id>", "application to pin the workflow to (default: $SOFTR_PINNED_APP_ID when set)").option("--trigger <TYPE>", 'trigger type from "spec" (default WEBHOOK)', "WEBHOOK").option("--dir <path>", "directory to create (default: ./<title-slug>)").action(
|
|
15515
|
+
async (title, opts, command) => {
|
|
15516
|
+
const options = globalOptions(command);
|
|
15517
|
+
const cwd = process.cwd();
|
|
15518
|
+
const dir = path10.resolve(cwd, opts.dir ?? slug(title));
|
|
15519
|
+
if (await configExists(dir)) {
|
|
15520
|
+
throw new CliError(`${path10.join(dir, CONFIG_FILE_NAME)} already exists.`, "Pick another --dir or title.");
|
|
15521
|
+
}
|
|
15522
|
+
const trigger = findCatalogEntry(opts.trigger, "trigger");
|
|
15523
|
+
const target = resolveTarget();
|
|
15524
|
+
const client = await authenticatedClient(target);
|
|
15525
|
+
const workspaceId = await resolveWorkspace(client, opts.workspace ?? process.env["SOFTR_WORKSPACE_ID"]);
|
|
15526
|
+
const pinToApplicationId = opts.pinToApp ?? process.env["SOFTR_PINNED_APP_ID"];
|
|
15527
|
+
const created = await createWorkflow(client, {
|
|
15528
|
+
workspaceId,
|
|
15529
|
+
applicationId: null,
|
|
15530
|
+
...pinToApplicationId ? { pinToApplicationId } : {},
|
|
15531
|
+
title,
|
|
15532
|
+
thumbnail: null,
|
|
15533
|
+
triggers: [{ id: TRIGGER_ID, type: trigger.type, version: trigger.version, inputs: trigger.template }],
|
|
15534
|
+
actions: [],
|
|
15535
|
+
paths: [],
|
|
15536
|
+
// Like the studio: the author's time zone, so schedules and waits mean what they read.
|
|
15537
|
+
configuration: { timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC" }
|
|
15089
15538
|
});
|
|
15090
|
-
|
|
15539
|
+
const workflow = await getWorkflow(client, created.id);
|
|
15540
|
+
const config = { $schema: SCHEMA_POINTER, ...toLocalConfig(workflow) };
|
|
15541
|
+
const configFile2 = await writeConfig(dir, config);
|
|
15542
|
+
await writeSchemaFile(dir);
|
|
15543
|
+
const syncedHash = definitionHash((await packWorkflow(config, dir)).packed);
|
|
15544
|
+
await writeState(dir, {
|
|
15545
|
+
...target,
|
|
15546
|
+
workflowId: created.id,
|
|
15547
|
+
version: workflow.version,
|
|
15548
|
+
draft: workflow.draft,
|
|
15549
|
+
syncedHash
|
|
15550
|
+
});
|
|
15551
|
+
const studioUrl = `${target.studioUrl}/workflow/${created.id}`;
|
|
15552
|
+
emit(
|
|
15553
|
+
options,
|
|
15554
|
+
`Created "${title}" (${created.id}) with a ${trigger.type} trigger "${TRIGGER_ID}" in workspace ${workspaceId}${pinToApplicationId ? `, linked to application ${pinToApplicationId}` : ""}.
|
|
15555
|
+
${path10.relative(cwd, configFile2)}
|
|
15556
|
+
${studioUrl}
|
|
15557
|
+
Next: softr workflows add <TYPE> --dir ${path10.relative(cwd, dir) || "."} (see "spec" for the types), then push and test.`,
|
|
15558
|
+
{
|
|
15559
|
+
workflowId: created.id,
|
|
15560
|
+
workspaceId,
|
|
15561
|
+
pinnedToApplicationId: pinToApplicationId ?? null,
|
|
15562
|
+
title,
|
|
15563
|
+
triggerId: TRIGGER_ID,
|
|
15564
|
+
triggerType: trigger.type,
|
|
15565
|
+
dir,
|
|
15566
|
+
studioUrl
|
|
15567
|
+
}
|
|
15568
|
+
);
|
|
15091
15569
|
}
|
|
15092
|
-
|
|
15093
|
-
|
|
15094
|
-
|
|
15095
|
-
|
|
15096
|
-
|
|
15097
|
-
|
|
15570
|
+
);
|
|
15571
|
+
}
|
|
15572
|
+
|
|
15573
|
+
// src/commands/db/databases.ts
|
|
15574
|
+
function renderDatabases(databases) {
|
|
15575
|
+
if (!databases.length) {
|
|
15576
|
+
return "The token reaches no database. Create one with: softr db create <name>";
|
|
15577
|
+
}
|
|
15578
|
+
const table = new import_cli_table3.default(plainTable(["ID", "Name", "Workspace", "Tables"], [38, 34, 38, 8]));
|
|
15579
|
+
for (const database of databases) {
|
|
15580
|
+
table.push([database.id, database.name, database.workspaceId, String(database.tablesCount)]);
|
|
15581
|
+
}
|
|
15582
|
+
return table.toString();
|
|
15583
|
+
}
|
|
15584
|
+
function renderDatabase(database, tables) {
|
|
15585
|
+
return [
|
|
15586
|
+
`${database.name} (${database.id})`,
|
|
15587
|
+
...database.description ? [` ${database.description}`] : [],
|
|
15588
|
+
` Workspace: ${database.workspaceId}`,
|
|
15589
|
+
` Created: ${database.createdAt}`,
|
|
15590
|
+
` Updated: ${database.updatedAt}`,
|
|
15591
|
+
` Tables: ${tables.length}`,
|
|
15592
|
+
...tables.map((table) => ` ${table.name} (${table.id}), ${table.fields.length} fields`)
|
|
15593
|
+
].join("\n");
|
|
15594
|
+
}
|
|
15595
|
+
function relativeDir(cwd, dir) {
|
|
15596
|
+
return path11.relative(cwd, dir) || ".";
|
|
15597
|
+
}
|
|
15598
|
+
function registerDatabases(db2) {
|
|
15599
|
+
db2.command("list").description("List the databases the token reaches, across all workspaces").action(async (_opts, command) => {
|
|
15600
|
+
const options = globalOptions(command);
|
|
15601
|
+
const client = await authenticatedClient(resolveTarget());
|
|
15602
|
+
const databases = await listDatabases(client);
|
|
15603
|
+
emit(options, renderDatabases(databases), { databases });
|
|
15604
|
+
});
|
|
15605
|
+
db2.command("get").description(
|
|
15606
|
+
`Show a database with its tables and keep its directory: ${DB_CONFIG_FILE_NAME} and the schema cache in ${DB_CACHE_DIR_NAME}/`
|
|
15607
|
+
).argument(
|
|
15608
|
+
"[database]",
|
|
15609
|
+
`the database id or exact name (default: $SOFTR_DATABASE_ID, else the one in ${DB_CONFIG_FILE_NAME})`
|
|
15610
|
+
).option(
|
|
15611
|
+
"--dir <path>",
|
|
15612
|
+
"database directory (default: this directory if it holds a database, else a new ./<name-slug> for the one given)"
|
|
15613
|
+
).action(async (argument, opts, command) => {
|
|
15614
|
+
const options = globalOptions(command);
|
|
15615
|
+
const cwd = process.cwd();
|
|
15616
|
+
const reference = argument ?? (process.env[DATABASE_ID_ENV_VAR] || void 0);
|
|
15617
|
+
const client = await authenticatedClient(resolveTarget());
|
|
15618
|
+
const { databaseId, dir, wrote } = await bindDatabaseDir(client, cwd, reference, opts.dir);
|
|
15619
|
+
const { database, tables } = await refreshSchemaCache(client, dir, databaseId);
|
|
15620
|
+
const note = wrote ? `
|
|
15621
|
+
Wrote ${path11.join(relativeDir(cwd, dir), DB_CONFIG_FILE_NAME)}.` : "";
|
|
15622
|
+
emit(options, renderDatabase(database, tables) + note, { database, tables, dir });
|
|
15623
|
+
});
|
|
15624
|
+
db2.command("create").description(`Create an empty database and a directory for it with ${DB_CONFIG_FILE_NAME}`).argument("<name>", "the database name").option("--description <text>", "what the database holds").option(
|
|
15625
|
+
"--workspace <id>",
|
|
15626
|
+
"workspace to create it in (default: $SOFTR_WORKSPACE_ID, else your only or default workspace)"
|
|
15627
|
+
).option("--dir <path>", "directory to create (default: ./<name-slug>)").action(
|
|
15628
|
+
async (name, opts, command) => {
|
|
15629
|
+
const options = globalOptions(command);
|
|
15630
|
+
const cwd = process.cwd();
|
|
15631
|
+
const { dir } = await databaseDirFor(cwd, opts.dir, void 0, slug(name, "database"));
|
|
15632
|
+
const client = await authenticatedClient(resolveTarget());
|
|
15633
|
+
const workspaceId = await resolveWorkspace(
|
|
15634
|
+
client,
|
|
15635
|
+
opts.workspace ?? process.env["SOFTR_WORKSPACE_ID"],
|
|
15636
|
+
DATABASES_SCHEMA_WRITE_SCOPE
|
|
15637
|
+
);
|
|
15638
|
+
const database = await createDatabase(client, { workspaceId, name, description: opts.description });
|
|
15639
|
+
await writeDbConfig(dir, { databaseId: database.id });
|
|
15640
|
+
await refreshSchemaCache(client, dir, database.id);
|
|
15641
|
+
emit(
|
|
15642
|
+
options,
|
|
15643
|
+
`Created database ${database.name} (${database.id}) in workspace ${workspaceId}.
|
|
15644
|
+
${path11.join(relativeDir(cwd, dir), DB_CONFIG_FILE_NAME)}`,
|
|
15645
|
+
{ database, dir }
|
|
15098
15646
|
);
|
|
15099
15647
|
}
|
|
15100
|
-
|
|
15101
|
-
|
|
15102
|
-
|
|
15103
|
-
|
|
15104
|
-
|
|
15105
|
-
|
|
15106
|
-
|
|
15107
|
-
|
|
15648
|
+
);
|
|
15649
|
+
db2.command("update").description("Change a database's name or description").argument("[databaseId]", DATABASE_ID_HELP).option("--name <name>", "the new name").option("--description <text>", "the new description").option("--dir <path>", DATABASE_DIR_HELP).action(
|
|
15650
|
+
async (databaseId, opts, command) => {
|
|
15651
|
+
if (!opts.name && !opts.description) {
|
|
15652
|
+
throw new CliError("Nothing to change: pass --name, --description or both.");
|
|
15653
|
+
}
|
|
15654
|
+
const options = globalOptions(command);
|
|
15655
|
+
const target = await resolveDatabase(process.cwd(), databaseId, opts.dir);
|
|
15656
|
+
const client = await authenticatedClient(resolveTarget());
|
|
15657
|
+
const database = await updateDatabase(client, target.databaseId, {
|
|
15658
|
+
name: opts.name,
|
|
15659
|
+
description: opts.description
|
|
15660
|
+
});
|
|
15661
|
+
if (target.dir) {
|
|
15662
|
+
const cache = await readSchemaCache(target.dir);
|
|
15663
|
+
await (cache ? writeSchemaCache(target.dir, database, cache.tables) : refreshSchemaCache(client, target.dir, database.id));
|
|
15664
|
+
}
|
|
15665
|
+
emit(options, `Updated database ${database.name} (${database.id}).`, { database });
|
|
15108
15666
|
}
|
|
15109
|
-
|
|
15110
|
-
|
|
15111
|
-
|
|
15112
|
-
const
|
|
15113
|
-
const
|
|
15114
|
-
await
|
|
15115
|
-
if (
|
|
15116
|
-
|
|
15667
|
+
);
|
|
15668
|
+
db2.command("delete").description("Delete a database; one that still has tables needs --force").argument("[databaseId]", DATABASE_ID_HELP).option("--force", "also delete its tables and all their records").option("--dir <path>", DATABASE_DIR_HELP).action(async (databaseId, opts, command) => {
|
|
15669
|
+
const options = globalOptions(command);
|
|
15670
|
+
const target = await resolveDatabase(process.cwd(), databaseId, opts.dir);
|
|
15671
|
+
const client = await authenticatedClient(resolveTarget());
|
|
15672
|
+
await deleteDatabase(client, target.databaseId, opts.force === true);
|
|
15673
|
+
if (target.dir) {
|
|
15674
|
+
await removeSchemaCache(target.dir);
|
|
15117
15675
|
}
|
|
15118
|
-
|
|
15119
|
-
emitWithVersionNote(
|
|
15676
|
+
emit(
|
|
15120
15677
|
options,
|
|
15121
|
-
|
|
15122
|
-
|
|
15123
|
-
|
|
15124
|
-
{
|
|
15125
|
-
workflowId,
|
|
15126
|
-
apiUrl: target.apiUrl,
|
|
15127
|
-
title: workflow.title,
|
|
15128
|
-
version: workflow.version,
|
|
15129
|
-
draft: workflow.draft,
|
|
15130
|
-
enabled: workflow.enabled ?? false,
|
|
15131
|
-
dir,
|
|
15132
|
-
files,
|
|
15133
|
-
outputsFile: outputsFile ?? null
|
|
15134
|
-
}
|
|
15678
|
+
`Deleted database ${target.databaseId}.` + (target.dir ? `
|
|
15679
|
+
${path11.join(relativeDir(process.cwd(), target.dir), DB_CONFIG_FILE_NAME)} still names the deleted database; delete the directory.` : ""),
|
|
15680
|
+
{ deleted: target.databaseId }
|
|
15135
15681
|
);
|
|
15136
15682
|
});
|
|
15137
15683
|
}
|
|
15138
15684
|
|
|
15139
|
-
// src/commands/
|
|
15140
|
-
|
|
15141
|
-
|
|
15142
|
-
|
|
15685
|
+
// src/commands/db/records.ts
|
|
15686
|
+
import { readFile as readFile9 } from "node:fs/promises";
|
|
15687
|
+
import path12 from "node:path";
|
|
15688
|
+
|
|
15689
|
+
// src/commands/db/json-input.ts
|
|
15690
|
+
import { readFile as readFile8 } from "node:fs/promises";
|
|
15691
|
+
async function readJsonArgument(value, option, hint) {
|
|
15692
|
+
let text = value;
|
|
15693
|
+
if (value === "-") {
|
|
15694
|
+
const chunks = [];
|
|
15695
|
+
for await (const chunk of process.stdin) {
|
|
15696
|
+
chunks.push(chunk);
|
|
15697
|
+
}
|
|
15698
|
+
text = Buffer.concat(chunks).toString("utf8");
|
|
15699
|
+
} else if (value.startsWith("@")) {
|
|
15700
|
+
text = await readFile8(value.slice(1), "utf8");
|
|
15143
15701
|
}
|
|
15144
|
-
|
|
15145
|
-
|
|
15146
|
-
|
|
15147
|
-
}
|
|
15702
|
+
try {
|
|
15703
|
+
return JSON.parse(text);
|
|
15704
|
+
} catch {
|
|
15705
|
+
throw new CliError(`${option} is not valid JSON.`, hint ?? "Pass inline JSON, @<file> or - for stdin.");
|
|
15148
15706
|
}
|
|
15149
15707
|
}
|
|
15150
|
-
|
|
15151
|
-
|
|
15152
|
-
|
|
15153
|
-
|
|
15154
|
-
|
|
15155
|
-
|
|
15156
|
-
|
|
15157
|
-
|
|
15158
|
-
|
|
15159
|
-
|
|
15708
|
+
|
|
15709
|
+
// src/commands/db/schema.ts
|
|
15710
|
+
var import_cli_table32 = __toESM(require_cli_table3(), 1);
|
|
15711
|
+
var FIELD_REFERENCE_URL = "https://docs.softr.io/softr-api/softr-database-api/table-fields/add-table-field";
|
|
15712
|
+
var TABLE_ARGUMENT = "the table id or exact name";
|
|
15713
|
+
var FIELD_ARGUMENT = "the field id or exact name";
|
|
15714
|
+
function withDatabaseOptions(command) {
|
|
15715
|
+
return command.option("--database <id>", DATABASE_ID_HELP).option("--dir <path>", DATABASE_DIR_HELP);
|
|
15716
|
+
}
|
|
15717
|
+
async function schemaContext(opts) {
|
|
15718
|
+
const target = await resolveDatabase(process.cwd(), opts.database, opts.dir);
|
|
15719
|
+
const client = await authenticatedClient(resolveTarget());
|
|
15720
|
+
const cache = target.dir ? await readSchemaCache(target.dir) : void 0;
|
|
15721
|
+
return { client, databaseId: target.databaseId, dir: target.dir, cache };
|
|
15722
|
+
}
|
|
15723
|
+
async function currentTables(context, fresh) {
|
|
15724
|
+
if (!context.dir) {
|
|
15725
|
+
return listTables(context.client, context.databaseId);
|
|
15726
|
+
}
|
|
15727
|
+
if (!fresh && context.cache) {
|
|
15728
|
+
return context.cache.tables;
|
|
15729
|
+
}
|
|
15730
|
+
context.cache = await refreshSchemaCache(context.client, context.dir, context.databaseId);
|
|
15731
|
+
return context.cache.tables;
|
|
15732
|
+
}
|
|
15733
|
+
async function findTable(context, reference, fresh = false) {
|
|
15734
|
+
const table = byIdOrName(await currentTables(context, fresh), reference, "table") ?? (context.dir && !fresh ? byIdOrName(await currentTables(context, true), reference, "table") : void 0);
|
|
15735
|
+
if (!table) {
|
|
15736
|
+
throw new CliError(
|
|
15737
|
+
`Database ${context.databaseId} has no table with the id or name "${reference}".`,
|
|
15738
|
+
'"softr db tables list" shows its tables.'
|
|
15739
|
+
);
|
|
15740
|
+
}
|
|
15741
|
+
return table;
|
|
15742
|
+
}
|
|
15743
|
+
async function findFields(context, table, references, fresh = false) {
|
|
15744
|
+
const resolve = (current3) => {
|
|
15745
|
+
const fields2 = /* @__PURE__ */ new Map();
|
|
15746
|
+
for (const reference of references) {
|
|
15747
|
+
const field = byIdOrName(current3.fields, reference, "field");
|
|
15748
|
+
if (field) {
|
|
15749
|
+
fields2.set(reference, field);
|
|
15750
|
+
}
|
|
15160
15751
|
}
|
|
15161
|
-
return
|
|
15752
|
+
return fields2;
|
|
15753
|
+
};
|
|
15754
|
+
let current2 = table;
|
|
15755
|
+
let fields = resolve(current2);
|
|
15756
|
+
if (fields.size < new Set(references).size && context.dir && !fresh) {
|
|
15757
|
+
current2 = await findTable(context, table.id, true);
|
|
15758
|
+
fields = resolve(current2);
|
|
15759
|
+
}
|
|
15760
|
+
const missing = references.filter((reference) => !fields.has(reference));
|
|
15761
|
+
if (missing.length) {
|
|
15762
|
+
throw new CliError(
|
|
15763
|
+
`Table ${current2.name} has no field with the id or name ${missing.map((name) => `"${name}"`).join(", ")}.`,
|
|
15764
|
+
`"softr db tables get ${current2.id}" shows its fields.`
|
|
15765
|
+
);
|
|
15162
15766
|
}
|
|
15163
|
-
|
|
15164
|
-
|
|
15165
|
-
|
|
15166
|
-
|
|
15167
|
-
|
|
15767
|
+
return { table: current2, fields };
|
|
15768
|
+
}
|
|
15769
|
+
async function findField(context, tableReference, reference, fresh = false) {
|
|
15770
|
+
const found = await findFields(context, await findTable(context, tableReference, fresh), [reference], fresh);
|
|
15771
|
+
return { table: found.table, field: found.fields.get(reference) };
|
|
15772
|
+
}
|
|
15773
|
+
async function afterSchemaWrite(context) {
|
|
15774
|
+
if (context.dir) {
|
|
15775
|
+
context.cache = await refreshSchemaCache(context.client, context.dir, context.databaseId);
|
|
15168
15776
|
}
|
|
15169
|
-
throw new CliError(
|
|
15170
|
-
leaves.length ? `Several nodes have no successor: ${leaves.map((node) => node.id).join(", ")}.` : "Every node already has a successor.",
|
|
15171
|
-
"Pass --after <nodeId> to say where the new node goes."
|
|
15172
|
-
);
|
|
15173
15777
|
}
|
|
15174
|
-
function
|
|
15175
|
-
|
|
15176
|
-
|
|
15177
|
-
|
|
15178
|
-
|
|
15179
|
-
|
|
15180
|
-
|
|
15181
|
-
|
|
15182
|
-
|
|
15183
|
-
|
|
15184
|
-
|
|
15185
|
-
|
|
15186
|
-
|
|
15187
|
-
|
|
15778
|
+
function parseOptions(value) {
|
|
15779
|
+
return value === void 0 ? Promise.resolve(void 0) : readJsonArgument(value, "--options", `The options of each field type: ${FIELD_REFERENCE_URL}`);
|
|
15780
|
+
}
|
|
15781
|
+
function parseFieldSpec(spec) {
|
|
15782
|
+
const colon = spec.lastIndexOf(":");
|
|
15783
|
+
const name = colon > 0 ? spec.slice(0, colon).trim() : "";
|
|
15784
|
+
const type = colon > 0 ? spec.slice(colon + 1).trim().toUpperCase() : "";
|
|
15785
|
+
if (!name || !type) {
|
|
15786
|
+
throw new CliError(`--field "${spec}" must be <name>:<TYPE>, for example "Due date:DATE".`);
|
|
15787
|
+
}
|
|
15788
|
+
return { name, type };
|
|
15789
|
+
}
|
|
15790
|
+
function renderTables(tables) {
|
|
15791
|
+
if (!tables.length) {
|
|
15792
|
+
return "The database has no tables. Create one with: softr db tables create <name>";
|
|
15793
|
+
}
|
|
15794
|
+
const table = new import_cli_table32.default(plainTable(["ID", "Name", "Fields", "Primary field"], [18, 34, 8, 30]));
|
|
15795
|
+
for (const item of tables) {
|
|
15796
|
+
const primary = item.fields.find((field) => field.id === item.primaryFieldId);
|
|
15797
|
+
table.push([item.id, item.name, String(item.fields.length), primary?.name ?? item.primaryFieldId]);
|
|
15798
|
+
}
|
|
15799
|
+
return table.toString();
|
|
15800
|
+
}
|
|
15801
|
+
function flags(field, primaryFieldId) {
|
|
15802
|
+
return [
|
|
15803
|
+
field.id === primaryFieldId ? "primary" : "",
|
|
15804
|
+
field.required ? "required" : "",
|
|
15805
|
+
field.readonly ? "read-only" : "",
|
|
15806
|
+
field.allowMultipleEntries ? "multiple" : ""
|
|
15807
|
+
].filter(Boolean).join(", ");
|
|
15808
|
+
}
|
|
15809
|
+
function renderTable(item) {
|
|
15810
|
+
const fields = new import_cli_table32.default(plainTable(["ID", "Name", "Type", ""], [18, 30, 18, 30]));
|
|
15811
|
+
for (const field of item.fields) {
|
|
15812
|
+
fields.push([field.id, field.name, field.type, flags(field, item.primaryFieldId)]);
|
|
15813
|
+
}
|
|
15814
|
+
return [
|
|
15815
|
+
`${item.name} (${item.id})`,
|
|
15816
|
+
...item.description ? [` ${item.description}`] : [],
|
|
15817
|
+
` Default view: ${item.defaultViewId}`,
|
|
15818
|
+
fields.toString()
|
|
15819
|
+
].join("\n");
|
|
15820
|
+
}
|
|
15821
|
+
function renderField(table, field) {
|
|
15822
|
+
return [
|
|
15823
|
+
`${field.name} (${field.id}) in ${table.name}`,
|
|
15824
|
+
...field.description ? [` ${field.description}`] : [],
|
|
15825
|
+
` Type: ${field.type}`,
|
|
15826
|
+
...flags(field, table.primaryFieldId) ? [` Flags: ${flags(field, table.primaryFieldId)}`] : [],
|
|
15827
|
+
` Options: ${JSON.stringify(field.options ?? null)}`
|
|
15828
|
+
].join("\n");
|
|
15829
|
+
}
|
|
15830
|
+
function renderViews(views) {
|
|
15831
|
+
const table = new import_cli_table32.default(plainTable(["ID", "Name"], [18, 50]));
|
|
15832
|
+
for (const view of views) {
|
|
15833
|
+
table.push([view.id, view.name]);
|
|
15834
|
+
}
|
|
15835
|
+
return table.toString();
|
|
15836
|
+
}
|
|
15837
|
+
function registerTables(db2) {
|
|
15838
|
+
const tables = db2.command("tables").description("The tables of a database, with their fields");
|
|
15839
|
+
withDatabaseOptions(tables.command("list").description("List the tables of the database")).action(
|
|
15840
|
+
async (opts, command) => {
|
|
15841
|
+
const options = globalOptions(command);
|
|
15842
|
+
const context = await schemaContext(opts);
|
|
15843
|
+
const items = await currentTables(context, true);
|
|
15844
|
+
emit(options, renderTables(items), { databaseId: context.databaseId, tables: items });
|
|
15188
15845
|
}
|
|
15189
|
-
|
|
15190
|
-
|
|
15191
|
-
|
|
15192
|
-
|
|
15193
|
-
|
|
15846
|
+
);
|
|
15847
|
+
withDatabaseOptions(
|
|
15848
|
+
tables.command("get").description("Show a table with its fields").argument("<table>", TABLE_ARGUMENT)
|
|
15849
|
+
).action(async (reference, opts, command) => {
|
|
15850
|
+
const options = globalOptions(command);
|
|
15851
|
+
const context = await schemaContext(opts);
|
|
15852
|
+
const table = await findTable(context, reference, true);
|
|
15853
|
+
emit(options, renderTable(table), { table });
|
|
15854
|
+
});
|
|
15855
|
+
withDatabaseOptions(
|
|
15856
|
+
tables.command("create").description(
|
|
15857
|
+
"Create a table with at least one field; the first one names a record unless --primary-field says otherwise"
|
|
15858
|
+
).argument("<name>", "the table name").option("--description <text>", "what the table holds").option(
|
|
15859
|
+
"--field <name:TYPE>",
|
|
15860
|
+
`a field to create with it, repeatable (types and their options: ${FIELD_REFERENCE_URL}); add fields with options through "softr db fields add"`,
|
|
15861
|
+
(value, previous) => [...previous, value],
|
|
15862
|
+
[]
|
|
15863
|
+
).option("--primary-field <name>", "the field that names a record (default: the first field)")
|
|
15864
|
+
).action(
|
|
15865
|
+
async (name, opts, command) => {
|
|
15866
|
+
const options = globalOptions(command);
|
|
15867
|
+
if (!opts.field.length) {
|
|
15194
15868
|
throw new CliError(
|
|
15195
|
-
|
|
15869
|
+
'A table needs at least one field: pass --field <name>:<TYPE>, for example --field "Name:SINGLE_LINE_TEXT".',
|
|
15870
|
+
"Repeat --field for more; the first one names a record unless --primary-field names another."
|
|
15196
15871
|
);
|
|
15197
15872
|
}
|
|
15198
|
-
|
|
15199
|
-
|
|
15873
|
+
const fields = opts.field.map(parseFieldSpec);
|
|
15874
|
+
if (opts.primaryField && !fields.some((field) => field.name === opts.primaryField)) {
|
|
15875
|
+
throw new CliError(`--primary-field "${opts.primaryField}" is not one of the --field names.`);
|
|
15200
15876
|
}
|
|
15201
|
-
await
|
|
15202
|
-
|
|
15203
|
-
|
|
15204
|
-
|
|
15205
|
-
|
|
15206
|
-
|
|
15207
|
-
|
|
15208
|
-
|
|
15209
|
-
|
|
15210
|
-
emitWithVersionNote(
|
|
15211
|
-
options,
|
|
15212
|
-
config,
|
|
15213
|
-
`Added ${entry.type} trigger "${id}"${opts.to ? ` leading to "${opts.to}"` : ""}.
|
|
15214
|
-
${notes.join("\n")}`,
|
|
15215
|
-
{
|
|
15216
|
-
id,
|
|
15217
|
-
kind: "trigger",
|
|
15218
|
-
type: entry.type,
|
|
15219
|
-
to: opts.to ?? null
|
|
15220
|
-
}
|
|
15221
|
-
);
|
|
15222
|
-
return;
|
|
15877
|
+
const context = await schemaContext(opts);
|
|
15878
|
+
const table = await createTable(context.client, context.databaseId, {
|
|
15879
|
+
name,
|
|
15880
|
+
description: opts.description,
|
|
15881
|
+
primaryFieldName: opts.primaryField ?? fields[0].name,
|
|
15882
|
+
fields
|
|
15883
|
+
});
|
|
15884
|
+
await afterSchemaWrite(context);
|
|
15885
|
+
emit(options, `Created table ${table.name} (${table.id}) with ${table.fields.length} fields.`, { table });
|
|
15223
15886
|
}
|
|
15224
|
-
|
|
15225
|
-
|
|
15226
|
-
|
|
15227
|
-
|
|
15887
|
+
);
|
|
15888
|
+
withDatabaseOptions(
|
|
15889
|
+
tables.command("update").description("Change a table's name or description").argument("<table>", TABLE_ARGUMENT).option("--name <name>", "the new name").option("--description <text>", "the new description")
|
|
15890
|
+
).action(
|
|
15891
|
+
async (reference, opts, command) => {
|
|
15892
|
+
if (!opts.name && opts.description === void 0) {
|
|
15893
|
+
throw new CliError("Nothing to change: pass --name, --description or both.");
|
|
15228
15894
|
}
|
|
15895
|
+
const options = globalOptions(command);
|
|
15896
|
+
const context = await schemaContext(opts);
|
|
15897
|
+
const current2 = await findTable(context, reference, true);
|
|
15898
|
+
const table = await updateTable(context.client, context.databaseId, current2.id, {
|
|
15899
|
+
name: opts.name ?? current2.name,
|
|
15900
|
+
description: opts.description ?? current2.description
|
|
15901
|
+
});
|
|
15902
|
+
await afterSchemaWrite(context);
|
|
15903
|
+
emit(options, `Updated table ${table.name} (${table.id}).`, { table });
|
|
15229
15904
|
}
|
|
15230
|
-
|
|
15231
|
-
|
|
15232
|
-
|
|
15905
|
+
);
|
|
15906
|
+
withDatabaseOptions(
|
|
15907
|
+
tables.command("delete").description("Delete a table; one that still has records needs --force").argument("<table>", TABLE_ARGUMENT).option("--force", "also delete all its records")
|
|
15908
|
+
).action(async (reference, opts, command) => {
|
|
15909
|
+
const options = globalOptions(command);
|
|
15910
|
+
const context = await schemaContext(opts);
|
|
15911
|
+
const table = await findTable(context, reference, true);
|
|
15912
|
+
await deleteTable(context.client, context.databaseId, table.id, opts.force === true);
|
|
15913
|
+
await afterSchemaWrite(context);
|
|
15914
|
+
emit(options, `Deleted table ${table.name} (${table.id}).`, { deleted: table.id });
|
|
15915
|
+
});
|
|
15916
|
+
}
|
|
15917
|
+
function registerFields(db2) {
|
|
15918
|
+
const fields = db2.command("fields").description("The fields of a table");
|
|
15919
|
+
withDatabaseOptions(
|
|
15920
|
+
fields.command("get").description("Show a field with its options").argument("<table>", TABLE_ARGUMENT).argument("<field>", FIELD_ARGUMENT)
|
|
15921
|
+
).action(async (tableReference, reference, opts, command) => {
|
|
15922
|
+
const options = globalOptions(command);
|
|
15923
|
+
const context = await schemaContext(opts);
|
|
15924
|
+
const { table, field } = await findField(context, tableReference, reference, true);
|
|
15925
|
+
emit(options, renderField(table, field), { tableId: table.id, field });
|
|
15926
|
+
});
|
|
15927
|
+
withDatabaseOptions(
|
|
15928
|
+
fields.command("add").description("Add a field to a table").argument("<table>", TABLE_ARGUMENT).argument("<name>", "the field name").requiredOption("--type <TYPE>", "the field type, for example SINGLE_LINE_TEXT, NUMBER, SELECT, LINKED_RECORD").option("--options <json>", `the type's options as JSON, @<file> or - for stdin (${FIELD_REFERENCE_URL})`).option("--description <text>", "what the field holds").option("--multiple", "allow several values (SELECT, LINKED_RECORD, USER, ATTACHMENT)")
|
|
15929
|
+
).action(
|
|
15930
|
+
async (tableReference, name, opts, command) => {
|
|
15931
|
+
const options = globalOptions(command);
|
|
15932
|
+
const fieldOptions = await parseOptions(opts.options);
|
|
15933
|
+
const context = await schemaContext(opts);
|
|
15934
|
+
const table = await findTable(context, tableReference);
|
|
15935
|
+
const field = await addField(context.client, context.databaseId, table.id, {
|
|
15936
|
+
name,
|
|
15937
|
+
type: opts.type.toUpperCase(),
|
|
15938
|
+
description: opts.description,
|
|
15939
|
+
...fieldOptions === void 0 ? {} : { options: fieldOptions },
|
|
15940
|
+
allowMultipleEntries: opts.multiple === true
|
|
15941
|
+
});
|
|
15942
|
+
await afterSchemaWrite(context);
|
|
15943
|
+
emit(options, `Added field ${field.name} (${field.id}, ${field.type}) to ${table.name}.`, {
|
|
15944
|
+
tableId: table.id,
|
|
15945
|
+
field
|
|
15946
|
+
});
|
|
15233
15947
|
}
|
|
15234
|
-
|
|
15235
|
-
|
|
15236
|
-
|
|
15237
|
-
|
|
15238
|
-
|
|
15239
|
-
|
|
15240
|
-
|
|
15241
|
-
|
|
15242
|
-
|
|
15243
|
-
|
|
15244
|
-
|
|
15245
|
-
emitWithVersionNote(
|
|
15246
|
-
options,
|
|
15247
|
-
config,
|
|
15248
|
-
`Added ${entry.type} "${id}" after "${after}"${opts.inLoop ? ` inside loop "${opts.inLoop}"` : ""}.
|
|
15249
|
-
${next.join("\n")}`,
|
|
15250
|
-
{
|
|
15251
|
-
id,
|
|
15252
|
-
kind: "action",
|
|
15253
|
-
type: entry.type,
|
|
15254
|
-
after,
|
|
15255
|
-
loopId: opts.inLoop ?? null,
|
|
15256
|
-
codeFile: codeFile ? path9.relative(process.cwd(), codeFile) : null
|
|
15948
|
+
);
|
|
15949
|
+
withDatabaseOptions(
|
|
15950
|
+
fields.command("update").description("Change a field: what is not passed stays as it is").argument("<table>", TABLE_ARGUMENT).argument("<field>", FIELD_ARGUMENT).option("--name <name>", "the new name").option("--type <TYPE>", "the new type").option(
|
|
15951
|
+
"--options <json>",
|
|
15952
|
+
`the new options as JSON, @<file> or - for stdin, replacing the current ones (${FIELD_REFERENCE_URL})`
|
|
15953
|
+
).option("--description <text>", "the new description").option("--multiple", "allow several values").option("--no-multiple", "allow one value only")
|
|
15954
|
+
).action(
|
|
15955
|
+
async (tableReference, reference, opts, command) => {
|
|
15956
|
+
const fieldOptions = await parseOptions(opts.options);
|
|
15957
|
+
if (!opts.name && !opts.type && fieldOptions === void 0 && opts.description === void 0 && opts.multiple === void 0) {
|
|
15958
|
+
throw new CliError("Nothing to change: pass --name, --type, --options, --description or --[no-]multiple.");
|
|
15257
15959
|
}
|
|
15258
|
-
|
|
15960
|
+
const options = globalOptions(command);
|
|
15961
|
+
const context = await schemaContext(opts);
|
|
15962
|
+
const { table, field: current2 } = await findField(context, tableReference, reference, true);
|
|
15963
|
+
const field = await updateField(context.client, context.databaseId, table.id, current2.id, {
|
|
15964
|
+
name: opts.name ?? current2.name,
|
|
15965
|
+
type: opts.type?.toUpperCase() ?? current2.type,
|
|
15966
|
+
description: opts.description ?? current2.description,
|
|
15967
|
+
options: fieldOptions ?? current2.options,
|
|
15968
|
+
aiOptions: current2.aiOptions ?? null,
|
|
15969
|
+
defaultValue: current2.defaultValue ?? null,
|
|
15970
|
+
allowMultipleEntries: opts.multiple ?? current2.allowMultipleEntries
|
|
15971
|
+
});
|
|
15972
|
+
await afterSchemaWrite(context);
|
|
15973
|
+
emit(options, `Updated field ${field.name} (${field.id}, ${field.type}) in ${table.name}.`, {
|
|
15974
|
+
tableId: table.id,
|
|
15975
|
+
field
|
|
15976
|
+
});
|
|
15977
|
+
}
|
|
15978
|
+
);
|
|
15979
|
+
withDatabaseOptions(
|
|
15980
|
+
fields.command("delete").description("Delete a field and its values in every record").argument("<table>", TABLE_ARGUMENT).argument("<field>", FIELD_ARGUMENT)
|
|
15981
|
+
).action(async (tableReference, reference, opts, command) => {
|
|
15982
|
+
const options = globalOptions(command);
|
|
15983
|
+
const context = await schemaContext(opts);
|
|
15984
|
+
const { table, field } = await findField(context, tableReference, reference, true);
|
|
15985
|
+
await deleteField(context.client, context.databaseId, table.id, field.id);
|
|
15986
|
+
await afterSchemaWrite(context);
|
|
15987
|
+
emit(options, `Deleted field ${field.name} (${field.id}) from ${table.name}.`, {
|
|
15988
|
+
tableId: table.id,
|
|
15989
|
+
deleted: field.id
|
|
15990
|
+
});
|
|
15991
|
+
});
|
|
15992
|
+
}
|
|
15993
|
+
function registerViews(db2) {
|
|
15994
|
+
const views = db2.command("views").description("The views of a table");
|
|
15995
|
+
withDatabaseOptions(
|
|
15996
|
+
views.command("list").description("List the views of a table").argument("<table>", TABLE_ARGUMENT)
|
|
15997
|
+
).action(async (reference, opts, command) => {
|
|
15998
|
+
const options = globalOptions(command);
|
|
15999
|
+
const context = await schemaContext(opts);
|
|
16000
|
+
const table = await findTable(context, reference, true);
|
|
16001
|
+
const items = await listViews(context.client, context.databaseId, table.id);
|
|
16002
|
+
if (context.dir) {
|
|
16003
|
+
await writeViewsCache(context.dir, table.id, items);
|
|
16004
|
+
}
|
|
16005
|
+
emit(options, renderViews(items), { tableId: table.id, views: items });
|
|
15259
16006
|
});
|
|
15260
16007
|
}
|
|
15261
16008
|
|
|
15262
|
-
// src/commands/
|
|
15263
|
-
|
|
15264
|
-
var
|
|
15265
|
-
|
|
15266
|
-
|
|
15267
|
-
|
|
15268
|
-
|
|
15269
|
-
|
|
15270
|
-
|
|
15271
|
-
|
|
16009
|
+
// src/commands/db/records.ts
|
|
16010
|
+
var SEARCH_DOCS_URL = "https://docs.softr.io/softr-api/softr-database-api/records/search-records";
|
|
16011
|
+
var MAX_PAGE = 200;
|
|
16012
|
+
function withOutputOptions(command) {
|
|
16013
|
+
return command.option("--field-ids", "key record fields by field id instead of field name");
|
|
16014
|
+
}
|
|
16015
|
+
function withPagingOptions(command) {
|
|
16016
|
+
return command.option("--limit <n>", `records per page, 1 to ${MAX_PAGE} (default 20)`).option("--offset <n>", "records to skip (default 0)").option("--all", `every record, read in pages of ${MAX_PAGE}`);
|
|
16017
|
+
}
|
|
16018
|
+
function parseCount(value, option, max) {
|
|
16019
|
+
if (value === void 0) {
|
|
16020
|
+
return void 0;
|
|
16021
|
+
}
|
|
16022
|
+
const count = Number(value);
|
|
16023
|
+
if (!Number.isInteger(count) || count < 0 || max !== void 0 && (count < 1 || count > max)) {
|
|
16024
|
+
throw new CliError(`${option} must be a whole number${max ? ` from 1 to ${max}` : " of 0 or more"}.`);
|
|
16025
|
+
}
|
|
16026
|
+
return count;
|
|
16027
|
+
}
|
|
16028
|
+
function paging(opts) {
|
|
16029
|
+
if (opts.all && opts.limit !== void 0) {
|
|
16030
|
+
throw new CliError("Pass --all or --limit, not both.");
|
|
16031
|
+
}
|
|
16032
|
+
return {
|
|
16033
|
+
offset: parseCount(opts.offset, "--offset") ?? 0,
|
|
16034
|
+
limit: opts.all ? MAX_PAGE : parseCount(opts.limit, "--limit", MAX_PAGE) ?? 20,
|
|
16035
|
+
all: opts.all === true
|
|
16036
|
+
};
|
|
16037
|
+
}
|
|
16038
|
+
async function readPages(first, read) {
|
|
16039
|
+
const page = await read(first.offset, first.limit);
|
|
16040
|
+
if (!first.all) {
|
|
16041
|
+
return page;
|
|
16042
|
+
}
|
|
16043
|
+
const records = [...page.records];
|
|
16044
|
+
let offset2 = first.offset + page.records.length;
|
|
16045
|
+
while (page.records.length && offset2 < page.metadata.total) {
|
|
16046
|
+
const next = await read(offset2, first.limit);
|
|
16047
|
+
if (!next.records.length) {
|
|
16048
|
+
break;
|
|
16049
|
+
}
|
|
16050
|
+
records.push(...next.records);
|
|
16051
|
+
offset2 += next.records.length;
|
|
16052
|
+
}
|
|
16053
|
+
return { records, metadata: { offset: first.offset, limit: records.length, total: page.metadata.total } };
|
|
16054
|
+
}
|
|
16055
|
+
function asObject(value, option) {
|
|
16056
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
16057
|
+
throw new CliError(`${option} must be a JSON object.`);
|
|
16058
|
+
}
|
|
16059
|
+
return value;
|
|
16060
|
+
}
|
|
16061
|
+
async function fieldIdsFor(context, table, references) {
|
|
16062
|
+
const found = await findFields(context, table, references);
|
|
16063
|
+
return { table: found.table, ids: new Map([...found.fields].map(([reference, field]) => [reference, field.id])) };
|
|
16064
|
+
}
|
|
16065
|
+
async function mapFieldKeys(context, table, fields) {
|
|
16066
|
+
const { ids } = await fieldIdsFor(context, table, Object.keys(fields));
|
|
16067
|
+
return Object.fromEntries(Object.entries(fields).map(([key, value]) => [ids.get(key), value]));
|
|
16068
|
+
}
|
|
16069
|
+
function fieldReferences(condition, found) {
|
|
16070
|
+
if (Array.isArray(condition)) {
|
|
16071
|
+
condition.forEach((item) => fieldReferences(item, found));
|
|
16072
|
+
} else if (condition && typeof condition === "object") {
|
|
16073
|
+
for (const [key, value] of Object.entries(condition)) {
|
|
16074
|
+
if (key === "leftSide" && typeof value === "string") {
|
|
16075
|
+
found.add(value);
|
|
16076
|
+
} else {
|
|
16077
|
+
fieldReferences(value, found);
|
|
16078
|
+
}
|
|
16079
|
+
}
|
|
16080
|
+
}
|
|
16081
|
+
}
|
|
16082
|
+
function replaceFieldReferences(condition, ids) {
|
|
16083
|
+
if (Array.isArray(condition)) {
|
|
16084
|
+
return condition.map((item) => replaceFieldReferences(item, ids));
|
|
16085
|
+
}
|
|
16086
|
+
if (condition && typeof condition === "object") {
|
|
16087
|
+
return Object.fromEntries(
|
|
16088
|
+
Object.entries(condition).map(([key, value]) => [
|
|
16089
|
+
key,
|
|
16090
|
+
key === "leftSide" && typeof value === "string" ? ids.get(value) ?? value : replaceFieldReferences(value, ids)
|
|
16091
|
+
])
|
|
16092
|
+
);
|
|
16093
|
+
}
|
|
16094
|
+
return condition;
|
|
16095
|
+
}
|
|
16096
|
+
function parseSort(spec) {
|
|
16097
|
+
const colon = spec.lastIndexOf(":");
|
|
16098
|
+
const direction = colon > 0 ? spec.slice(colon + 1).trim().toUpperCase() : "";
|
|
16099
|
+
if (direction === "ASC" || direction === "DESC") {
|
|
16100
|
+
return { field: spec.slice(0, colon).trim(), sortType: direction };
|
|
16101
|
+
}
|
|
16102
|
+
return { field: spec.trim(), sortType: "ASC" };
|
|
16103
|
+
}
|
|
16104
|
+
async function findView(context, table, reference) {
|
|
16105
|
+
const fresh = async () => {
|
|
16106
|
+
const views = await listViews(context.client, context.databaseId, table.id);
|
|
16107
|
+
if (context.dir) {
|
|
16108
|
+
await writeViewsCache(context.dir, table.id, views);
|
|
16109
|
+
}
|
|
16110
|
+
return views;
|
|
16111
|
+
};
|
|
16112
|
+
const cached = context.dir ? (await readViewsCache(context.dir, table.id))?.views : void 0;
|
|
16113
|
+
const view = (cached ? byIdOrName(cached, reference, "view") : void 0) ?? byIdOrName(await fresh(), reference, "view");
|
|
16114
|
+
if (!view) {
|
|
16115
|
+
throw new CliError(
|
|
16116
|
+
`Table ${table.name} has no view with the id or name "${reference}".`,
|
|
16117
|
+
`"softr db views list ${table.id}" shows its views.`
|
|
16118
|
+
);
|
|
16119
|
+
}
|
|
16120
|
+
return view;
|
|
16121
|
+
}
|
|
16122
|
+
function withFieldNames(record, table) {
|
|
16123
|
+
const names2 = new Map(table.fields.map((field) => [field.id, field.name]));
|
|
16124
|
+
return {
|
|
16125
|
+
...record,
|
|
16126
|
+
fields: Object.fromEntries(Object.entries(record.fields).map(([key, value]) => [names2.get(key) ?? key, value]))
|
|
16127
|
+
};
|
|
16128
|
+
}
|
|
16129
|
+
function formatValue(value) {
|
|
16130
|
+
return typeof value === "string" ? value : JSON.stringify(value);
|
|
16131
|
+
}
|
|
16132
|
+
function renderRecord(record) {
|
|
16133
|
+
return [record.id, ...Object.entries(record.fields).map(([key, value]) => ` ${key}: ${formatValue(value)}`)].join(
|
|
16134
|
+
"\n"
|
|
16135
|
+
);
|
|
16136
|
+
}
|
|
16137
|
+
function renderPage(page) {
|
|
16138
|
+
if (!page.records.length) {
|
|
16139
|
+
return page.metadata.total ? `No records from offset ${page.metadata.offset}; ${page.metadata.total} in all.` : "No records.";
|
|
16140
|
+
}
|
|
16141
|
+
const shown = page.records.length;
|
|
16142
|
+
const end = page.metadata.offset + shown;
|
|
16143
|
+
const footer = end < page.metadata.total ? `${shown} of ${page.metadata.total} records; --offset ${end} for the next ones, or --all.` : `${shown} of ${page.metadata.total} records.`;
|
|
16144
|
+
return [...page.records.map(renderRecord), footer].join("\n\n");
|
|
16145
|
+
}
|
|
16146
|
+
function registerRecords(db2) {
|
|
16147
|
+
const records = db2.command("records").description("The records of a table; fields are keyed by name unless --field-ids");
|
|
16148
|
+
withOutputOptions(
|
|
16149
|
+
withPagingOptions(
|
|
16150
|
+
withDatabaseOptions(
|
|
16151
|
+
records.command("list").description(
|
|
16152
|
+
`List a table's records in the order and with the filter of a view, the table's default view unless --view; "records search" without --filter reads every record`
|
|
16153
|
+
).argument("<table>", TABLE_ARGUMENT).option("--view <view>", "the view id or exact name (default: the table's default view)")
|
|
16154
|
+
)
|
|
16155
|
+
)
|
|
16156
|
+
).action(
|
|
16157
|
+
async (reference, opts, command) => {
|
|
16158
|
+
const options = globalOptions(command);
|
|
16159
|
+
const pages = paging(opts);
|
|
16160
|
+
const context = await schemaContext(opts);
|
|
16161
|
+
const table = await findTable(context, reference);
|
|
16162
|
+
const viewId = opts.view ? (await findView(context, table, opts.view)).id : void 0;
|
|
16163
|
+
const page = await readPages(
|
|
16164
|
+
pages,
|
|
16165
|
+
(offset2, limit) => listRecords(context.client, context.databaseId, table.id, {
|
|
16166
|
+
viewId,
|
|
16167
|
+
offset: offset2,
|
|
16168
|
+
limit,
|
|
16169
|
+
fieldNames: !opts.fieldIds
|
|
16170
|
+
})
|
|
16171
|
+
);
|
|
16172
|
+
emit(options, renderPage(page), { tableId: table.id, ...page });
|
|
16173
|
+
}
|
|
16174
|
+
);
|
|
16175
|
+
withOutputOptions(
|
|
16176
|
+
withPagingOptions(
|
|
16177
|
+
withDatabaseOptions(
|
|
16178
|
+
records.command("search").description("Find records by a filter, sorted by fields").argument("<table>", TABLE_ARGUMENT).option(
|
|
16179
|
+
"--filter <json>",
|
|
16180
|
+
`the condition as JSON, @<file> or - for stdin; leftSide takes a field name or id (operators: ${SEARCH_DOCS_URL})`
|
|
16181
|
+
).option(
|
|
16182
|
+
"--sort <field[:asc|desc]>",
|
|
16183
|
+
"sort by a field, repeatable",
|
|
16184
|
+
(value, previous) => [...previous, value],
|
|
16185
|
+
[]
|
|
16186
|
+
)
|
|
16187
|
+
)
|
|
16188
|
+
)
|
|
16189
|
+
).action(
|
|
16190
|
+
async (reference, opts, command) => {
|
|
16191
|
+
const options = globalOptions(command);
|
|
16192
|
+
const pages = paging(opts);
|
|
16193
|
+
const parsed = opts.filter === void 0 ? void 0 : asObject(await readJsonArgument(opts.filter, "--filter"), "--filter");
|
|
16194
|
+
const condition = parsed && "condition" in parsed ? parsed["condition"] : parsed;
|
|
16195
|
+
const sorts = opts.sort.map(parseSort);
|
|
16196
|
+
const context = await schemaContext(opts);
|
|
16197
|
+
const references = new Set(sorts.map((sort) => sort.field));
|
|
16198
|
+
fieldReferences(condition, references);
|
|
16199
|
+
const { table, ids } = await fieldIdsFor(context, await findTable(context, reference), [...references]);
|
|
16200
|
+
const search = (offset2, limit) => ({
|
|
16201
|
+
paging: { offset: offset2, limit },
|
|
16202
|
+
...sorts.length ? { sorting: sorts.map((sort) => ({ sortingField: ids.get(sort.field), sortType: sort.sortType })) } : {},
|
|
16203
|
+
...condition === void 0 ? {} : { filter: { condition: replaceFieldReferences(condition, ids) } }
|
|
16204
|
+
});
|
|
16205
|
+
const page = await readPages(
|
|
16206
|
+
pages,
|
|
16207
|
+
(offset2, limit) => searchRecords(context.client, context.databaseId, table.id, search(offset2, limit), !opts.fieldIds)
|
|
16208
|
+
);
|
|
16209
|
+
emit(options, renderPage(page), { tableId: table.id, ...page });
|
|
16210
|
+
}
|
|
16211
|
+
);
|
|
16212
|
+
withOutputOptions(
|
|
16213
|
+
withDatabaseOptions(
|
|
16214
|
+
records.command("get").description("Show one record").argument("<table>", TABLE_ARGUMENT).argument("<recordId>", "the record id")
|
|
16215
|
+
)
|
|
16216
|
+
).action(async (reference, recordId, opts, command) => {
|
|
16217
|
+
const options = globalOptions(command);
|
|
16218
|
+
const context = await schemaContext(opts);
|
|
16219
|
+
const table = await findTable(context, reference);
|
|
16220
|
+
const record = await getRecord(context.client, context.databaseId, table.id, recordId, !opts.fieldIds);
|
|
16221
|
+
emit(options, renderRecord(record), { tableId: table.id, record });
|
|
16222
|
+
});
|
|
16223
|
+
withOutputOptions(
|
|
16224
|
+
withDatabaseOptions(
|
|
16225
|
+
records.command("create").description("Create a record").argument("<table>", TABLE_ARGUMENT).requiredOption(
|
|
16226
|
+
"--fields <json>",
|
|
16227
|
+
"the values as a JSON object keyed by field name or id, @<file> or - for stdin; {} creates an empty record"
|
|
16228
|
+
)
|
|
16229
|
+
)
|
|
16230
|
+
).action(async (reference, opts, command) => {
|
|
16231
|
+
const options = globalOptions(command);
|
|
16232
|
+
const fields = asObject(await readJsonArgument(opts.fields, "--fields"), "--fields");
|
|
16233
|
+
const context = await schemaContext(opts);
|
|
16234
|
+
const table = await findTable(context, reference);
|
|
16235
|
+
const record = await createRecord(
|
|
16236
|
+
context.client,
|
|
16237
|
+
context.databaseId,
|
|
16238
|
+
table.id,
|
|
16239
|
+
await mapFieldKeys(context, table, fields),
|
|
16240
|
+
!opts.fieldIds
|
|
16241
|
+
);
|
|
16242
|
+
emit(options, `Created record ${record.id} in ${table.name}.`, { tableId: table.id, record });
|
|
16243
|
+
});
|
|
16244
|
+
withOutputOptions(
|
|
16245
|
+
withDatabaseOptions(
|
|
16246
|
+
records.command("update").description("Change some fields of a record; the others keep their values").argument("<table>", TABLE_ARGUMENT).argument("<recordId>", "the record id").requiredOption(
|
|
16247
|
+
"--fields <json>",
|
|
16248
|
+
"the new values as a JSON object keyed by field name or id, @<file> or - for stdin; null clears a field"
|
|
16249
|
+
)
|
|
16250
|
+
)
|
|
16251
|
+
).action(
|
|
16252
|
+
async (reference, recordId, opts, command) => {
|
|
16253
|
+
const options = globalOptions(command);
|
|
16254
|
+
const fields = asObject(await readJsonArgument(opts.fields, "--fields"), "--fields");
|
|
16255
|
+
if (!Object.keys(fields).length) {
|
|
16256
|
+
throw new CliError("Nothing to change: --fields is an empty object.");
|
|
16257
|
+
}
|
|
16258
|
+
const context = await schemaContext(opts);
|
|
16259
|
+
const table = await findTable(context, reference);
|
|
16260
|
+
const record = await updateRecord(
|
|
16261
|
+
context.client,
|
|
16262
|
+
context.databaseId,
|
|
16263
|
+
table.id,
|
|
16264
|
+
recordId,
|
|
16265
|
+
await mapFieldKeys(context, table, fields),
|
|
16266
|
+
!opts.fieldIds
|
|
15272
16267
|
);
|
|
16268
|
+
emit(options, `Updated record ${record.id} in ${table.name}.`, { tableId: table.id, record });
|
|
15273
16269
|
}
|
|
15274
|
-
return requested;
|
|
15275
|
-
}
|
|
15276
|
-
if (workspaces.length === 1 && workspaces[0]) {
|
|
15277
|
-
return workspaces[0].id;
|
|
15278
|
-
}
|
|
15279
|
-
const preferred = workspaces.find((workspace) => workspace.isDefault);
|
|
15280
|
-
if (preferred) {
|
|
15281
|
-
return preferred.id;
|
|
15282
|
-
}
|
|
15283
|
-
throw new CliError(
|
|
15284
|
-
"Pass --workspace <id>: you have access to several workspaces.",
|
|
15285
|
-
`Choose one of: ${workspaces.map((w) => `${w.name} (${w.id})`).join(", ")}`
|
|
15286
16270
|
);
|
|
15287
|
-
|
|
15288
|
-
|
|
15289
|
-
|
|
15290
|
-
|
|
15291
|
-
|
|
15292
|
-
|
|
15293
|
-
|
|
16271
|
+
withDatabaseOptions(
|
|
16272
|
+
records.command("delete").description("Delete a record").argument("<table>", TABLE_ARGUMENT).argument("<recordId>", "the record id")
|
|
16273
|
+
).action(async (reference, recordId, opts, command) => {
|
|
16274
|
+
const options = globalOptions(command);
|
|
16275
|
+
const context = await schemaContext(opts);
|
|
16276
|
+
const table = await findTable(context, reference, true);
|
|
16277
|
+
await deleteRecord(context.client, context.databaseId, table.id, recordId);
|
|
16278
|
+
emit(options, `Deleted record ${recordId} from ${table.name}.`, { tableId: table.id, deleted: recordId });
|
|
16279
|
+
});
|
|
16280
|
+
withOutputOptions(
|
|
16281
|
+
withDatabaseOptions(
|
|
16282
|
+
records.command("upload").description("Add a local file to a record's attachment field").argument("<table>", TABLE_ARGUMENT).argument("<recordId>", "the record id").argument("<field>", "the attachment field, by id or exact name").argument("<file>", "the file to upload").option("--name <fileName>", "the file name to store (default: the local file name)")
|
|
16283
|
+
)
|
|
16284
|
+
).action(
|
|
16285
|
+
async (reference, recordId, fieldReference, file, opts, command) => {
|
|
15294
16286
|
const options = globalOptions(command);
|
|
15295
|
-
|
|
15296
|
-
|
|
15297
|
-
|
|
15298
|
-
|
|
15299
|
-
|
|
15300
|
-
|
|
15301
|
-
const
|
|
15302
|
-
const
|
|
15303
|
-
const
|
|
15304
|
-
const
|
|
15305
|
-
|
|
15306
|
-
|
|
15307
|
-
|
|
15308
|
-
|
|
15309
|
-
|
|
15310
|
-
|
|
15311
|
-
triggers: [{ id: TRIGGER_ID, type: trigger.type, version: trigger.version, inputs: trigger.template }],
|
|
15312
|
-
actions: [],
|
|
15313
|
-
paths: [],
|
|
15314
|
-
// Like the studio: the author's time zone, so schedules and waits mean what they read.
|
|
15315
|
-
configuration: { timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC" }
|
|
16287
|
+
let content;
|
|
16288
|
+
try {
|
|
16289
|
+
content = await readFile9(file);
|
|
16290
|
+
} catch {
|
|
16291
|
+
throw new CliError(`Cannot read ${file}.`);
|
|
16292
|
+
}
|
|
16293
|
+
const context = await schemaContext(opts);
|
|
16294
|
+
const found = await fieldIdsFor(context, await findTable(context, reference), [fieldReference]);
|
|
16295
|
+
const fieldId = found.ids.get(fieldReference);
|
|
16296
|
+
const fieldType = found.table.fields.find((field) => field.id === fieldId)?.type;
|
|
16297
|
+
if (fieldType !== "ATTACHMENT") {
|
|
16298
|
+
throw new CliError(`Field "${fieldReference}" has the type ${fieldType}; upload needs an ATTACHMENT field.`);
|
|
16299
|
+
}
|
|
16300
|
+
const uploaded = await uploadAttachment(context.client, context.databaseId, found.table.id, recordId, fieldId, {
|
|
16301
|
+
name: opts.name ?? path12.basename(file),
|
|
16302
|
+
content
|
|
15316
16303
|
});
|
|
15317
|
-
const
|
|
15318
|
-
|
|
15319
|
-
|
|
15320
|
-
|
|
15321
|
-
const syncedHash = definitionHash((await packWorkflow(config, dir)).packed);
|
|
15322
|
-
await writeState(dir, {
|
|
15323
|
-
...target,
|
|
15324
|
-
workflowId: created.id,
|
|
15325
|
-
version: workflow.version,
|
|
15326
|
-
draft: workflow.draft,
|
|
15327
|
-
syncedHash
|
|
16304
|
+
const record = opts.fieldIds ? uploaded : withFieldNames(uploaded, found.table);
|
|
16305
|
+
emit(options, `Uploaded ${opts.name ?? path12.basename(file)} to record ${record.id} in ${found.table.name}.`, {
|
|
16306
|
+
tableId: found.table.id,
|
|
16307
|
+
record
|
|
15328
16308
|
});
|
|
15329
|
-
const studioUrl = `${target.studioUrl}/workflow/${created.id}`;
|
|
15330
|
-
emit(
|
|
15331
|
-
options,
|
|
15332
|
-
`Created "${title}" (${created.id}) with a ${trigger.type} trigger "${TRIGGER_ID}" in workspace ${workspaceId}${pinToApplicationId ? `, linked to application ${pinToApplicationId}` : ""}.
|
|
15333
|
-
${path10.relative(cwd, configFile)}
|
|
15334
|
-
${studioUrl}
|
|
15335
|
-
Next: softr workflows add <TYPE> --dir ${path10.relative(cwd, dir) || "."} (see "spec" for the types), then push and test.`,
|
|
15336
|
-
{
|
|
15337
|
-
workflowId: created.id,
|
|
15338
|
-
workspaceId,
|
|
15339
|
-
pinnedToApplicationId: pinToApplicationId ?? null,
|
|
15340
|
-
title,
|
|
15341
|
-
triggerId: TRIGGER_ID,
|
|
15342
|
-
triggerType: trigger.type,
|
|
15343
|
-
dir,
|
|
15344
|
-
studioUrl
|
|
15345
|
-
}
|
|
15346
|
-
);
|
|
15347
16309
|
}
|
|
15348
16310
|
);
|
|
15349
16311
|
}
|
|
15350
16312
|
|
|
15351
16313
|
// src/commands/integrations.ts
|
|
15352
|
-
var
|
|
15353
|
-
import
|
|
15354
|
-
function
|
|
16314
|
+
var import_cli_table33 = __toESM(require_cli_table3(), 1);
|
|
16315
|
+
import path13 from "node:path";
|
|
16316
|
+
function toRow(integration) {
|
|
15355
16317
|
return {
|
|
15356
16318
|
id: integration.id,
|
|
15357
16319
|
name: integration.name,
|
|
@@ -15368,7 +16330,7 @@ function renderIntegrations(rows) {
|
|
|
15368
16330
|
if (!rows.length) {
|
|
15369
16331
|
return "No integrations connected in this workspace. Connect one in the studio (Workspace settings > Integrations).";
|
|
15370
16332
|
}
|
|
15371
|
-
const table = new
|
|
16333
|
+
const table = new import_cli_table33.default({
|
|
15372
16334
|
head: ["ID", "Name", "Type", "Account", "Auth"],
|
|
15373
16335
|
colWidths: [38, 34, 22, 30, 11],
|
|
15374
16336
|
wordWrap: true,
|
|
@@ -15382,7 +16344,7 @@ Use the ID as the integrationId input of a node (see "spec <TYPE>").`;
|
|
|
15382
16344
|
}
|
|
15383
16345
|
async function workflowDirOf(cwd, dir) {
|
|
15384
16346
|
if (dir) {
|
|
15385
|
-
return
|
|
16347
|
+
return path13.resolve(cwd, dir);
|
|
15386
16348
|
}
|
|
15387
16349
|
return await configExists(cwd) ? cwd : void 0;
|
|
15388
16350
|
}
|
|
@@ -15399,7 +16361,7 @@ async function workspaceFor(client, requested, workflowDir) {
|
|
|
15399
16361
|
return resolveWorkspace(client, void 0);
|
|
15400
16362
|
}
|
|
15401
16363
|
function registerIntegrations(program3) {
|
|
15402
|
-
program3.command("integrations").description("List the workspace's connected integrations
|
|
16364
|
+
program3.command("integrations").description("List the workspace's connected integrations").option(
|
|
15403
16365
|
"--workspace <id>",
|
|
15404
16366
|
"workspace to list (default: $SOFTR_WORKSPACE_ID, else the workflow directory's, else your only or default workspace)"
|
|
15405
16367
|
).option("--type <TYPE>", "only this integration type, e.g. SLACK or SOFTR_TABLES").option("--dir <path>", "workflow directory whose workspace to use (default: the current directory)").action(async (opts, command) => {
|
|
@@ -15409,7 +16371,7 @@ function registerIntegrations(program3) {
|
|
|
15409
16371
|
const client = await authenticatedClient(target);
|
|
15410
16372
|
const workspaceId = await workspaceFor(client, opts.workspace ?? process.env["SOFTR_WORKSPACE_ID"], workflowDir);
|
|
15411
16373
|
const wanted = opts.type?.toUpperCase();
|
|
15412
|
-
const rows = (await listWorkspaceIntegrations(client, workspaceId)).map(
|
|
16374
|
+
const rows = (await listWorkspaceIntegrations(client, workspaceId)).map(toRow).filter((row) => !wanted || row.type === wanted).sort(byTypeThenName);
|
|
15413
16375
|
emit(options, `Workspace ${workspaceId}${wanted ? `, type ${wanted}` : ""}:
|
|
15414
16376
|
${renderIntegrations(rows)}`, {
|
|
15415
16377
|
workspaceId,
|
|
@@ -15419,6 +16381,105 @@ ${renderIntegrations(rows)}`, {
|
|
|
15419
16381
|
});
|
|
15420
16382
|
}
|
|
15421
16383
|
|
|
16384
|
+
// src/commands/list.ts
|
|
16385
|
+
var import_cli_table34 = __toESM(require_cli_table3(), 1);
|
|
16386
|
+
function toRow2(workflow, workspace) {
|
|
16387
|
+
return {
|
|
16388
|
+
id: workflow.id,
|
|
16389
|
+
title: workflow.title,
|
|
16390
|
+
workspaceId: workspace.id,
|
|
16391
|
+
workspaceName: workspace.name,
|
|
16392
|
+
live: workflow.enabledVersion ?? null,
|
|
16393
|
+
version: workflow.version,
|
|
16394
|
+
draft: workflow.draft,
|
|
16395
|
+
updatedAt: workflow.updatedAt ?? null,
|
|
16396
|
+
triggers: (workflow.triggerSummaries ?? []).map(({ id, type }) => ({ id, type }))
|
|
16397
|
+
};
|
|
16398
|
+
}
|
|
16399
|
+
async function collectWorkflows(client, { workspaceId, applicationId } = {}) {
|
|
16400
|
+
const workspaces = await listWorkspacesWith(client, WORKFLOWS_READ_SCOPE);
|
|
16401
|
+
if (workspaceId) {
|
|
16402
|
+
requireGrantedWorkspace(workspaces, workspaceId, WORKFLOWS_READ_SCOPE);
|
|
16403
|
+
}
|
|
16404
|
+
const selected = workspaceId ? workspaces.granted.filter((workspace) => workspace.id === workspaceId) : workspaces.granted;
|
|
16405
|
+
const listing = { workspaces: [], workflows: [], failures: [] };
|
|
16406
|
+
for (const workspace of selected) {
|
|
16407
|
+
try {
|
|
16408
|
+
const page = await listWorkflows(client, workspace.id, { applicationId });
|
|
16409
|
+
listing.workspaces.push({ id: workspace.id, name: workspace.name });
|
|
16410
|
+
listing.workflows.push(...page.items.map((workflow) => toRow2(workflow, workspace)));
|
|
16411
|
+
} catch (error) {
|
|
16412
|
+
if (!(error instanceof SoftrApiError) || selected.length === 1) {
|
|
16413
|
+
throw error;
|
|
16414
|
+
}
|
|
16415
|
+
listing.failures.push({
|
|
16416
|
+
workspaceId: workspace.id,
|
|
16417
|
+
workspaceName: workspace.name,
|
|
16418
|
+
status: error.status,
|
|
16419
|
+
message: error.body
|
|
16420
|
+
});
|
|
16421
|
+
}
|
|
16422
|
+
}
|
|
16423
|
+
return listing;
|
|
16424
|
+
}
|
|
16425
|
+
var TITLE_WIDTH = 34;
|
|
16426
|
+
function workflowTable(rows) {
|
|
16427
|
+
const table = new import_cli_table34.default({
|
|
16428
|
+
head: ["ID", "Title", "Triggers", "Status", "Version", "Draft"],
|
|
16429
|
+
// Only the free-text title wraps; the other columns size to their content, so trigger types are never cut off.
|
|
16430
|
+
colWidths: [null, TITLE_WIDTH, null, null, null, null],
|
|
16431
|
+
wordWrap: true,
|
|
16432
|
+
style: { head: [], border: [], "padding-left": 1, "padding-right": 1 }
|
|
16433
|
+
});
|
|
16434
|
+
for (const row of rows) {
|
|
16435
|
+
table.push([
|
|
16436
|
+
row.id,
|
|
16437
|
+
row.title,
|
|
16438
|
+
row.triggers.map((trigger) => trigger.type).join("\n") || "-",
|
|
16439
|
+
row.live === null ? "not enabled" : `live v${row.live}`,
|
|
16440
|
+
row.version,
|
|
16441
|
+
row.draft
|
|
16442
|
+
]);
|
|
16443
|
+
}
|
|
16444
|
+
return table.toString();
|
|
16445
|
+
}
|
|
16446
|
+
function renderWorkflows({ workspaces, workflows: workflows2, failures }) {
|
|
16447
|
+
const blocks = [];
|
|
16448
|
+
for (const workspace of workspaces) {
|
|
16449
|
+
const rows = workflows2.filter((row) => row.workspaceId === workspace.id);
|
|
16450
|
+
const heading = `Workspace: ${workspace.name} (${workspace.id})`;
|
|
16451
|
+
blocks.push(rows.length ? `${heading}
|
|
16452
|
+
${workflowTable(rows)}` : `${heading}
|
|
16453
|
+
(no workflows)`);
|
|
16454
|
+
}
|
|
16455
|
+
if (failures.length) {
|
|
16456
|
+
blocks.push(
|
|
16457
|
+
[
|
|
16458
|
+
`Listing failed for ${failures.length} workspace${failures.length === 1 ? "" : "s"} (skipped):`,
|
|
16459
|
+
...failures.map(
|
|
16460
|
+
(failure) => ` ${failure.workspaceName} (${failure.workspaceId}): status ${failure.status}${failure.message ? `: ${failure.message}` : ""}`
|
|
16461
|
+
)
|
|
16462
|
+
].join("\n")
|
|
16463
|
+
);
|
|
16464
|
+
}
|
|
16465
|
+
return blocks.join("\n\n");
|
|
16466
|
+
}
|
|
16467
|
+
async function emitWorkflowList(options, client, { workspaceId, applicationId, human } = {}) {
|
|
16468
|
+
const listing = await collectWorkflows(client, { workspaceId, applicationId });
|
|
16469
|
+
emit(options, [human, renderWorkflows(listing)].filter(Boolean).join("\n\n"), { ...listing });
|
|
16470
|
+
}
|
|
16471
|
+
function registerList(program3) {
|
|
16472
|
+
program3.command("list").description("List the workflows you can access, grouped by workspace").option("--workspace <id>", "only this workspace (default: $SOFTR_WORKSPACE_ID when set)").option("--app <id>", "only the workflows linked to this application").action(async (opts, command) => {
|
|
16473
|
+
const options = globalOptions(command);
|
|
16474
|
+
const target = resolveTarget(await readState(process.cwd()));
|
|
16475
|
+
const client = await authenticatedClient(target);
|
|
16476
|
+
await emitWorkflowList(options, client, {
|
|
16477
|
+
workspaceId: opts.workspace ?? process.env["SOFTR_WORKSPACE_ID"],
|
|
16478
|
+
applicationId: opts.app
|
|
16479
|
+
});
|
|
16480
|
+
});
|
|
16481
|
+
}
|
|
16482
|
+
|
|
15422
16483
|
// src/commands/login.ts
|
|
15423
16484
|
import { execFile } from "node:child_process";
|
|
15424
16485
|
import { createServer } from "node:http";
|
|
@@ -15431,10 +16492,10 @@ async function identify(client) {
|
|
|
15431
16492
|
return { userId: owner.id, email: owner.email, statements: owner.statements };
|
|
15432
16493
|
}
|
|
15433
16494
|
function scopeWarning(identity) {
|
|
15434
|
-
if (grantsWorkflows(identity.statements)) {
|
|
16495
|
+
if (grantsWorkflows(identity.statements) || grantsDatabases(identity.statements)) {
|
|
15435
16496
|
return "";
|
|
15436
16497
|
}
|
|
15437
|
-
return "\nWarning: this token grants no workflows action (workflows:read, workflows:write, workflows:execute); most commands will be refused.";
|
|
16498
|
+
return "\nWarning: this token grants no workflows action (workflows:read, workflows:write, workflows:execute) and no databases action (databases.records:read, databases.records:write, databases.schema:write); most commands will be refused.";
|
|
15438
16499
|
}
|
|
15439
16500
|
async function interactiveLogin(target) {
|
|
15440
16501
|
if (!stdin.isTTY) {
|
|
@@ -15684,7 +16745,7 @@ ${identity.statements.map((s) => ` ${s.actions.join(", ")} on ${s.resources.joi
|
|
|
15684
16745
|
}
|
|
15685
16746
|
|
|
15686
16747
|
// src/commands/outputs.ts
|
|
15687
|
-
var
|
|
16748
|
+
var import_cli_table36 = __toESM(require_cli_table3(), 1);
|
|
15688
16749
|
|
|
15689
16750
|
// src/project/test-state.ts
|
|
15690
16751
|
function outdatedReferences(row) {
|
|
@@ -15697,28 +16758,28 @@ function renderIssues(issues) {
|
|
|
15697
16758
|
|
|
15698
16759
|
// src/project/variables.ts
|
|
15699
16760
|
var MAX_VARIABLES = 200;
|
|
15700
|
-
function appendKey(
|
|
15701
|
-
return /^[A-Za-z_]\w*$/.test(key) ? `${
|
|
16761
|
+
function appendKey(path16, key) {
|
|
16762
|
+
return /^[A-Za-z_]\w*$/.test(key) ? `${path16}.${key}` : `${path16}['${key.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}']`;
|
|
15702
16763
|
}
|
|
15703
16764
|
function isPrimitive2(value) {
|
|
15704
16765
|
return value !== void 0 && value !== null && typeof value !== "object";
|
|
15705
16766
|
}
|
|
15706
|
-
function walk(type, raw,
|
|
16767
|
+
function walk(type, raw, path16, token, out) {
|
|
15707
16768
|
if (out.length >= MAX_VARIABLES) {
|
|
15708
16769
|
return;
|
|
15709
16770
|
}
|
|
15710
|
-
out.push({ variable: token(
|
|
16771
|
+
out.push({ variable: token(path16), path: path16, type, ...isPrimitive2(raw) ? { example: raw } : {} });
|
|
15711
16772
|
if (type.kind === "object") {
|
|
15712
16773
|
for (const [key, field] of Object.entries(type.fields)) {
|
|
15713
16774
|
const rawField = raw !== null && typeof raw === "object" && !Array.isArray(raw) ? raw[key] : void 0;
|
|
15714
|
-
walk(field.type, rawField, appendKey(
|
|
16775
|
+
walk(field.type, rawField, appendKey(path16, key), token, out);
|
|
15715
16776
|
}
|
|
15716
16777
|
} else if (type.kind === "array") {
|
|
15717
|
-
walk(type.items, Array.isArray(raw) ? raw[0] : void 0, `${
|
|
16778
|
+
walk(type.items, Array.isArray(raw) ? raw[0] : void 0, `${path16}[*]`, token, out);
|
|
15718
16779
|
}
|
|
15719
16780
|
}
|
|
15720
16781
|
function collectVariables(nodeId, sample, metadata) {
|
|
15721
|
-
const token = (
|
|
16782
|
+
const token = (path16) => `{outputs.${nodeId}:::${path16}}`;
|
|
15722
16783
|
const variables = [];
|
|
15723
16784
|
if (sample !== void 0 && sample !== null) {
|
|
15724
16785
|
walk(typeOfJson(sample), sample, "$", token, variables);
|
|
@@ -15733,7 +16794,7 @@ function collectVariables(nodeId, sample, metadata) {
|
|
|
15733
16794
|
}
|
|
15734
16795
|
|
|
15735
16796
|
// src/commands/test.ts
|
|
15736
|
-
var
|
|
16797
|
+
var import_cli_table35 = __toESM(require_cli_table3(), 1);
|
|
15737
16798
|
var OUTPUTS_FILE = "outputs.json";
|
|
15738
16799
|
function fieldCount(row) {
|
|
15739
16800
|
const output = row.sampleOutput?.result?.output;
|
|
@@ -15834,7 +16895,7 @@ var LABEL = {
|
|
|
15834
16895
|
error: "ERROR"
|
|
15835
16896
|
};
|
|
15836
16897
|
function renderResults(results) {
|
|
15837
|
-
const table = new
|
|
16898
|
+
const table = new import_cli_table35.default({
|
|
15838
16899
|
head: ["Result", "Node", "Type", "Detail"],
|
|
15839
16900
|
colWidths: [9, 30, 24, 60],
|
|
15840
16901
|
wordWrap: true,
|
|
@@ -15950,7 +17011,7 @@ function sampleState(row) {
|
|
|
15950
17011
|
}
|
|
15951
17012
|
}
|
|
15952
17013
|
function renderVariables(nodeId, entries, unverified) {
|
|
15953
|
-
const table = new
|
|
17014
|
+
const table = new import_cli_table36.default({
|
|
15954
17015
|
head: ["Variable", "Type", "Example"],
|
|
15955
17016
|
colWidths: [64, 32, 44],
|
|
15956
17017
|
wordWrap: true,
|
|
@@ -16018,7 +17079,7 @@ function registerOutputs(program3) {
|
|
|
16018
17079
|
outdated: Object.keys(outdatedReferences(row))
|
|
16019
17080
|
};
|
|
16020
17081
|
});
|
|
16021
|
-
const table = new
|
|
17082
|
+
const table = new import_cli_table36.default({
|
|
16022
17083
|
head: ["Node", "Type", "Sample", "Fields"],
|
|
16023
17084
|
colWidths: [30, 24, 60, 8],
|
|
16024
17085
|
wordWrap: true,
|
|
@@ -16050,19 +17111,19 @@ function pathFromKeys(keys) {
|
|
|
16050
17111
|
const only = keys[0].trim();
|
|
16051
17112
|
return only.startsWith("$") ? only : `$${only.startsWith("[") ? "" : "."}${only}`;
|
|
16052
17113
|
}
|
|
16053
|
-
let
|
|
17114
|
+
let path16 = "$";
|
|
16054
17115
|
for (const key of keys) {
|
|
16055
17116
|
if (key === "*") {
|
|
16056
|
-
|
|
17117
|
+
path16 += "[*]";
|
|
16057
17118
|
} else if (/^\d+$/.test(key)) {
|
|
16058
|
-
|
|
17119
|
+
path16 += `[${key}]`;
|
|
16059
17120
|
} else if (/^[A-Za-z_]\w*$/.test(key)) {
|
|
16060
|
-
|
|
17121
|
+
path16 += `.${key}`;
|
|
16061
17122
|
} else {
|
|
16062
|
-
|
|
17123
|
+
path16 += `['${key.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}']`;
|
|
16063
17124
|
}
|
|
16064
17125
|
}
|
|
16065
|
-
return
|
|
17126
|
+
return path16;
|
|
16066
17127
|
}
|
|
16067
17128
|
function keysAt(type) {
|
|
16068
17129
|
switch (type.kind) {
|
|
@@ -16087,12 +17148,12 @@ function registerPlaceholder(program3) {
|
|
|
16087
17148
|
}
|
|
16088
17149
|
const rows = await readStateFile(dir, OUTPUTS_FILE) ?? [];
|
|
16089
17150
|
const outputs = indexOutputs(rows);
|
|
16090
|
-
const
|
|
16091
|
-
if (!parsePath(
|
|
16092
|
-
throw new CliError(`Cannot read the path "${
|
|
17151
|
+
const path16 = pathFromKeys(keys);
|
|
17152
|
+
if (!parsePath(path16)) {
|
|
17153
|
+
throw new CliError(`Cannot read the path "${path16}".`);
|
|
16093
17154
|
}
|
|
16094
17155
|
const isLoop = location.node.type === LOOP_TYPE;
|
|
16095
|
-
const token = isLoop ? `{loopActionGroup.${nodeId}:::loopVariables.items${
|
|
17156
|
+
const token = isLoop ? `{loopActionGroup.${nodeId}:::loopVariables.items${path16 === "$" ? "" : path16.slice(1)}}` : `{outputs.${nodeId}:::${path16}}`;
|
|
16096
17157
|
const [description] = describeInputData(
|
|
16097
17158
|
{ id: "probe", type: "CUSTOM_CODE", inputs: { inputData: { probe: token } } },
|
|
16098
17159
|
config,
|
|
@@ -16102,7 +17163,7 @@ function registerPlaceholder(program3) {
|
|
|
16102
17163
|
throw new CliError("Could not evaluate the token.");
|
|
16103
17164
|
}
|
|
16104
17165
|
if (description.type.kind === "unknown") {
|
|
16105
|
-
const segments = parsePath(
|
|
17166
|
+
const segments = parsePath(path16) ?? [];
|
|
16106
17167
|
let hint = description.note ?? "not found";
|
|
16107
17168
|
const row = outputs[nodeId];
|
|
16108
17169
|
if (row?.hasSample && segments.length) {
|
|
@@ -16128,7 +17189,7 @@ function registerPlaceholder(program3) {
|
|
|
16128
17189
|
emit(options, `${token}
|
|
16129
17190
|
type: ${type}${example3} (${status})`, {
|
|
16130
17191
|
token,
|
|
16131
|
-
path:
|
|
17192
|
+
path: path16,
|
|
16132
17193
|
type,
|
|
16133
17194
|
example: description.example ?? null,
|
|
16134
17195
|
verified: description.verified,
|
|
@@ -16137,6 +17198,84 @@ type: ${type}${example3} (${status})`, {
|
|
|
16137
17198
|
});
|
|
16138
17199
|
}
|
|
16139
17200
|
|
|
17201
|
+
// src/commands/pull.ts
|
|
17202
|
+
import path14 from "node:path";
|
|
17203
|
+
async function targetDir(cwd, explicitDir, workflowId, title) {
|
|
17204
|
+
if (explicitDir) {
|
|
17205
|
+
return path14.resolve(cwd, explicitDir);
|
|
17206
|
+
}
|
|
17207
|
+
if (await configExists(cwd)) {
|
|
17208
|
+
const existing = await readConfig(cwd);
|
|
17209
|
+
if (existing.id && existing.id !== workflowId) {
|
|
17210
|
+
throw new CliError(
|
|
17211
|
+
`${CONFIG_FILE_NAME} here belongs to workflow ${existing.id}, not ${workflowId}.`,
|
|
17212
|
+
"Run pull from the parent directory (a new subdirectory is created) or pass --dir."
|
|
17213
|
+
);
|
|
17214
|
+
}
|
|
17215
|
+
return cwd;
|
|
17216
|
+
}
|
|
17217
|
+
return path14.join(cwd, slug(title));
|
|
17218
|
+
}
|
|
17219
|
+
function registerPull(program3) {
|
|
17220
|
+
program3.command("pull").description("Download a workflow into softr-workflows.jsonc and one code file per custom code node").argument("[workflowId]", "the workflow id (last segment of its studio URL); omit to list what you can pull").option("--dir <path>", "target directory (default: this directory if it holds the workflow, else ./<title-slug>)").option("--force", "overwrite an existing softr-workflows.jsonc and its code files").action(async (workflowId, opts, command) => {
|
|
17221
|
+
const options = globalOptions(command);
|
|
17222
|
+
const cwd = process.cwd();
|
|
17223
|
+
const remembered = await readState(opts.dir ? path14.resolve(cwd, opts.dir) : cwd);
|
|
17224
|
+
const target = resolveTarget(remembered);
|
|
17225
|
+
const client = await authenticatedClient(target);
|
|
17226
|
+
if (!workflowId) {
|
|
17227
|
+
await emitWorkflowList(options, client, {
|
|
17228
|
+
human: "Pick a workflow and run: softr workflows pull <workflowId>"
|
|
17229
|
+
});
|
|
17230
|
+
return;
|
|
17231
|
+
}
|
|
17232
|
+
const workflow = await getWorkflow(client, workflowId);
|
|
17233
|
+
const dir = await targetDir(cwd, opts.dir, workflowId, workflow.title);
|
|
17234
|
+
if (await configExists(dir) && !opts.force) {
|
|
17235
|
+
throw new CliError(
|
|
17236
|
+
`${path14.join(dir, CONFIG_FILE_NAME)} already exists.`,
|
|
17237
|
+
"Pass --force to overwrite it and its code files."
|
|
17238
|
+
);
|
|
17239
|
+
}
|
|
17240
|
+
let rows = [];
|
|
17241
|
+
let outputsNote = "";
|
|
17242
|
+
let outputsFile;
|
|
17243
|
+
try {
|
|
17244
|
+
rows = await getWorkflowOutputs(client, workflowId);
|
|
17245
|
+
} catch (error) {
|
|
17246
|
+
outputsNote = `
|
|
17247
|
+
Sample outputs not available, inputData types are unknown: ${error.message}`;
|
|
17248
|
+
}
|
|
17249
|
+
const config = { $schema: SCHEMA_POINTER, ...toLocalConfig(workflow) };
|
|
17250
|
+
const codeFiles = await externalizeCode(config, dir, indexOutputs(rows));
|
|
17251
|
+
await writeSchemaFile(dir);
|
|
17252
|
+
const configFile2 = await writeConfig(dir, config);
|
|
17253
|
+
const syncedHash = definitionHash((await packWorkflow(config, dir)).packed);
|
|
17254
|
+
await writeState(dir, { ...target, workflowId, version: workflow.version, draft: workflow.draft, syncedHash });
|
|
17255
|
+
if (!outputsNote) {
|
|
17256
|
+
outputsFile = await writeStateFile(dir, "outputs.json", rows);
|
|
17257
|
+
}
|
|
17258
|
+
const files = [configFile2, ...codeFiles].map((file) => path14.relative(cwd, file));
|
|
17259
|
+
emitWithVersionNote(
|
|
17260
|
+
options,
|
|
17261
|
+
config,
|
|
17262
|
+
`Pulled "${workflow.title}" (version ${workflow.version}, draft ${workflow.draft}, ${workflow.enabled ? "enabled" : "not enabled"}).
|
|
17263
|
+
` + files.map((file) => ` ${file}`).join("\n") + outputsNote,
|
|
17264
|
+
{
|
|
17265
|
+
workflowId,
|
|
17266
|
+
apiUrl: target.apiUrl,
|
|
17267
|
+
title: workflow.title,
|
|
17268
|
+
version: workflow.version,
|
|
17269
|
+
draft: workflow.draft,
|
|
17270
|
+
enabled: workflow.enabled ?? false,
|
|
17271
|
+
dir,
|
|
17272
|
+
files,
|
|
17273
|
+
outputsFile: outputsFile ?? null
|
|
17274
|
+
}
|
|
17275
|
+
);
|
|
17276
|
+
});
|
|
17277
|
+
}
|
|
17278
|
+
|
|
16140
17279
|
// src/commands/publish.ts
|
|
16141
17280
|
import { stdin as stdin2, stdout } from "node:process";
|
|
16142
17281
|
import { createInterface as createInterface2 } from "node:readline/promises";
|
|
@@ -16374,23 +17513,23 @@ function registerPin(program3) {
|
|
|
16374
17513
|
}
|
|
16375
17514
|
|
|
16376
17515
|
// src/commands/push.ts
|
|
16377
|
-
import
|
|
17516
|
+
import path15 from "node:path";
|
|
16378
17517
|
|
|
16379
17518
|
// src/project/placeholder-check.ts
|
|
16380
17519
|
var TOKEN = /\{(?:outputs|loopActionGroup)\.[^:{}]+:::[^{}]*\}/g;
|
|
16381
|
-
function* stringsIn(value,
|
|
17520
|
+
function* stringsIn(value, path16) {
|
|
16382
17521
|
if (typeof value === "string") {
|
|
16383
|
-
yield [
|
|
17522
|
+
yield [path16, value];
|
|
16384
17523
|
} else if (Array.isArray(value)) {
|
|
16385
17524
|
for (const [index, item] of value.entries()) {
|
|
16386
|
-
yield* stringsIn(item, `${
|
|
17525
|
+
yield* stringsIn(item, `${path16}[${index}]`);
|
|
16387
17526
|
}
|
|
16388
17527
|
} else if (value !== null && typeof value === "object") {
|
|
16389
17528
|
for (const [key, item] of Object.entries(value)) {
|
|
16390
17529
|
if (key === "actions" || key === "paths") {
|
|
16391
17530
|
continue;
|
|
16392
17531
|
}
|
|
16393
|
-
yield* stringsIn(item,
|
|
17532
|
+
yield* stringsIn(item, path16 ? `${path16}.${key}` : key);
|
|
16394
17533
|
}
|
|
16395
17534
|
}
|
|
16396
17535
|
}
|
|
@@ -16501,7 +17640,7 @@ ${unbacked.map(
|
|
|
16501
17640
|
draft: result.draft,
|
|
16502
17641
|
inlined,
|
|
16503
17642
|
studioUrl,
|
|
16504
|
-
dir:
|
|
17643
|
+
dir: path15.resolve(dir),
|
|
16505
17644
|
unbackedPlaceholders: unbacked
|
|
16506
17645
|
}
|
|
16507
17646
|
);
|
|
@@ -16509,10 +17648,10 @@ ${unbacked.map(
|
|
|
16509
17648
|
}
|
|
16510
17649
|
|
|
16511
17650
|
// src/commands/spec.ts
|
|
16512
|
-
var
|
|
17651
|
+
var import_cli_table37 = __toESM(require_cli_table3(), 1);
|
|
16513
17652
|
var TABLE_STYLE2 = { head: [], border: [], "padding-left": 1, "padding-right": 1 };
|
|
16514
17653
|
function renderList() {
|
|
16515
|
-
const table = new
|
|
17654
|
+
const table = new import_cli_table37.default({
|
|
16516
17655
|
head: ["Type", "Kind", "Label", "Summary"],
|
|
16517
17656
|
colWidths: [40, 9, 24, 70],
|
|
16518
17657
|
wordWrap: true,
|
|
@@ -16540,7 +17679,7 @@ function renderEntry(entry) {
|
|
|
16540
17679
|
const blocks = [`${entry.type} (${entry.kind}) \u2014 ${entry.label}`, entry.summary, `Docs: ${entry.docs}`];
|
|
16541
17680
|
const inputs = Object.entries(entry.inputs);
|
|
16542
17681
|
if (inputs.length) {
|
|
16543
|
-
const table = new
|
|
17682
|
+
const table = new import_cli_table37.default({
|
|
16544
17683
|
head: ["Input", "Type", "Required", "Description"],
|
|
16545
17684
|
colWidths: [20, 9, 10, 90],
|
|
16546
17685
|
wordWrap: true,
|
|
@@ -16790,21 +17929,27 @@ ${item.lines.map((line) => ` ${line}`).join("\n")}`
|
|
|
16790
17929
|
}
|
|
16791
17930
|
|
|
16792
17931
|
// src/commands/workspaces.ts
|
|
16793
|
-
var
|
|
17932
|
+
var import_cli_table38 = __toESM(require_cli_table3(), 1);
|
|
16794
17933
|
function registerWorkspaces(program3) {
|
|
16795
|
-
program3.command("workspaces").description("The workspaces the stored token reaches, with your role in each").action(async (_opts, command) => {
|
|
17934
|
+
program3.command("workspaces").description("The workspaces the stored token reaches, with your role and the scopes the token grants in each").action(async (_opts, command) => {
|
|
16796
17935
|
const options = globalOptions(command);
|
|
16797
17936
|
const target = resolveTarget();
|
|
16798
17937
|
const client = await authenticatedClient(target);
|
|
16799
17938
|
const workspaces = await listWorkspaces(client);
|
|
16800
|
-
const table = new
|
|
16801
|
-
head: ["ID", "Name", "Role", "Default"],
|
|
16802
|
-
colWidths: [38,
|
|
17939
|
+
const table = new import_cli_table38.default({
|
|
17940
|
+
head: ["ID", "Name", "Role", "Default", "Scopes"],
|
|
17941
|
+
colWidths: [38, 26, 10, 9, 34],
|
|
16803
17942
|
wordWrap: true,
|
|
16804
17943
|
style: { head: [], border: [], "padding-left": 1, "padding-right": 1 }
|
|
16805
17944
|
});
|
|
16806
17945
|
for (const workspace of workspaces) {
|
|
16807
|
-
table.push([
|
|
17946
|
+
table.push([
|
|
17947
|
+
workspace.id,
|
|
17948
|
+
workspace.name,
|
|
17949
|
+
workspace.userRole ?? "",
|
|
17950
|
+
workspace.isDefault ? "yes" : "",
|
|
17951
|
+
(workspace.scopes ?? []).join(", ")
|
|
17952
|
+
]);
|
|
16808
17953
|
}
|
|
16809
17954
|
emit(
|
|
16810
17955
|
options,
|
|
@@ -16818,11 +17963,12 @@ Pass --workspace <id> to a command, or set SOFTR_WORKSPACE_ID, to work in one of
|
|
|
16818
17963
|
// package.json
|
|
16819
17964
|
var package_default = {
|
|
16820
17965
|
name: "@softr/cli",
|
|
16821
|
-
version: "0.1.
|
|
16822
|
-
description: "The Softr command line: build, test and publish Softr workflows from local files; more Softr APIs to come",
|
|
17966
|
+
version: "0.1.6-se21219.17",
|
|
17967
|
+
description: "The Softr command line: build, test and publish Softr workflows from local files, manage Softr Databases; more Softr APIs to come",
|
|
16823
17968
|
keywords: [
|
|
16824
17969
|
"softr",
|
|
16825
17970
|
"workflows",
|
|
17971
|
+
"databases",
|
|
16826
17972
|
"cli"
|
|
16827
17973
|
],
|
|
16828
17974
|
license: "UNLICENSED",
|
|
@@ -16845,7 +17991,7 @@ var package_default = {
|
|
|
16845
17991
|
format: "prettier --write .",
|
|
16846
17992
|
"format:check": "prettier --check .",
|
|
16847
17993
|
check: "npm run typecheck && npm run format:check && npm test && npm run build",
|
|
16848
|
-
"check:skill": "npx -y skills-ref@0.1.5 validate skills/softr-cli && npx -y skills-ref@0.1.5 validate skills/softr-workflows"
|
|
17994
|
+
"check:skill": "npx -y skills-ref@0.1.5 validate skills/softr-cli && npx -y skills-ref@0.1.5 validate skills/softr-workflows && npx -y skills-ref@0.1.5 validate skills/softr-db"
|
|
16849
17995
|
},
|
|
16850
17996
|
publishConfig: {
|
|
16851
17997
|
access: "public"
|
|
@@ -16868,7 +18014,7 @@ var package_default = {
|
|
|
16868
18014
|
};
|
|
16869
18015
|
|
|
16870
18016
|
// src/cli.ts
|
|
16871
|
-
var program2 = new Command().name("softr").description("The Softr command line: workflows today, more of Softr to come").version(package_default.version).option("--json", "print results as JSON");
|
|
18017
|
+
var program2 = new Command().name("softr").description("The Softr command line: workflows and databases today, more of Softr to come").version(package_default.version).option("--json", "print results as JSON");
|
|
16872
18018
|
registerLogin(program2);
|
|
16873
18019
|
registerWorkspaces(program2);
|
|
16874
18020
|
registerIntegrations(program2);
|
|
@@ -16888,4 +18034,10 @@ registerPublish(workflows);
|
|
|
16888
18034
|
registerPin(workflows);
|
|
16889
18035
|
registerSpec(workflows);
|
|
16890
18036
|
registerSchema(workflows);
|
|
18037
|
+
var db = program2.command("db").description("Manage Softr Databases: databases, their tables, fields and records");
|
|
18038
|
+
registerDatabases(db);
|
|
18039
|
+
registerTables(db);
|
|
18040
|
+
registerFields(db);
|
|
18041
|
+
registerViews(db);
|
|
18042
|
+
registerRecords(db);
|
|
16891
18043
|
program2.parseAsync(process.argv).catch(reportError);
|