@softr/cli 0.1.6-se21219.17 → 0.1.7-se22117.20
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 +27 -23
- package/dist/cli.mjs +223 -103
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -188,9 +188,13 @@ synced. That is how `push` knows there is nothing to send, how it notices that t
|
|
|
188
188
|
|
|
189
189
|
#### The node types on offer
|
|
190
190
|
|
|
191
|
-
`spec` lists
|
|
191
|
+
`spec` lists the curated set, small on purpose: the generic triggers (webhook, schedules, Softr Tables and Softr Apps
|
|
192
192
|
events, incoming email), the control nodes (branch, filter, loop, wait), `RESPONDED_TO_WEBHOOK`, `CALL_API`, and
|
|
193
|
-
`CUSTOM_CODE` for
|
|
193
|
+
`CUSTOM_CODE` for code. Each entry points to its public docs page and comes with a template and an example.
|
|
194
|
+
|
|
195
|
+
Any other node type the workflows service knows works in the file as well; the CLI just has no specification for it.
|
|
196
|
+
`spec <TYPE>` says where to get one (the Softr MCP server or the studio), `add <TYPE> --node-version <v>` writes the node
|
|
197
|
+
with empty inputs, and the service validates it on publish, as for every node.
|
|
194
198
|
|
|
195
199
|
#### A typical session
|
|
196
200
|
|
|
@@ -226,27 +230,27 @@ crm/
|
|
|
226
230
|
.softr-db/views/<tableId>.json # the views of a table, once views list read them
|
|
227
231
|
```
|
|
228
232
|
|
|
229
|
-
| Command
|
|
230
|
-
|
|
|
231
|
-
| `db list`
|
|
232
|
-
| `db get [<database>] [--dir <path>]`
|
|
233
|
-
| `db create <name> [--description <text>] [--workspace <id>] [--dir <path>]`
|
|
234
|
-
| `db update [<databaseId>] [--name <name>] [--description <text>] [--dir <path>]`
|
|
235
|
-
| `db delete [<databaseId>] [--force] [--dir <path>]`
|
|
236
|
-
| `db tables list` / `get <table>`
|
|
237
|
-
| `db tables create <name> [--field <name:TYPE>]... [--primary-field <name>]`
|
|
238
|
-
| `db tables update <table> [--name] [--description]` / `delete <table> [--force]`
|
|
239
|
-
| `db fields get <table> <field>`
|
|
240
|
-
| `db fields add <table> <name> --type <TYPE> [--options <json>] [--multiple]`
|
|
241
|
-
| `db fields update <table> <field> [--name] [--type] [--options <json>] [--[no-]multiple]`
|
|
242
|
-
| `db fields delete <table> <field>`
|
|
243
|
-
| `db views list <table>`
|
|
244
|
-
| `db records list <table> [--view <view>] [--limit <n>] [--offset <n>] [--all]`
|
|
245
|
-
| `db records search <table> [--filter <json>] [--sort <field[:desc]>]...`
|
|
246
|
-
| `db records get <table> <recordId>`
|
|
247
|
-
| `db records create <table> --fields <json>` / `update <table> <recordId> --fields <json>`
|
|
248
|
-
| `db records delete <table> <recordId>`
|
|
249
|
-
| `db records upload <table> <recordId> <field> <file> [--name <fileName>]`
|
|
233
|
+
| Command | What it does |
|
|
234
|
+
| --------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
235
|
+
| `db list` | The databases the token reaches, across all workspaces, with their workspace and number of tables. |
|
|
236
|
+
| `db get [<database>] [--dir <path>]` | One database with its tables, by id or exact name. Outside a database directory it creates `./<name-slug>/`; inside one it refreshes the cache, and a different database is an error. |
|
|
237
|
+
| `db create <name> [--description <text>] [--workspace <id>] [--dir <path>]` | Create an empty database and its directory. The workspace defaults to `SOFTR_WORKSPACE_ID`, else to your only or default workspace among those where the token grants `databases.schema:write`. |
|
|
238
|
+
| `db update [<databaseId>] [--name <name>] [--description <text>] [--dir <path>]` | Change the name, the description or both. |
|
|
239
|
+
| `db delete [<databaseId>] [--force] [--dir <path>]` | Delete a database. One that still has tables needs `--force`, which also deletes the tables and all their records. The directory keeps its `softr-db.jsonc`; the cache goes. |
|
|
240
|
+
| `db tables list` / `get <table>` | The tables of the database; one table with its fields (ids, types, primary/required/read-only). |
|
|
241
|
+
| `db tables create <name> [--field <name:TYPE>]... [--fields <json>] [--primary-field <name>]` | Create a table with at least one field: `--field` for types without options, `--fields` (a JSON array, `@<file>` or `-`) for any; the first one names a record. |
|
|
242
|
+
| `db tables update <table> [--name] [--description]` / `delete <table> [--force]` | Rename or describe a table; delete it (`--force` when it still has records). |
|
|
243
|
+
| `db fields get <table> <field>` | One field with its options. |
|
|
244
|
+
| `db fields add <table> <name> --type <TYPE> [--options <json>] [--multiple]` | Add a field. The types and their options: [field reference](https://docs.softr.io/softr-api/softr-database-api/table-fields/add-table-field). |
|
|
245
|
+
| `db fields update <table> <field> [--name] [--type] [--options <json>] [--[no-]multiple]` | Change a field; what is not passed stays as it is, `--options` replaces the options as a whole. |
|
|
246
|
+
| `db fields delete <table> <field>` | Delete a field and its values in every record. |
|
|
247
|
+
| `db views list <table>` | The views of a table (the API reads views only). |
|
|
248
|
+
| `db records list <table> [--view <view>] [--limit <n>] [--offset <n>] [--all]` | Records through a view, the table's default view unless `--view`; one page or `--all` in pages of 200. |
|
|
249
|
+
| `db records search <table> [--filter <json>] [--sort <field[:desc]>]...` | Records by a filter ([operators](https://docs.softr.io/softr-api/softr-database-api/records/search-records)); without `--filter`, every record. |
|
|
250
|
+
| `db records get <table> <recordId>` | One record. |
|
|
251
|
+
| `db records create <table> --fields <json>` / `update <table> <recordId> --fields <json>` | Create a record, or change some of its fields. `--fields` is inline JSON, `@<file>` or `-`. |
|
|
252
|
+
| `db records delete <table> <recordId>` | Delete a record. |
|
|
253
|
+
| `db records upload <table> <recordId> <field> <file> [--name <fileName>]` | Add a local file to an attachment field, stored under the local file name unless `--name`. |
|
|
250
254
|
|
|
251
255
|
Tables and fields take an id or an exact name; a name two items share is an error that lists their ids. The table,
|
|
252
256
|
field and view commands take the database from `--database <id>`, else `SOFTR_DATABASE_ID`, else the database
|
package/dist/cli.mjs
CHANGED
|
@@ -6070,18 +6070,74 @@ var CATALOG = catalog_default;
|
|
|
6070
6070
|
function catalogEntries(kind) {
|
|
6071
6071
|
return CATALOG.filter((entry) => !kind || entry.kind === kind);
|
|
6072
6072
|
}
|
|
6073
|
-
function
|
|
6073
|
+
function catalogEntryOrNull(type, kind) {
|
|
6074
6074
|
const wanted = type.trim().toUpperCase();
|
|
6075
|
-
|
|
6075
|
+
return CATALOG.find((candidate) => candidate.type === wanted && (!kind || candidate.kind === kind));
|
|
6076
|
+
}
|
|
6077
|
+
function findCatalogEntry(type, kind) {
|
|
6078
|
+
const entry = catalogEntryOrNull(type, kind);
|
|
6076
6079
|
if (!entry) {
|
|
6077
6080
|
const known = catalogEntries(kind).map((candidate) => candidate.type).join(", ");
|
|
6078
6081
|
throw new CliError(
|
|
6079
|
-
`"${type}" is not
|
|
6080
|
-
`
|
|
6082
|
+
`"${type.trim().toUpperCase()}" is not in the CLI's curated set${kind ? ` of ${kind}s` : ""}.`,
|
|
6083
|
+
`Curated: ${known}.
|
|
6084
|
+
${uncuratedGuidance(type)}`
|
|
6081
6085
|
);
|
|
6082
6086
|
}
|
|
6083
6087
|
return entry;
|
|
6084
6088
|
}
|
|
6089
|
+
function uncuratedGuidance(type) {
|
|
6090
|
+
const wanted = type.trim().toUpperCase();
|
|
6091
|
+
return [
|
|
6092
|
+
`${wanted} is not in the curated set, so the CLI has no template or specification for it; the workflows service`,
|
|
6093
|
+
"still runs it. Take its specification (inputs, the required ones, fixed options, version) from the Softr MCP server",
|
|
6094
|
+
"(get_node_specifications; workflow_get_node_specifications in an agent runtime) or from the studio. Then write the",
|
|
6095
|
+
"node with its type, version and inputs, or scaffold it: softr workflows add <TYPE> --node-version <v> [--kind trigger].",
|
|
6096
|
+
"Push and test it as any node: the service validates it on publish and names what is wrong."
|
|
6097
|
+
].join("\n");
|
|
6098
|
+
}
|
|
6099
|
+
function resolveNodeType(type, choice) {
|
|
6100
|
+
const entry = catalogEntryOrNull(type, choice.kind);
|
|
6101
|
+
if (entry) {
|
|
6102
|
+
if (choice.version) {
|
|
6103
|
+
throw new CliError(
|
|
6104
|
+
`${entry.type} is curated: the CLI pins its version (${entry.version}).`,
|
|
6105
|
+
'Drop --node-version; "softr workflows upgrade" moves nodes to the current one.'
|
|
6106
|
+
);
|
|
6107
|
+
}
|
|
6108
|
+
const { type: curatedType, kind, label, version: version2, template } = entry;
|
|
6109
|
+
return { type: curatedType, kind, label, version: version2, template: structuredClone(template), curated: true };
|
|
6110
|
+
}
|
|
6111
|
+
const otherKind = catalogEntryOrNull(type);
|
|
6112
|
+
if (otherKind) {
|
|
6113
|
+
throw new CliError(
|
|
6114
|
+
`${otherKind.type} is curated as ${article(otherKind.kind)}, not ${article(choice.kind ?? "action")}.`,
|
|
6115
|
+
`Curated ${choice.kind ?? "action"}s: ${catalogEntries(choice.kind ?? "action").map((candidate) => candidate.type).join(", ")}.`
|
|
6116
|
+
);
|
|
6117
|
+
}
|
|
6118
|
+
const wanted = type.trim().toUpperCase();
|
|
6119
|
+
if (!choice.version) {
|
|
6120
|
+
throw new CliError(
|
|
6121
|
+
`"${wanted}" is not in the CLI's curated set, so pass --node-version <v>: the node version the service runs.`,
|
|
6122
|
+
uncuratedGuidance(type)
|
|
6123
|
+
);
|
|
6124
|
+
}
|
|
6125
|
+
return {
|
|
6126
|
+
type: wanted,
|
|
6127
|
+
kind: choice.kind ?? "action",
|
|
6128
|
+
label: labelOf(wanted),
|
|
6129
|
+
version: choice.version,
|
|
6130
|
+
template: {},
|
|
6131
|
+
curated: false
|
|
6132
|
+
};
|
|
6133
|
+
}
|
|
6134
|
+
function article(kind) {
|
|
6135
|
+
return kind === "action" ? "an action" : "a trigger";
|
|
6136
|
+
}
|
|
6137
|
+
function labelOf(type) {
|
|
6138
|
+
const words = type.toLowerCase().split("_");
|
|
6139
|
+
return [words[0].charAt(0).toUpperCase() + words[0].slice(1), ...words.slice(1)].join(" ");
|
|
6140
|
+
}
|
|
6085
6141
|
function isTestableType(type) {
|
|
6086
6142
|
return !NOT_TESTABLE_TYPES.has(type);
|
|
6087
6143
|
}
|
|
@@ -14346,42 +14402,48 @@ function pathsAt(config, pathsPath) {
|
|
|
14346
14402
|
return Array.isArray(current2) ? current2 : [];
|
|
14347
14403
|
}
|
|
14348
14404
|
function registerEdit(program3) {
|
|
14349
|
-
program3.command("replace").description("Give a node another type: keeps its id, title and paths; inputs start from the new type's template").argument("<nodeId>", "the trigger or action to change").argument(
|
|
14350
|
-
|
|
14351
|
-
|
|
14352
|
-
|
|
14353
|
-
|
|
14354
|
-
|
|
14355
|
-
|
|
14356
|
-
|
|
14357
|
-
|
|
14358
|
-
|
|
14359
|
-
|
|
14360
|
-
|
|
14361
|
-
|
|
14362
|
-
|
|
14363
|
-
|
|
14364
|
-
|
|
14365
|
-
|
|
14366
|
-
|
|
14367
|
-
|
|
14368
|
-
|
|
14369
|
-
|
|
14370
|
-
|
|
14371
|
-
|
|
14372
|
-
|
|
14373
|
-
|
|
14374
|
-
|
|
14375
|
-
|
|
14405
|
+
program3.command("replace").description("Give a node another type: keeps its id, title and paths; inputs start from the new type's template").argument("<nodeId>", "the trigger or action to change").argument(
|
|
14406
|
+
"<type>",
|
|
14407
|
+
'the new type from "spec" (same kind: trigger or action), or any type the service knows (with --node-version)'
|
|
14408
|
+
).option("--lang <language>", "for CUSTOM_CODE: JAVASCRIPT or PYTHON (default JAVASCRIPT)").option("--node-version <version>", "for a type outside the curated set: the node version the service runs").option("--dir <path>", "workflow directory (default: the current directory)").action(
|
|
14409
|
+
async (nodeId, type, opts, command) => {
|
|
14410
|
+
const options = globalOptions(command);
|
|
14411
|
+
const dir = await findWorkflowDir(process.cwd(), opts.dir);
|
|
14412
|
+
const config = await readConfig(dir);
|
|
14413
|
+
const location = locateNode(config, nodeId);
|
|
14414
|
+
if (!location) {
|
|
14415
|
+
throw new CliError(`No node "${nodeId}" in softr-workflows.jsonc.`);
|
|
14416
|
+
}
|
|
14417
|
+
const entry = resolveNodeType(type, { kind: location.kind, version: opts.nodeVersion });
|
|
14418
|
+
if (location.node.type === LOOP_TYPE && entry.type !== LOOP_TYPE) {
|
|
14419
|
+
throw new CliError(`"${nodeId}" is a loop with a body; remove it instead of replacing it.`);
|
|
14420
|
+
}
|
|
14421
|
+
const title = location.node.title ?? entry.label;
|
|
14422
|
+
const { inputs, codeFile } = await newNodeInputs(entry, nodeId, title, opts.lang, dir);
|
|
14423
|
+
const node = { ...location.node, type: entry.type, version: entry.version, inputs };
|
|
14424
|
+
delete node["outputSchema"];
|
|
14425
|
+
await editConfig(dir, [{ path: location.path, value: node }]);
|
|
14426
|
+
const oldCode = location.node.type === CUSTOM_CODE_TYPE && isFileRef(location.node.inputs?.["code"]) ? location.node.inputs?.["code"] : void 0;
|
|
14427
|
+
const notes = [
|
|
14428
|
+
oldCode && oldCode !== inputs["code"] ? `The previous code file (${oldCode}) is left in place; delete it when you no longer need it.` : "",
|
|
14429
|
+
entry.kind === "trigger" && (config.triggers?.length ?? 0) > 1 ? "The service accepts one trigger per published workflow for now." : "",
|
|
14430
|
+
entry.curated ? `Fill inputs.${Object.keys(entry.template)[0] ?? "\u2026"} etc. in softr-workflows.jsonc (see "spec ${entry.type}"), then push and test ${nodeId}.` : `Fill its inputs in softr-workflows.jsonc from its specification ("spec ${entry.type}" says where to find it), then push and test ${nodeId}.`
|
|
14431
|
+
].filter(Boolean);
|
|
14432
|
+
emitWithVersionNote(
|
|
14433
|
+
options,
|
|
14434
|
+
await readConfig(dir),
|
|
14435
|
+
`Replaced "${nodeId}" (${location.node.type}) with ${entry.type}; paths kept.
|
|
14376
14436
|
${notes.join("\n")}`,
|
|
14377
|
-
|
|
14378
|
-
|
|
14379
|
-
|
|
14380
|
-
|
|
14381
|
-
|
|
14382
|
-
|
|
14383
|
-
|
|
14384
|
-
|
|
14437
|
+
{
|
|
14438
|
+
id: nodeId,
|
|
14439
|
+
from: location.node.type,
|
|
14440
|
+
to: entry.type,
|
|
14441
|
+
curated: entry.curated,
|
|
14442
|
+
codeFile: codeFile ? path5.relative(process.cwd(), codeFile) : null
|
|
14443
|
+
}
|
|
14444
|
+
);
|
|
14445
|
+
}
|
|
14446
|
+
);
|
|
14385
14447
|
program3.command("remove").description("Remove a node and every path that leads to or from it (a loop goes with its body)").argument("<nodeId>", "the trigger or action to remove").option("--dir <path>", "workflow directory (default: the current directory)").action(async (nodeId, opts, command) => {
|
|
14386
14448
|
const options = globalOptions(command);
|
|
14387
14449
|
const dir = await findWorkflowDir(process.cwd(), opts.dir);
|
|
@@ -14430,6 +14492,15 @@ function slug(title, fallback = "workflow") {
|
|
|
14430
14492
|
}
|
|
14431
14493
|
|
|
14432
14494
|
// src/commands/add.ts
|
|
14495
|
+
function parseKind(kind) {
|
|
14496
|
+
if (kind === void 0) {
|
|
14497
|
+
return void 0;
|
|
14498
|
+
}
|
|
14499
|
+
if (kind === "trigger" || kind === "action") {
|
|
14500
|
+
return kind;
|
|
14501
|
+
}
|
|
14502
|
+
throw new CliError(`--kind takes trigger or action, not "${kind}".`);
|
|
14503
|
+
}
|
|
14433
14504
|
function uniqueId(base, taken) {
|
|
14434
14505
|
if (!taken.has(base)) {
|
|
14435
14506
|
return base;
|
|
@@ -14465,11 +14536,14 @@ function defaultAfter(config, loopId) {
|
|
|
14465
14536
|
);
|
|
14466
14537
|
}
|
|
14467
14538
|
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(
|
|
14539
|
+
program3.command("add").description("Add an action after an existing node, or a trigger; custom code gets its own file under actions/").argument(
|
|
14540
|
+
"<type>",
|
|
14541
|
+
'node type from "spec", e.g. CUSTOM_CODE or WEBHOOK, or any type the service knows (with --node-version)'
|
|
14542
|
+
).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("--node-version <version>", "for a type outside the curated set: the node version the service runs").option("--kind <kind>", "for a type outside the curated set: trigger or action (default action)").option("--dir <path>", "workflow directory (default: the current directory)").action(async (type, opts, command) => {
|
|
14469
14543
|
const options = globalOptions(command);
|
|
14470
14544
|
const dir = await findWorkflowDir(process.cwd(), opts.dir);
|
|
14471
14545
|
const config = await readConfig(dir);
|
|
14472
|
-
const entry =
|
|
14546
|
+
const entry = resolveNodeType(type, { kind: parseKind(opts.kind), version: opts.nodeVersion });
|
|
14473
14547
|
const nodes = [...config.triggers ?? [], ...allActions(config.actions)];
|
|
14474
14548
|
const taken = new Set(nodes.map((node2) => node2.id));
|
|
14475
14549
|
const id = opts.id ?? uniqueId(entry.kind === "trigger" ? "trigger" : slug(entry.label), taken);
|
|
@@ -14509,6 +14583,7 @@ ${notes.join("\n")}`,
|
|
|
14509
14583
|
id,
|
|
14510
14584
|
kind: "trigger",
|
|
14511
14585
|
type: entry.type,
|
|
14586
|
+
curated: entry.curated,
|
|
14512
14587
|
to: opts.to ?? null
|
|
14513
14588
|
}
|
|
14514
14589
|
);
|
|
@@ -14533,6 +14608,7 @@ ${notes.join("\n")}`,
|
|
|
14533
14608
|
const next = [
|
|
14534
14609
|
codeFile ? `Write the function in ${path6.relative(process.cwd(), codeFile)} and map its inputs under inputs.inputData in softr-workflows.jsonc.` : "",
|
|
14535
14610
|
entry.type === "BRANCH" || entry.type === "FILTER" ? `Add the condition(s) on the path(s) leaving it in softr-workflows.jsonc (see "spec ${entry.type}").` : "",
|
|
14611
|
+
entry.curated ? "" : `Fill its inputs in softr-workflows.jsonc from its specification ("spec ${entry.type}" says where to find it).`,
|
|
14536
14612
|
`Then: softr workflows push, softr workflows test ${id}.`
|
|
14537
14613
|
].filter(Boolean);
|
|
14538
14614
|
emitWithVersionNote(
|
|
@@ -14544,6 +14620,7 @@ ${next.join("\n")}`,
|
|
|
14544
14620
|
id,
|
|
14545
14621
|
kind: "action",
|
|
14546
14622
|
type: entry.type,
|
|
14623
|
+
curated: entry.curated,
|
|
14547
14624
|
after,
|
|
14548
14625
|
loopId: opts.inLoop ?? null,
|
|
14549
14626
|
codeFile: codeFile ? path6.relative(process.cwd(), codeFile) : null
|
|
@@ -15697,7 +15774,11 @@ async function readJsonArgument(value, option, hint) {
|
|
|
15697
15774
|
}
|
|
15698
15775
|
text = Buffer.concat(chunks).toString("utf8");
|
|
15699
15776
|
} else if (value.startsWith("@")) {
|
|
15700
|
-
|
|
15777
|
+
try {
|
|
15778
|
+
text = await readFile8(value.slice(1), "utf8");
|
|
15779
|
+
} catch {
|
|
15780
|
+
throw new CliError(`Cannot read ${value.slice(1)} for ${option}.`);
|
|
15781
|
+
}
|
|
15701
15782
|
}
|
|
15702
15783
|
try {
|
|
15703
15784
|
return JSON.parse(text);
|
|
@@ -15730,8 +15811,8 @@ async function currentTables(context, fresh) {
|
|
|
15730
15811
|
context.cache = await refreshSchemaCache(context.client, context.dir, context.databaseId);
|
|
15731
15812
|
return context.cache.tables;
|
|
15732
15813
|
}
|
|
15733
|
-
|
|
15734
|
-
const table = byIdOrName(
|
|
15814
|
+
function tableIn(context, tables, reference) {
|
|
15815
|
+
const table = byIdOrName(tables, reference, "table");
|
|
15735
15816
|
if (!table) {
|
|
15736
15817
|
throw new CliError(
|
|
15737
15818
|
`Database ${context.databaseId} has no table with the id or name "${reference}".`,
|
|
@@ -15740,7 +15821,14 @@ async function findTable(context, reference, fresh = false) {
|
|
|
15740
15821
|
}
|
|
15741
15822
|
return table;
|
|
15742
15823
|
}
|
|
15743
|
-
async function
|
|
15824
|
+
async function findTableOnServer(context, reference) {
|
|
15825
|
+
return tableIn(context, await currentTables(context, true), reference);
|
|
15826
|
+
}
|
|
15827
|
+
async function findTableCached(context, reference) {
|
|
15828
|
+
const cached = context.dir && context.cache ? byIdOrName(context.cache.tables, reference, "table") : void 0;
|
|
15829
|
+
return cached ?? findTableOnServer(context, reference);
|
|
15830
|
+
}
|
|
15831
|
+
async function findFields(context, table, references) {
|
|
15744
15832
|
const resolve = (current3) => {
|
|
15745
15833
|
const fields2 = /* @__PURE__ */ new Map();
|
|
15746
15834
|
for (const reference of references) {
|
|
@@ -15753,8 +15841,8 @@ async function findFields(context, table, references, fresh = false) {
|
|
|
15753
15841
|
};
|
|
15754
15842
|
let current2 = table;
|
|
15755
15843
|
let fields = resolve(current2);
|
|
15756
|
-
if (fields.size < new Set(references).size && context.dir
|
|
15757
|
-
current2 = await
|
|
15844
|
+
if (fields.size < new Set(references).size && context.dir) {
|
|
15845
|
+
current2 = await findTableOnServer(context, table.id);
|
|
15758
15846
|
fields = resolve(current2);
|
|
15759
15847
|
}
|
|
15760
15848
|
const missing = references.filter((reference) => !fields.has(reference));
|
|
@@ -15766,9 +15854,34 @@ async function findFields(context, table, references, fresh = false) {
|
|
|
15766
15854
|
}
|
|
15767
15855
|
return { table: current2, fields };
|
|
15768
15856
|
}
|
|
15769
|
-
async function
|
|
15770
|
-
const
|
|
15771
|
-
|
|
15857
|
+
async function readViews(context, table) {
|
|
15858
|
+
const views = await listViews(context.client, context.databaseId, table.id);
|
|
15859
|
+
if (context.dir) {
|
|
15860
|
+
await writeViewsCache(context.dir, table.id, views);
|
|
15861
|
+
}
|
|
15862
|
+
return views;
|
|
15863
|
+
}
|
|
15864
|
+
async function findView(context, table, reference) {
|
|
15865
|
+
const cached = context.dir ? (await readViewsCache(context.dir, table.id))?.views : void 0;
|
|
15866
|
+
const view = (cached ? byIdOrName(cached, reference, "view") : void 0) ?? byIdOrName(await readViews(context, table), reference, "view");
|
|
15867
|
+
if (!view) {
|
|
15868
|
+
throw new CliError(
|
|
15869
|
+
`Table ${table.name} has no view with the id or name "${reference}".`,
|
|
15870
|
+
`"softr db views list ${table.id}" shows its views.`
|
|
15871
|
+
);
|
|
15872
|
+
}
|
|
15873
|
+
return view;
|
|
15874
|
+
}
|
|
15875
|
+
async function findField(context, tableReference, reference) {
|
|
15876
|
+
const table = await findTableOnServer(context, tableReference);
|
|
15877
|
+
const field = byIdOrName(table.fields, reference, "field");
|
|
15878
|
+
if (!field) {
|
|
15879
|
+
throw new CliError(
|
|
15880
|
+
`Table ${table.name} has no field with the id or name "${reference}".`,
|
|
15881
|
+
`"softr db tables get ${table.id}" shows its fields.`
|
|
15882
|
+
);
|
|
15883
|
+
}
|
|
15884
|
+
return { table, field };
|
|
15772
15885
|
}
|
|
15773
15886
|
async function afterSchemaWrite(context) {
|
|
15774
15887
|
if (context.dir) {
|
|
@@ -15787,6 +15900,21 @@ function parseFieldSpec(spec) {
|
|
|
15787
15900
|
}
|
|
15788
15901
|
return { name, type };
|
|
15789
15902
|
}
|
|
15903
|
+
function parseFieldList(value) {
|
|
15904
|
+
if (!Array.isArray(value)) {
|
|
15905
|
+
throw new CliError(
|
|
15906
|
+
"--fields must be a JSON array of fields.",
|
|
15907
|
+
`For example: [{"name": "Stage", "type": "SELECT", "options": {...}}]`
|
|
15908
|
+
);
|
|
15909
|
+
}
|
|
15910
|
+
return value.map((item, index) => {
|
|
15911
|
+
const field = item;
|
|
15912
|
+
if (!field || typeof field !== "object" || typeof field.name !== "string" || !field.name || typeof field.type !== "string" || !field.type) {
|
|
15913
|
+
throw new CliError(`--fields item ${index + 1} needs a "name" and a "type".`);
|
|
15914
|
+
}
|
|
15915
|
+
return { ...field, name: field.name, type: field.type.toUpperCase() };
|
|
15916
|
+
});
|
|
15917
|
+
}
|
|
15790
15918
|
function renderTables(tables) {
|
|
15791
15919
|
if (!tables.length) {
|
|
15792
15920
|
return "The database has no tables. Create one with: softr db tables create <name>";
|
|
@@ -15849,7 +15977,7 @@ function registerTables(db2) {
|
|
|
15849
15977
|
).action(async (reference, opts, command) => {
|
|
15850
15978
|
const options = globalOptions(command);
|
|
15851
15979
|
const context = await schemaContext(opts);
|
|
15852
|
-
const table = await
|
|
15980
|
+
const table = await findTableOnServer(context, reference);
|
|
15853
15981
|
emit(options, renderTable(table), { table });
|
|
15854
15982
|
});
|
|
15855
15983
|
withDatabaseOptions(
|
|
@@ -15857,22 +15985,28 @@ function registerTables(db2) {
|
|
|
15857
15985
|
"Create a table with at least one field; the first one names a record unless --primary-field says otherwise"
|
|
15858
15986
|
).argument("<name>", "the table name").option("--description <text>", "what the table holds").option(
|
|
15859
15987
|
"--field <name:TYPE>",
|
|
15860
|
-
|
|
15988
|
+
"a field of a type without options, repeatable",
|
|
15861
15989
|
(value, previous) => [...previous, value],
|
|
15862
15990
|
[]
|
|
15991
|
+
).option(
|
|
15992
|
+
"--fields <json>",
|
|
15993
|
+
`fields as a JSON array of {name, type, options?, description?, allowMultipleEntries?}, @<file> or - for stdin; they follow the --field ones (types and options: ${FIELD_REFERENCE_URL})`
|
|
15863
15994
|
).option("--primary-field <name>", "the field that names a record (default: the first field)")
|
|
15864
15995
|
).action(
|
|
15865
15996
|
async (name, opts, command) => {
|
|
15866
15997
|
const options = globalOptions(command);
|
|
15867
|
-
|
|
15998
|
+
const fields = [
|
|
15999
|
+
...opts.field.map(parseFieldSpec),
|
|
16000
|
+
...opts.fields === void 0 ? [] : parseFieldList(await readJsonArgument(opts.fields, "--fields"))
|
|
16001
|
+
];
|
|
16002
|
+
if (!fields.length) {
|
|
15868
16003
|
throw new CliError(
|
|
15869
|
-
'A table needs at least one field: pass --field <name>:<TYPE>, for example --field "Name:SINGLE_LINE_TEXT".',
|
|
15870
|
-
"
|
|
16004
|
+
'A table needs at least one field: pass --field <name>:<TYPE>, for example --field "Name:SINGLE_LINE_TEXT", or --fields with a JSON array.',
|
|
16005
|
+
"The first field names a record unless --primary-field names another."
|
|
15871
16006
|
);
|
|
15872
16007
|
}
|
|
15873
|
-
const fields = opts.field.map(parseFieldSpec);
|
|
15874
16008
|
if (opts.primaryField && !fields.some((field) => field.name === opts.primaryField)) {
|
|
15875
|
-
throw new CliError(`--primary-field "${opts.primaryField}" is not one of the
|
|
16009
|
+
throw new CliError(`--primary-field "${opts.primaryField}" is not one of the fields' names.`);
|
|
15876
16010
|
}
|
|
15877
16011
|
const context = await schemaContext(opts);
|
|
15878
16012
|
const table = await createTable(context.client, context.databaseId, {
|
|
@@ -15894,7 +16028,7 @@ function registerTables(db2) {
|
|
|
15894
16028
|
}
|
|
15895
16029
|
const options = globalOptions(command);
|
|
15896
16030
|
const context = await schemaContext(opts);
|
|
15897
|
-
const current2 = await
|
|
16031
|
+
const current2 = await findTableOnServer(context, reference);
|
|
15898
16032
|
const table = await updateTable(context.client, context.databaseId, current2.id, {
|
|
15899
16033
|
name: opts.name ?? current2.name,
|
|
15900
16034
|
description: opts.description ?? current2.description
|
|
@@ -15908,7 +16042,7 @@ function registerTables(db2) {
|
|
|
15908
16042
|
).action(async (reference, opts, command) => {
|
|
15909
16043
|
const options = globalOptions(command);
|
|
15910
16044
|
const context = await schemaContext(opts);
|
|
15911
|
-
const table = await
|
|
16045
|
+
const table = await findTableOnServer(context, reference);
|
|
15912
16046
|
await deleteTable(context.client, context.databaseId, table.id, opts.force === true);
|
|
15913
16047
|
await afterSchemaWrite(context);
|
|
15914
16048
|
emit(options, `Deleted table ${table.name} (${table.id}).`, { deleted: table.id });
|
|
@@ -15921,7 +16055,7 @@ function registerFields(db2) {
|
|
|
15921
16055
|
).action(async (tableReference, reference, opts, command) => {
|
|
15922
16056
|
const options = globalOptions(command);
|
|
15923
16057
|
const context = await schemaContext(opts);
|
|
15924
|
-
const { table, field } = await findField(context, tableReference, reference
|
|
16058
|
+
const { table, field } = await findField(context, tableReference, reference);
|
|
15925
16059
|
emit(options, renderField(table, field), { tableId: table.id, field });
|
|
15926
16060
|
});
|
|
15927
16061
|
withDatabaseOptions(
|
|
@@ -15931,7 +16065,7 @@ function registerFields(db2) {
|
|
|
15931
16065
|
const options = globalOptions(command);
|
|
15932
16066
|
const fieldOptions = await parseOptions(opts.options);
|
|
15933
16067
|
const context = await schemaContext(opts);
|
|
15934
|
-
const table = await
|
|
16068
|
+
const table = await findTableCached(context, tableReference);
|
|
15935
16069
|
const field = await addField(context.client, context.databaseId, table.id, {
|
|
15936
16070
|
name,
|
|
15937
16071
|
type: opts.type.toUpperCase(),
|
|
@@ -15959,7 +16093,7 @@ function registerFields(db2) {
|
|
|
15959
16093
|
}
|
|
15960
16094
|
const options = globalOptions(command);
|
|
15961
16095
|
const context = await schemaContext(opts);
|
|
15962
|
-
const { table, field: current2 } = await findField(context, tableReference, reference
|
|
16096
|
+
const { table, field: current2 } = await findField(context, tableReference, reference);
|
|
15963
16097
|
const field = await updateField(context.client, context.databaseId, table.id, current2.id, {
|
|
15964
16098
|
name: opts.name ?? current2.name,
|
|
15965
16099
|
type: opts.type?.toUpperCase() ?? current2.type,
|
|
@@ -15981,7 +16115,7 @@ function registerFields(db2) {
|
|
|
15981
16115
|
).action(async (tableReference, reference, opts, command) => {
|
|
15982
16116
|
const options = globalOptions(command);
|
|
15983
16117
|
const context = await schemaContext(opts);
|
|
15984
|
-
const { table, field } = await findField(context, tableReference, reference
|
|
16118
|
+
const { table, field } = await findField(context, tableReference, reference);
|
|
15985
16119
|
await deleteField(context.client, context.databaseId, table.id, field.id);
|
|
15986
16120
|
await afterSchemaWrite(context);
|
|
15987
16121
|
emit(options, `Deleted field ${field.name} (${field.id}) from ${table.name}.`, {
|
|
@@ -15997,11 +16131,8 @@ function registerViews(db2) {
|
|
|
15997
16131
|
).action(async (reference, opts, command) => {
|
|
15998
16132
|
const options = globalOptions(command);
|
|
15999
16133
|
const context = await schemaContext(opts);
|
|
16000
|
-
const table = await
|
|
16001
|
-
const items = await
|
|
16002
|
-
if (context.dir) {
|
|
16003
|
-
await writeViewsCache(context.dir, table.id, items);
|
|
16004
|
-
}
|
|
16134
|
+
const table = await findTableOnServer(context, reference);
|
|
16135
|
+
const items = await readViews(context, table);
|
|
16005
16136
|
emit(options, renderViews(items), { tableId: table.id, views: items });
|
|
16006
16137
|
});
|
|
16007
16138
|
}
|
|
@@ -16101,24 +16232,6 @@ function parseSort(spec) {
|
|
|
16101
16232
|
}
|
|
16102
16233
|
return { field: spec.trim(), sortType: "ASC" };
|
|
16103
16234
|
}
|
|
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
16235
|
function withFieldNames(record, table) {
|
|
16123
16236
|
const names2 = new Map(table.fields.map((field) => [field.id, field.name]));
|
|
16124
16237
|
return {
|
|
@@ -16158,7 +16271,7 @@ function registerRecords(db2) {
|
|
|
16158
16271
|
const options = globalOptions(command);
|
|
16159
16272
|
const pages = paging(opts);
|
|
16160
16273
|
const context = await schemaContext(opts);
|
|
16161
|
-
const table = await
|
|
16274
|
+
const table = await findTableCached(context, reference);
|
|
16162
16275
|
const viewId = opts.view ? (await findView(context, table, opts.view)).id : void 0;
|
|
16163
16276
|
const page = await readPages(
|
|
16164
16277
|
pages,
|
|
@@ -16196,7 +16309,7 @@ function registerRecords(db2) {
|
|
|
16196
16309
|
const context = await schemaContext(opts);
|
|
16197
16310
|
const references = new Set(sorts.map((sort) => sort.field));
|
|
16198
16311
|
fieldReferences(condition, references);
|
|
16199
|
-
const { table, ids } = await fieldIdsFor(context, await
|
|
16312
|
+
const { table, ids } = await fieldIdsFor(context, await findTableCached(context, reference), [...references]);
|
|
16200
16313
|
const search = (offset2, limit) => ({
|
|
16201
16314
|
paging: { offset: offset2, limit },
|
|
16202
16315
|
...sorts.length ? { sorting: sorts.map((sort) => ({ sortingField: ids.get(sort.field), sortType: sort.sortType })) } : {},
|
|
@@ -16216,7 +16329,7 @@ function registerRecords(db2) {
|
|
|
16216
16329
|
).action(async (reference, recordId, opts, command) => {
|
|
16217
16330
|
const options = globalOptions(command);
|
|
16218
16331
|
const context = await schemaContext(opts);
|
|
16219
|
-
const table = await
|
|
16332
|
+
const table = await findTableCached(context, reference);
|
|
16220
16333
|
const record = await getRecord(context.client, context.databaseId, table.id, recordId, !opts.fieldIds);
|
|
16221
16334
|
emit(options, renderRecord(record), { tableId: table.id, record });
|
|
16222
16335
|
});
|
|
@@ -16231,7 +16344,7 @@ function registerRecords(db2) {
|
|
|
16231
16344
|
const options = globalOptions(command);
|
|
16232
16345
|
const fields = asObject(await readJsonArgument(opts.fields, "--fields"), "--fields");
|
|
16233
16346
|
const context = await schemaContext(opts);
|
|
16234
|
-
const table = await
|
|
16347
|
+
const table = await findTableCached(context, reference);
|
|
16235
16348
|
const record = await createRecord(
|
|
16236
16349
|
context.client,
|
|
16237
16350
|
context.databaseId,
|
|
@@ -16256,7 +16369,7 @@ function registerRecords(db2) {
|
|
|
16256
16369
|
throw new CliError("Nothing to change: --fields is an empty object.");
|
|
16257
16370
|
}
|
|
16258
16371
|
const context = await schemaContext(opts);
|
|
16259
|
-
const table = await
|
|
16372
|
+
const table = await findTableCached(context, reference);
|
|
16260
16373
|
const record = await updateRecord(
|
|
16261
16374
|
context.client,
|
|
16262
16375
|
context.databaseId,
|
|
@@ -16273,7 +16386,7 @@ function registerRecords(db2) {
|
|
|
16273
16386
|
).action(async (reference, recordId, opts, command) => {
|
|
16274
16387
|
const options = globalOptions(command);
|
|
16275
16388
|
const context = await schemaContext(opts);
|
|
16276
|
-
const table = await
|
|
16389
|
+
const table = await findTableOnServer(context, reference);
|
|
16277
16390
|
await deleteRecord(context.client, context.databaseId, table.id, recordId);
|
|
16278
16391
|
emit(options, `Deleted record ${recordId} from ${table.name}.`, { tableId: table.id, deleted: recordId });
|
|
16279
16392
|
});
|
|
@@ -16291,13 +16404,12 @@ function registerRecords(db2) {
|
|
|
16291
16404
|
throw new CliError(`Cannot read ${file}.`);
|
|
16292
16405
|
}
|
|
16293
16406
|
const context = await schemaContext(opts);
|
|
16294
|
-
const found = await
|
|
16295
|
-
const
|
|
16296
|
-
|
|
16297
|
-
|
|
16298
|
-
throw new CliError(`Field "${fieldReference}" has the type ${fieldType}; upload needs an ATTACHMENT field.`);
|
|
16407
|
+
const found = await findFields(context, await findTableCached(context, reference), [fieldReference]);
|
|
16408
|
+
const field = found.fields.get(fieldReference);
|
|
16409
|
+
if (field.type !== "ATTACHMENT") {
|
|
16410
|
+
throw new CliError(`Field "${fieldReference}" has the type ${field.type}; upload needs an ATTACHMENT field.`);
|
|
16299
16411
|
}
|
|
16300
|
-
const uploaded = await uploadAttachment(context.client, context.databaseId, found.table.id, recordId,
|
|
16412
|
+
const uploaded = await uploadAttachment(context.client, context.databaseId, found.table.id, recordId, field.id, {
|
|
16301
16413
|
name: opts.name ?? path12.basename(file),
|
|
16302
16414
|
content
|
|
16303
16415
|
});
|
|
@@ -17662,7 +17774,8 @@ function renderList() {
|
|
|
17662
17774
|
}
|
|
17663
17775
|
return [
|
|
17664
17776
|
table.toString(),
|
|
17665
|
-
"
|
|
17777
|
+
"These are the curated types: documented here, with a template and an example each. Any other node type the",
|
|
17778
|
+
'workflows service knows can be written into softr-workflows.jsonc as well; "softr workflows spec <TYPE>" says how.',
|
|
17666
17779
|
'Run "softr workflows spec <TYPE>" for the inputs, output and an example of one type.'
|
|
17667
17780
|
].join("\n");
|
|
17668
17781
|
}
|
|
@@ -17721,7 +17834,9 @@ ${JSON.stringify(extra.json, null, 2)}`);
|
|
|
17721
17834
|
return blocks.join("\n\n");
|
|
17722
17835
|
}
|
|
17723
17836
|
function registerSpec(program3) {
|
|
17724
|
-
program3.command("spec").description(
|
|
17837
|
+
program3.command("spec").description(
|
|
17838
|
+
"List the curated node types, or show the inputs, output and an example of one; any other type: how to add it"
|
|
17839
|
+
).argument("[type]", "a node type such as CUSTOM_CODE").action((type, _opts, command) => {
|
|
17725
17840
|
const options = globalOptions(command);
|
|
17726
17841
|
if (!type) {
|
|
17727
17842
|
emit(options, renderList(), {
|
|
@@ -17735,8 +17850,13 @@ function registerSpec(program3) {
|
|
|
17735
17850
|
});
|
|
17736
17851
|
return;
|
|
17737
17852
|
}
|
|
17738
|
-
const entry =
|
|
17739
|
-
|
|
17853
|
+
const entry = catalogEntryOrNull(type);
|
|
17854
|
+
if (!entry) {
|
|
17855
|
+
const guidance = uncuratedGuidance(type);
|
|
17856
|
+
emit(options, guidance, { type: type.trim().toUpperCase(), curated: false, guidance });
|
|
17857
|
+
return;
|
|
17858
|
+
}
|
|
17859
|
+
emit(options, renderEntry(entry), { ...entry, curated: true, testable: isTestableType(entry.type) });
|
|
17740
17860
|
});
|
|
17741
17861
|
}
|
|
17742
17862
|
|
|
@@ -17963,7 +18083,7 @@ Pass --workspace <id> to a command, or set SOFTR_WORKSPACE_ID, to work in one of
|
|
|
17963
18083
|
// package.json
|
|
17964
18084
|
var package_default = {
|
|
17965
18085
|
name: "@softr/cli",
|
|
17966
|
-
version: "0.1.
|
|
18086
|
+
version: "0.1.7-se22117.20",
|
|
17967
18087
|
description: "The Softr command line: build, test and publish Softr workflows from local files, manage Softr Databases; more Softr APIs to come",
|
|
17968
18088
|
keywords: [
|
|
17969
18089
|
"softr",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@softr/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7-se22117.20",
|
|
4
4
|
"description": "The Softr command line: build, test and publish Softr workflows from local files, manage Softr Databases; more Softr APIs to come",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"softr",
|