@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.
Files changed (3) hide show
  1. package/README.md +27 -23
  2. package/dist/cli.mjs +223 -103
  3. 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 them. The set is small on purpose: the generic triggers (webhook, schedules, Softr Tables and Softr Apps
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 everything else. Other integrations stay in the studio. Each entry points to its public docs page.
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 | What it does |
230
- | ----------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
231
- | `db list` | The databases the token reaches, across all workspaces, with their workspace and number of tables. |
232
- | `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. |
233
- | `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`. |
234
- | `db update [<databaseId>] [--name <name>] [--description <text>] [--dir <path>]` | Change the name, the description or both. |
235
- | `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. |
236
- | `db tables list` / `get <table>` | The tables of the database; one table with its fields (ids, types, primary/required/read-only). |
237
- | `db tables create <name> [--field <name:TYPE>]... [--primary-field <name>]` | Create a table with at least one `--field`; the first one names a record. |
238
- | `db tables update <table> [--name] [--description]` / `delete <table> [--force]` | Rename or describe a table; delete it (`--force` when it still has records). |
239
- | `db fields get <table> <field>` | One field with its options. |
240
- | `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). |
241
- | `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. |
242
- | `db fields delete <table> <field>` | Delete a field and its values in every record. |
243
- | `db views list <table>` | The views of a table (the API reads views only). |
244
- | `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. |
245
- | `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. |
246
- | `db records get <table> <recordId>` | One record. |
247
- | `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 `-`. |
248
- | `db records delete <table> <recordId>` | Delete a record. |
249
- | `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`. |
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 findCatalogEntry(type, kind) {
6073
+ function catalogEntryOrNull(type, kind) {
6074
6074
  const wanted = type.trim().toUpperCase();
6075
- const entry = CATALOG.find((candidate) => candidate.type === wanted && (!kind || candidate.kind === kind));
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 a node type this CLI offers${kind ? ` as a ${kind}` : ""}.`,
6080
- `Available: ${known}. The CLI is limited to code-first workflows on purpose; other integrations are configured in the studio.`
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("<type>", 'the new type from "spec", of the same kind (trigger or action)').option("--lang <language>", "for CUSTOM_CODE: JAVASCRIPT or PYTHON (default JAVASCRIPT)").option("--dir <path>", "workflow directory (default: the current directory)").action(async (nodeId, type, opts, command) => {
14350
- const options = globalOptions(command);
14351
- const dir = await findWorkflowDir(process.cwd(), opts.dir);
14352
- const config = await readConfig(dir);
14353
- const location = locateNode(config, nodeId);
14354
- if (!location) {
14355
- throw new CliError(`No node "${nodeId}" in softr-workflows.jsonc.`);
14356
- }
14357
- const entry = findCatalogEntry(type, location.kind);
14358
- if (location.node.type === LOOP_TYPE && entry.type !== LOOP_TYPE) {
14359
- throw new CliError(`"${nodeId}" is a loop with a body; remove it instead of replacing it.`);
14360
- }
14361
- const title = location.node.title ?? entry.label;
14362
- const { inputs, codeFile } = await newNodeInputs(entry, nodeId, title, opts.lang, dir);
14363
- const node = { ...location.node, type: entry.type, version: entry.version, inputs };
14364
- delete node["outputSchema"];
14365
- await editConfig(dir, [{ path: location.path, value: node }]);
14366
- const oldCode = location.node.type === CUSTOM_CODE_TYPE && isFileRef(location.node.inputs?.["code"]) ? location.node.inputs?.["code"] : void 0;
14367
- const notes = [
14368
- oldCode && oldCode !== inputs["code"] ? `The previous code file (${oldCode}) is left in place; delete it when you no longer need it.` : "",
14369
- entry.kind === "trigger" && (config.triggers?.length ?? 0) > 1 ? "The service accepts one trigger per published workflow for now." : "",
14370
- `Fill inputs.${Object.keys(entry.template)[0] ?? "\u2026"} etc. in softr-workflows.jsonc (see "spec ${entry.type}"), then push and test ${nodeId}.`
14371
- ].filter(Boolean);
14372
- emitWithVersionNote(
14373
- options,
14374
- await readConfig(dir),
14375
- `Replaced "${nodeId}" (${location.node.type}) with ${entry.type}; paths kept.
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
- id: nodeId,
14379
- from: location.node.type,
14380
- to: entry.type,
14381
- codeFile: codeFile ? path5.relative(process.cwd(), codeFile) : null
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("<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) => {
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 = findCatalogEntry(type);
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
- text = await readFile8(value.slice(1), "utf8");
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
- 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);
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 findFields(context, table, references, fresh = false) {
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 && !fresh) {
15757
- current2 = await findTable(context, table.id, true);
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 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) };
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 findTable(context, reference, true);
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
- `a field to create with it, repeatable (types and their options: ${FIELD_REFERENCE_URL}); add fields with options through "softr db fields add"`,
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
- if (!opts.field.length) {
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
- "Repeat --field for more; the first one names a record unless --primary-field names another."
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 --field names.`);
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 findTable(context, reference, true);
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 findTable(context, reference, true);
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, true);
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 findTable(context, tableReference);
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, true);
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, true);
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 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
- }
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 findTable(context, reference);
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 findTable(context, reference), [...references]);
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 findTable(context, reference);
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 findTable(context, reference);
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 findTable(context, reference);
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 findTable(context, reference, true);
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 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.`);
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, fieldId, {
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
- "This CLI offers only these node types on purpose: code-first workflows use custom code for everything else.",
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("List the node types this CLI offers, or show the inputs, output and an example of one").argument("[type]", "a node type such as CUSTOM_CODE").action((type, _opts, command) => {
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 = findCatalogEntry(type);
17739
- emit(options, renderEntry(entry), { ...entry, testable: isTestableType(entry.type) });
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.6-se21219.17",
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.6-se21219.17",
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",