@softr/cli 0.1.5 → 0.1.6

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 +8 -0
  2. package/dist/cli.mjs +506 -98
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -241,10 +241,18 @@ crm/
241
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
242
  | `db fields delete <table> <field>` | Delete a field and its values in every record. |
243
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`. |
244
250
 
245
251
  Tables and fields take an id or an exact name; a name two items share is an error that lists their ids. The table,
246
252
  field and view commands take the database from `--database <id>`, else `SOFTR_DATABASE_ID`, else the database
247
253
  directory, and keep its cache current; a delete always resolves the name on the server, never from the cache.
254
+ Records are keyed by field name in the output and take field names in `--fields`, filters and `--sort`; the CLI maps
255
+ them to ids, which the API needs. `--field-ids` keys the output by id. Records are never written to disk.
248
256
 
249
257
  ## Agent skills
250
258
 
package/dist/cli.mjs CHANGED
@@ -2661,8 +2661,8 @@ var Option = class {
2661
2661
  * @param {(string | string[])} names
2662
2662
  * @return {Option}
2663
2663
  */
2664
- conflicts(names) {
2665
- this.conflictsWith = this.conflictsWith.concat(names);
2664
+ conflicts(names2) {
2665
+ this.conflictsWith = this.conflictsWith.concat(names2);
2666
2666
  return this;
2667
2667
  }
2668
2668
  /**
@@ -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(names) {
3272
- names.trim().split(/ +/).forEach((detail) => {
3271
+ arguments(names2) {
3272
+ names2.trim().split(/ +/).forEach((detail) => {
3273
3273
  this.argument(detail);
3274
3274
  });
3275
3275
  return this;
@@ -4951,9 +4951,9 @@ Expecting one of '${allowedValues.join("', '")}'`);
4951
4951
  * @param {string} [path]
4952
4952
  * @return {(string|null|Command)}
4953
4953
  */
4954
- executableDir(path15) {
4955
- if (path15 === void 0) return this._executableDir;
4956
- this._executableDir = path15;
4954
+ executableDir(path16) {
4955
+ if (path16 === void 0) return this._executableDir;
4956
+ this._executableDir = path16;
4957
4957
  return this;
4958
4958
  }
4959
4959
  /**
@@ -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 path15 of pathSets.flat()) {
5314
- const to = path15.toActionId;
5315
- if (!to || !nodes.has(path15.fromActionId) || !nodes.has(to) || outgoing.get(path15.fromActionId).includes(to)) {
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(path15.fromActionId).push(to);
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);
@@ -6130,43 +6130,45 @@ var BASE_URLS = {
6130
6130
  studio: (target) => target.studioApiUrl,
6131
6131
  tables: (target) => target.tablesApiUrl
6132
6132
  };
6133
- async function send(client, service, method, path15, body) {
6133
+ async function send(client, service, method, path16, body) {
6134
6134
  const init = {
6135
6135
  method,
6136
6136
  headers: { ...authHeaders(client, service), Accept: "application/json" }
6137
6137
  };
6138
- if (body !== void 0) {
6138
+ if (body instanceof FormData) {
6139
+ init.body = body;
6140
+ } else if (body !== void 0) {
6139
6141
  init.headers = { ...init.headers, "Content-Type": "application/json" };
6140
6142
  init.body = JSON.stringify(body);
6141
6143
  }
6142
- const response = await fetch(`${BASE_URLS[service](client.target)}${path15}`, init);
6144
+ const response = await fetch(`${BASE_URLS[service](client.target)}${path16}`, init);
6143
6145
  if (response.ok) {
6144
6146
  return response;
6145
6147
  }
6146
6148
  const text = await response.text();
6147
6149
  throw new SoftrApiError(
6148
- `${method} ${path15} failed with status ${response.status}${text ? `: ${text}` : ""}`,
6150
+ `${method} ${path16} failed with status ${response.status}${text ? `: ${text}` : ""}`,
6149
6151
  response.status,
6150
6152
  text,
6151
6153
  hintFor(response.status, client, service)
6152
6154
  );
6153
6155
  }
6154
- async function apiRequest(client, method, path15, body) {
6155
- const response = await send(client, "workflows", method, path15, body);
6156
+ async function apiRequest(client, method, path16, body) {
6157
+ const response = await send(client, "workflows", method, path16, body);
6156
6158
  return await response.json();
6157
6159
  }
6158
- async function apiRequestVoid(client, method, path15, body) {
6159
- await send(client, "workflows", method, path15, body);
6160
+ async function apiRequestVoid(client, method, path16, body) {
6161
+ await send(client, "workflows", method, path16, body);
6160
6162
  }
6161
- async function tablesRequest(client, method, path15, body) {
6162
- const response = await send(client, "tables", method, path15, body);
6163
+ async function tablesRequest(client, method, path16, body) {
6164
+ const response = await send(client, "tables", method, path16, body);
6163
6165
  return await response.json();
6164
6166
  }
6165
- async function tablesRequestVoid(client, method, path15, body) {
6166
- await send(client, "tables", method, path15, body);
6167
+ async function tablesRequestVoid(client, method, path16, body) {
6168
+ await send(client, "tables", method, path16, body);
6167
6169
  }
6168
- async function studioRequest(client, method, path15, body) {
6169
- const response = await send(client, "studio", method, path15, body);
6170
+ async function studioRequest(client, method, path16, body) {
6171
+ const response = await send(client, "studio", method, path16, body);
6170
6172
  return await response.json();
6171
6173
  }
6172
6174
 
@@ -6992,12 +6994,12 @@ function parseTree(text, errors = [], options = ParseOptions.DEFAULT) {
6992
6994
  }
6993
6995
  return result;
6994
6996
  }
6995
- function findNodeAtLocation(root, path15) {
6997
+ function findNodeAtLocation(root, path16) {
6996
6998
  if (!root) {
6997
6999
  return void 0;
6998
7000
  }
6999
7001
  let node = root;
7000
- for (let segment of path15) {
7002
+ for (let segment of path16) {
7001
7003
  if (typeof segment === "string") {
7002
7004
  if (node.type !== "object" || !Array.isArray(node.children)) {
7003
7005
  return void 0;
@@ -7351,14 +7353,14 @@ function getNodeType(value) {
7351
7353
 
7352
7354
  // node_modules/jsonc-parser/lib/esm/impl/edit.js
7353
7355
  function setProperty(text, originalPath, value, options) {
7354
- const path15 = originalPath.slice();
7356
+ const path16 = originalPath.slice();
7355
7357
  const errors = [];
7356
7358
  const root = parseTree(text, errors);
7357
7359
  let parent = void 0;
7358
7360
  let lastSegment = void 0;
7359
- while (path15.length > 0) {
7360
- lastSegment = path15.pop();
7361
- parent = findNodeAtLocation(root, path15);
7361
+ while (path16.length > 0) {
7362
+ lastSegment = path16.pop();
7363
+ parent = findNodeAtLocation(root, path16);
7362
7364
  if (parent === void 0 && value !== void 0) {
7363
7365
  if (typeof lastSegment === "string") {
7364
7366
  value = { [lastSegment]: value };
@@ -7581,8 +7583,8 @@ function printParseErrorCode(code) {
7581
7583
  }
7582
7584
  return "<unknown ParseErrorCode>";
7583
7585
  }
7584
- function modify(text, path15, value, options) {
7585
- return setProperty(text, path15, value, options);
7586
+ function modify(text, path16, value, options) {
7587
+ return setProperty(text, path16, value, options);
7586
7588
  }
7587
7589
  function applyEdits(text, edits) {
7588
7590
  let sortedEdits = edits.slice(0).sort((a, b) => {
@@ -13593,8 +13595,8 @@ function typeOfField(field) {
13593
13595
  return field.allowMultipleEntries ? arrayOf(base) : base;
13594
13596
  }
13595
13597
  var SEGMENT = /^(?:\.?\[\s*'((?:[^'\\]|\\.)*)'\s*\]|\.?\[\s*"((?:[^"\\]|\\.)*)"\s*\]|\.?\[\s*(\d+)\s*\]|\.?\[\s*\*\s*\]|\.([^.[\]]+))/;
13596
- function parsePath(path15) {
13597
- let rest = path15.trim();
13598
+ function parsePath(path16) {
13599
+ let rest = path16.trim();
13598
13600
  if (rest.startsWith("$")) {
13599
13601
  rest = rest.slice(1);
13600
13602
  }
@@ -14094,10 +14096,10 @@ function describePlaceholder(placeholder, raw, config, outputs, depth) {
14094
14096
  function untested(nodeId) {
14095
14097
  return `node "${nodeId}" has no test output yet (run: softr workflows test ${nodeId})`;
14096
14098
  }
14097
- function describeOutputRef(nodeId, path15, source, outputs) {
14098
- const segments = parsePath(path15);
14099
+ function describeOutputRef(nodeId, path16, source, outputs) {
14100
+ const segments = parsePath(path16);
14099
14101
  if (!segments) {
14100
- return { type: UNKNOWN, source, verified: false, note: `unsupported path "${path15}"` };
14102
+ return { type: UNKNOWN, source, verified: false, note: `unsupported path "${path16}"` };
14101
14103
  }
14102
14104
  const row = outputs[nodeId];
14103
14105
  if (!row) {
@@ -14608,6 +14610,73 @@ async function deleteField(client, databaseId, tableId, fieldId) {
14608
14610
  async function listViews(client, databaseId, tableId) {
14609
14611
  return (await tablesRequest(client, "GET", `${tablePath(databaseId, tableId)}/views`)).data;
14610
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;
14679
+ }
14611
14680
 
14612
14681
  // src/config/credentials.ts
14613
14682
  import { chmod, mkdir as mkdir4, readFile as readFile5, writeFile as writeFile5 } from "node:fs/promises";
@@ -14645,8 +14714,8 @@ function refreshFailureHint(status) {
14645
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.";
14646
14715
  }
14647
14716
  async function tokenRequest(target, form) {
14648
- const path15 = "/v1/oauth/token";
14649
- const response = await fetch(`${target.studioApiUrl}${path15}`, {
14717
+ const path16 = "/v1/oauth/token";
14718
+ const response = await fetch(`${target.studioApiUrl}${path16}`, {
14650
14719
  method: "POST",
14651
14720
  headers: { "Content-Type": "application/x-www-form-urlencoded", Accept: "application/json" },
14652
14721
  body: new URLSearchParams({ ...form, client_id: oauthClientId() }).toString()
@@ -14654,7 +14723,7 @@ async function tokenRequest(target, form) {
14654
14723
  if (!response.ok) {
14655
14724
  const text = await response.text();
14656
14725
  throw new SoftrApiError(
14657
- `POST ${path15} failed with status ${response.status}${text ? `: ${text}` : ""}`,
14726
+ `POST ${path16} failed with status ${response.status}${text ? `: ${text}` : ""}`,
14658
14727
  response.status,
14659
14728
  text,
14660
14729
  form["grant_type"] === "refresh_token" ? refreshFailureHint(response.status) : void 0
@@ -14898,15 +14967,25 @@ async function writeSchemaCache(dir, database, tables) {
14898
14967
  function viewsDir(dir) {
14899
14968
  return path8.join(dir, DB_CACHE_DIR_NAME, "views");
14900
14969
  }
14970
+ async function readViewsCache(dir, tableId) {
14971
+ try {
14972
+ return JSON.parse(await readFile6(path8.join(viewsDir(dir), `${tableId}.json`), "utf8"));
14973
+ } catch (error) {
14974
+ if (error.code === "ENOENT") {
14975
+ return void 0;
14976
+ }
14977
+ throw error;
14978
+ }
14979
+ }
14901
14980
  async function writeViewsCache(dir, tableId, views) {
14902
14981
  const value = { tableId, views, refreshedAt: (/* @__PURE__ */ new Date()).toISOString() };
14903
14982
  await mkdir5(viewsDir(dir), { recursive: true });
14904
14983
  await writeFile6(path8.join(viewsDir(dir), `${tableId}.json`), JSON.stringify(value, null, 2) + "\n");
14905
14984
  }
14906
14985
  async function pruneViewsCache(dir, tableIds) {
14907
- let names;
14986
+ let names2;
14908
14987
  try {
14909
- names = await readdir2(viewsDir(dir));
14988
+ names2 = await readdir2(viewsDir(dir));
14910
14989
  } catch (error) {
14911
14990
  if (error.code === "ENOENT") {
14912
14991
  return;
@@ -14914,7 +14993,7 @@ async function pruneViewsCache(dir, tableIds) {
14914
14993
  throw error;
14915
14994
  }
14916
14995
  await Promise.all(
14917
- names.filter((name) => name.endsWith(".json") && !tableIds.has(name.slice(0, -".json".length))).map((name) => rm(path8.join(viewsDir(dir), name), { force: true }))
14996
+ names2.filter((name) => name.endsWith(".json") && !tableIds.has(name.slice(0, -".json".length))).map((name) => rm(path8.join(viewsDir(dir), name), { force: true }))
14918
14997
  );
14919
14998
  }
14920
14999
  async function refreshSchemaCache(client, dir, databaseId) {
@@ -15603,8 +15682,9 @@ ${path11.join(relativeDir(process.cwd(), target.dir), DB_CONFIG_FILE_NAME)} stil
15603
15682
  });
15604
15683
  }
15605
15684
 
15606
- // src/commands/db/schema.ts
15607
- var import_cli_table32 = __toESM(require_cli_table3(), 1);
15685
+ // src/commands/db/records.ts
15686
+ import { readFile as readFile9 } from "node:fs/promises";
15687
+ import path12 from "node:path";
15608
15688
 
15609
15689
  // src/commands/db/json-input.ts
15610
15690
  import { readFile as readFile8 } from "node:fs/promises";
@@ -15617,7 +15697,11 @@ async function readJsonArgument(value, option, hint) {
15617
15697
  }
15618
15698
  text = Buffer.concat(chunks).toString("utf8");
15619
15699
  } else if (value.startsWith("@")) {
15620
- text = await readFile8(value.slice(1), "utf8");
15700
+ try {
15701
+ text = await readFile8(value.slice(1), "utf8");
15702
+ } catch {
15703
+ throw new CliError(`Cannot read ${value.slice(1)} for ${option}.`);
15704
+ }
15621
15705
  }
15622
15706
  try {
15623
15707
  return JSON.parse(text);
@@ -15627,6 +15711,7 @@ async function readJsonArgument(value, option, hint) {
15627
15711
  }
15628
15712
 
15629
15713
  // src/commands/db/schema.ts
15714
+ var import_cli_table32 = __toESM(require_cli_table3(), 1);
15630
15715
  var FIELD_REFERENCE_URL = "https://docs.softr.io/softr-api/softr-database-api/table-fields/add-table-field";
15631
15716
  var TABLE_ARGUMENT = "the table id or exact name";
15632
15717
  var FIELD_ARGUMENT = "the field id or exact name";
@@ -15666,6 +15751,50 @@ async function findTableCached(context, reference) {
15666
15751
  const cached = context.dir && context.cache ? byIdOrName(context.cache.tables, reference, "table") : void 0;
15667
15752
  return cached ?? findTableOnServer(context, reference);
15668
15753
  }
15754
+ async function findFields(context, table, references) {
15755
+ const resolve = (current3) => {
15756
+ const fields2 = /* @__PURE__ */ new Map();
15757
+ for (const reference of references) {
15758
+ const field = byIdOrName(current3.fields, reference, "field");
15759
+ if (field) {
15760
+ fields2.set(reference, field);
15761
+ }
15762
+ }
15763
+ return fields2;
15764
+ };
15765
+ let current2 = table;
15766
+ let fields = resolve(current2);
15767
+ if (fields.size < new Set(references).size && context.dir) {
15768
+ current2 = await findTableOnServer(context, table.id);
15769
+ fields = resolve(current2);
15770
+ }
15771
+ const missing = references.filter((reference) => !fields.has(reference));
15772
+ if (missing.length) {
15773
+ throw new CliError(
15774
+ `Table ${current2.name} has no field with the id or name ${missing.map((name) => `"${name}"`).join(", ")}.`,
15775
+ `"softr db tables get ${current2.id}" shows its fields.`
15776
+ );
15777
+ }
15778
+ return { table: current2, fields };
15779
+ }
15780
+ async function readViews(context, table) {
15781
+ const views = await listViews(context.client, context.databaseId, table.id);
15782
+ if (context.dir) {
15783
+ await writeViewsCache(context.dir, table.id, views);
15784
+ }
15785
+ return views;
15786
+ }
15787
+ async function findView(context, table, reference) {
15788
+ const cached = context.dir ? (await readViewsCache(context.dir, table.id))?.views : void 0;
15789
+ const view = (cached ? byIdOrName(cached, reference, "view") : void 0) ?? byIdOrName(await readViews(context, table), reference, "view");
15790
+ if (!view) {
15791
+ throw new CliError(
15792
+ `Table ${table.name} has no view with the id or name "${reference}".`,
15793
+ `"softr db views list ${table.id}" shows its views.`
15794
+ );
15795
+ }
15796
+ return view;
15797
+ }
15669
15798
  async function findField(context, tableReference, reference) {
15670
15799
  const table = await findTableOnServer(context, tableReference);
15671
15800
  const field = byIdOrName(table.fields, reference, "field");
@@ -15682,15 +15811,8 @@ async function afterSchemaWrite(context) {
15682
15811
  context.cache = await refreshSchemaCache(context.client, context.dir, context.databaseId);
15683
15812
  }
15684
15813
  }
15685
- function parseOptions(json) {
15686
- if (json === void 0) {
15687
- return void 0;
15688
- }
15689
- try {
15690
- return JSON.parse(json);
15691
- } catch {
15692
- throw new CliError("--options is not valid JSON.", `The options of each field type: ${FIELD_REFERENCE_URL}`);
15693
- }
15814
+ function parseOptions(value) {
15815
+ return value === void 0 ? Promise.resolve(void 0) : readJsonArgument(value, "--options", `The options of each field type: ${FIELD_REFERENCE_URL}`);
15694
15816
  }
15695
15817
  function parseFieldSpec(spec) {
15696
15818
  const colon = spec.lastIndexOf(":");
@@ -15860,11 +15982,11 @@ function registerFields(db2) {
15860
15982
  emit(options, renderField(table, field), { tableId: table.id, field });
15861
15983
  });
15862
15984
  withDatabaseOptions(
15863
- 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 (${FIELD_REFERENCE_URL})`).option("--description <text>", "what the field holds").option("--multiple", "allow several values (SELECT, LINKED_RECORD, USER, ATTACHMENT)")
15985
+ 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)")
15864
15986
  ).action(
15865
15987
  async (tableReference, name, opts, command) => {
15866
15988
  const options = globalOptions(command);
15867
- const fieldOptions = parseOptions(opts.options);
15989
+ const fieldOptions = await parseOptions(opts.options);
15868
15990
  const context = await schemaContext(opts);
15869
15991
  const table = await findTableCached(context, tableReference);
15870
15992
  const field = await addField(context.client, context.databaseId, table.id, {
@@ -15882,10 +16004,13 @@ function registerFields(db2) {
15882
16004
  }
15883
16005
  );
15884
16006
  withDatabaseOptions(
15885
- 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("--options <json>", `the new options as JSON, replacing the current ones (${FIELD_REFERENCE_URL})`).option("--description <text>", "the new description").option("--multiple", "allow several values").option("--no-multiple", "allow one value only")
16007
+ 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(
16008
+ "--options <json>",
16009
+ `the new options as JSON, @<file> or - for stdin, replacing the current ones (${FIELD_REFERENCE_URL})`
16010
+ ).option("--description <text>", "the new description").option("--multiple", "allow several values").option("--no-multiple", "allow one value only")
15886
16011
  ).action(
15887
16012
  async (tableReference, reference, opts, command) => {
15888
- const fieldOptions = parseOptions(opts.options);
16013
+ const fieldOptions = await parseOptions(opts.options);
15889
16014
  if (!opts.name && !opts.type && fieldOptions === void 0 && opts.description === void 0 && opts.multiple === void 0) {
15890
16015
  throw new CliError("Nothing to change: pass --name, --type, --options, --description or --[no-]multiple.");
15891
16016
  }
@@ -15930,17 +16055,299 @@ function registerViews(db2) {
15930
16055
  const options = globalOptions(command);
15931
16056
  const context = await schemaContext(opts);
15932
16057
  const table = await findTableOnServer(context, reference);
15933
- const items = await listViews(context.client, context.databaseId, table.id);
15934
- if (context.dir) {
15935
- await writeViewsCache(context.dir, table.id, items);
15936
- }
16058
+ const items = await readViews(context, table);
15937
16059
  emit(options, renderViews(items), { tableId: table.id, views: items });
15938
16060
  });
15939
16061
  }
15940
16062
 
16063
+ // src/commands/db/records.ts
16064
+ var SEARCH_DOCS_URL = "https://docs.softr.io/softr-api/softr-database-api/records/search-records";
16065
+ var MAX_PAGE = 200;
16066
+ function withOutputOptions(command) {
16067
+ return command.option("--field-ids", "key record fields by field id instead of field name");
16068
+ }
16069
+ function withPagingOptions(command) {
16070
+ 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}`);
16071
+ }
16072
+ function parseCount(value, option, max) {
16073
+ if (value === void 0) {
16074
+ return void 0;
16075
+ }
16076
+ const count = Number(value);
16077
+ if (!Number.isInteger(count) || count < 0 || max !== void 0 && (count < 1 || count > max)) {
16078
+ throw new CliError(`${option} must be a whole number${max ? ` from 1 to ${max}` : " of 0 or more"}.`);
16079
+ }
16080
+ return count;
16081
+ }
16082
+ function paging(opts) {
16083
+ if (opts.all && opts.limit !== void 0) {
16084
+ throw new CliError("Pass --all or --limit, not both.");
16085
+ }
16086
+ return {
16087
+ offset: parseCount(opts.offset, "--offset") ?? 0,
16088
+ limit: opts.all ? MAX_PAGE : parseCount(opts.limit, "--limit", MAX_PAGE) ?? 20,
16089
+ all: opts.all === true
16090
+ };
16091
+ }
16092
+ async function readPages(first, read) {
16093
+ const page = await read(first.offset, first.limit);
16094
+ if (!first.all) {
16095
+ return page;
16096
+ }
16097
+ const records = [...page.records];
16098
+ let offset2 = first.offset + page.records.length;
16099
+ while (page.records.length && offset2 < page.metadata.total) {
16100
+ const next = await read(offset2, first.limit);
16101
+ if (!next.records.length) {
16102
+ break;
16103
+ }
16104
+ records.push(...next.records);
16105
+ offset2 += next.records.length;
16106
+ }
16107
+ return { records, metadata: { offset: first.offset, limit: records.length, total: page.metadata.total } };
16108
+ }
16109
+ function asObject(value, option) {
16110
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
16111
+ throw new CliError(`${option} must be a JSON object.`);
16112
+ }
16113
+ return value;
16114
+ }
16115
+ async function fieldIdsFor(context, table, references) {
16116
+ const found = await findFields(context, table, references);
16117
+ return { table: found.table, ids: new Map([...found.fields].map(([reference, field]) => [reference, field.id])) };
16118
+ }
16119
+ async function mapFieldKeys(context, table, fields) {
16120
+ const { ids } = await fieldIdsFor(context, table, Object.keys(fields));
16121
+ return Object.fromEntries(Object.entries(fields).map(([key, value]) => [ids.get(key), value]));
16122
+ }
16123
+ function fieldReferences(condition, found) {
16124
+ if (Array.isArray(condition)) {
16125
+ condition.forEach((item) => fieldReferences(item, found));
16126
+ } else if (condition && typeof condition === "object") {
16127
+ for (const [key, value] of Object.entries(condition)) {
16128
+ if (key === "leftSide" && typeof value === "string") {
16129
+ found.add(value);
16130
+ } else {
16131
+ fieldReferences(value, found);
16132
+ }
16133
+ }
16134
+ }
16135
+ }
16136
+ function replaceFieldReferences(condition, ids) {
16137
+ if (Array.isArray(condition)) {
16138
+ return condition.map((item) => replaceFieldReferences(item, ids));
16139
+ }
16140
+ if (condition && typeof condition === "object") {
16141
+ return Object.fromEntries(
16142
+ Object.entries(condition).map(([key, value]) => [
16143
+ key,
16144
+ key === "leftSide" && typeof value === "string" ? ids.get(value) ?? value : replaceFieldReferences(value, ids)
16145
+ ])
16146
+ );
16147
+ }
16148
+ return condition;
16149
+ }
16150
+ function parseSort(spec) {
16151
+ const colon = spec.lastIndexOf(":");
16152
+ const direction = colon > 0 ? spec.slice(colon + 1).trim().toUpperCase() : "";
16153
+ if (direction === "ASC" || direction === "DESC") {
16154
+ return { field: spec.slice(0, colon).trim(), sortType: direction };
16155
+ }
16156
+ return { field: spec.trim(), sortType: "ASC" };
16157
+ }
16158
+ function withFieldNames(record, table) {
16159
+ const names2 = new Map(table.fields.map((field) => [field.id, field.name]));
16160
+ return {
16161
+ ...record,
16162
+ fields: Object.fromEntries(Object.entries(record.fields).map(([key, value]) => [names2.get(key) ?? key, value]))
16163
+ };
16164
+ }
16165
+ function formatValue(value) {
16166
+ return typeof value === "string" ? value : JSON.stringify(value);
16167
+ }
16168
+ function renderRecord(record) {
16169
+ return [record.id, ...Object.entries(record.fields).map(([key, value]) => ` ${key}: ${formatValue(value)}`)].join(
16170
+ "\n"
16171
+ );
16172
+ }
16173
+ function renderPage(page) {
16174
+ if (!page.records.length) {
16175
+ return page.metadata.total ? `No records from offset ${page.metadata.offset}; ${page.metadata.total} in all.` : "No records.";
16176
+ }
16177
+ const shown = page.records.length;
16178
+ const end = page.metadata.offset + shown;
16179
+ 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.`;
16180
+ return [...page.records.map(renderRecord), footer].join("\n\n");
16181
+ }
16182
+ function registerRecords(db2) {
16183
+ const records = db2.command("records").description("The records of a table; fields are keyed by name unless --field-ids");
16184
+ withOutputOptions(
16185
+ withPagingOptions(
16186
+ withDatabaseOptions(
16187
+ records.command("list").description(
16188
+ `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`
16189
+ ).argument("<table>", TABLE_ARGUMENT).option("--view <view>", "the view id or exact name (default: the table's default view)")
16190
+ )
16191
+ )
16192
+ ).action(
16193
+ async (reference, opts, command) => {
16194
+ const options = globalOptions(command);
16195
+ const pages = paging(opts);
16196
+ const context = await schemaContext(opts);
16197
+ const table = await findTableCached(context, reference);
16198
+ const viewId = opts.view ? (await findView(context, table, opts.view)).id : void 0;
16199
+ const page = await readPages(
16200
+ pages,
16201
+ (offset2, limit) => listRecords(context.client, context.databaseId, table.id, {
16202
+ viewId,
16203
+ offset: offset2,
16204
+ limit,
16205
+ fieldNames: !opts.fieldIds
16206
+ })
16207
+ );
16208
+ emit(options, renderPage(page), { tableId: table.id, ...page });
16209
+ }
16210
+ );
16211
+ withOutputOptions(
16212
+ withPagingOptions(
16213
+ withDatabaseOptions(
16214
+ records.command("search").description("Find records by a filter, sorted by fields").argument("<table>", TABLE_ARGUMENT).option(
16215
+ "--filter <json>",
16216
+ `the condition as JSON, @<file> or - for stdin; leftSide takes a field name or id (operators: ${SEARCH_DOCS_URL})`
16217
+ ).option(
16218
+ "--sort <field[:asc|desc]>",
16219
+ "sort by a field, repeatable",
16220
+ (value, previous) => [...previous, value],
16221
+ []
16222
+ )
16223
+ )
16224
+ )
16225
+ ).action(
16226
+ async (reference, opts, command) => {
16227
+ const options = globalOptions(command);
16228
+ const pages = paging(opts);
16229
+ const parsed = opts.filter === void 0 ? void 0 : asObject(await readJsonArgument(opts.filter, "--filter"), "--filter");
16230
+ const condition = parsed && "condition" in parsed ? parsed["condition"] : parsed;
16231
+ const sorts = opts.sort.map(parseSort);
16232
+ const context = await schemaContext(opts);
16233
+ const references = new Set(sorts.map((sort) => sort.field));
16234
+ fieldReferences(condition, references);
16235
+ const { table, ids } = await fieldIdsFor(context, await findTableCached(context, reference), [...references]);
16236
+ const search = (offset2, limit) => ({
16237
+ paging: { offset: offset2, limit },
16238
+ ...sorts.length ? { sorting: sorts.map((sort) => ({ sortingField: ids.get(sort.field), sortType: sort.sortType })) } : {},
16239
+ ...condition === void 0 ? {} : { filter: { condition: replaceFieldReferences(condition, ids) } }
16240
+ });
16241
+ const page = await readPages(
16242
+ pages,
16243
+ (offset2, limit) => searchRecords(context.client, context.databaseId, table.id, search(offset2, limit), !opts.fieldIds)
16244
+ );
16245
+ emit(options, renderPage(page), { tableId: table.id, ...page });
16246
+ }
16247
+ );
16248
+ withOutputOptions(
16249
+ withDatabaseOptions(
16250
+ records.command("get").description("Show one record").argument("<table>", TABLE_ARGUMENT).argument("<recordId>", "the record id")
16251
+ )
16252
+ ).action(async (reference, recordId, opts, command) => {
16253
+ const options = globalOptions(command);
16254
+ const context = await schemaContext(opts);
16255
+ const table = await findTableCached(context, reference);
16256
+ const record = await getRecord(context.client, context.databaseId, table.id, recordId, !opts.fieldIds);
16257
+ emit(options, renderRecord(record), { tableId: table.id, record });
16258
+ });
16259
+ withOutputOptions(
16260
+ withDatabaseOptions(
16261
+ records.command("create").description("Create a record").argument("<table>", TABLE_ARGUMENT).requiredOption(
16262
+ "--fields <json>",
16263
+ "the values as a JSON object keyed by field name or id, @<file> or - for stdin; {} creates an empty record"
16264
+ )
16265
+ )
16266
+ ).action(async (reference, opts, command) => {
16267
+ const options = globalOptions(command);
16268
+ const fields = asObject(await readJsonArgument(opts.fields, "--fields"), "--fields");
16269
+ const context = await schemaContext(opts);
16270
+ const table = await findTableCached(context, reference);
16271
+ const record = await createRecord(
16272
+ context.client,
16273
+ context.databaseId,
16274
+ table.id,
16275
+ await mapFieldKeys(context, table, fields),
16276
+ !opts.fieldIds
16277
+ );
16278
+ emit(options, `Created record ${record.id} in ${table.name}.`, { tableId: table.id, record });
16279
+ });
16280
+ withOutputOptions(
16281
+ withDatabaseOptions(
16282
+ 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(
16283
+ "--fields <json>",
16284
+ "the new values as a JSON object keyed by field name or id, @<file> or - for stdin; null clears a field"
16285
+ )
16286
+ )
16287
+ ).action(
16288
+ async (reference, recordId, opts, command) => {
16289
+ const options = globalOptions(command);
16290
+ const fields = asObject(await readJsonArgument(opts.fields, "--fields"), "--fields");
16291
+ if (!Object.keys(fields).length) {
16292
+ throw new CliError("Nothing to change: --fields is an empty object.");
16293
+ }
16294
+ const context = await schemaContext(opts);
16295
+ const table = await findTableCached(context, reference);
16296
+ const record = await updateRecord(
16297
+ context.client,
16298
+ context.databaseId,
16299
+ table.id,
16300
+ recordId,
16301
+ await mapFieldKeys(context, table, fields),
16302
+ !opts.fieldIds
16303
+ );
16304
+ emit(options, `Updated record ${record.id} in ${table.name}.`, { tableId: table.id, record });
16305
+ }
16306
+ );
16307
+ withDatabaseOptions(
16308
+ records.command("delete").description("Delete a record").argument("<table>", TABLE_ARGUMENT).argument("<recordId>", "the record id")
16309
+ ).action(async (reference, recordId, opts, command) => {
16310
+ const options = globalOptions(command);
16311
+ const context = await schemaContext(opts);
16312
+ const table = await findTableOnServer(context, reference);
16313
+ await deleteRecord(context.client, context.databaseId, table.id, recordId);
16314
+ emit(options, `Deleted record ${recordId} from ${table.name}.`, { tableId: table.id, deleted: recordId });
16315
+ });
16316
+ withOutputOptions(
16317
+ withDatabaseOptions(
16318
+ 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)")
16319
+ )
16320
+ ).action(
16321
+ async (reference, recordId, fieldReference, file, opts, command) => {
16322
+ const options = globalOptions(command);
16323
+ let content;
16324
+ try {
16325
+ content = await readFile9(file);
16326
+ } catch {
16327
+ throw new CliError(`Cannot read ${file}.`);
16328
+ }
16329
+ const context = await schemaContext(opts);
16330
+ const found = await findFields(context, await findTableCached(context, reference), [fieldReference]);
16331
+ const field = found.fields.get(fieldReference);
16332
+ if (field.type !== "ATTACHMENT") {
16333
+ throw new CliError(`Field "${fieldReference}" has the type ${field.type}; upload needs an ATTACHMENT field.`);
16334
+ }
16335
+ const uploaded = await uploadAttachment(context.client, context.databaseId, found.table.id, recordId, field.id, {
16336
+ name: opts.name ?? path12.basename(file),
16337
+ content
16338
+ });
16339
+ const record = opts.fieldIds ? uploaded : withFieldNames(uploaded, found.table);
16340
+ emit(options, `Uploaded ${opts.name ?? path12.basename(file)} to record ${record.id} in ${found.table.name}.`, {
16341
+ tableId: found.table.id,
16342
+ record
16343
+ });
16344
+ }
16345
+ );
16346
+ }
16347
+
15941
16348
  // src/commands/integrations.ts
15942
16349
  var import_cli_table33 = __toESM(require_cli_table3(), 1);
15943
- import path12 from "node:path";
16350
+ import path13 from "node:path";
15944
16351
  function toRow(integration) {
15945
16352
  return {
15946
16353
  id: integration.id,
@@ -15972,7 +16379,7 @@ Use the ID as the integrationId input of a node (see "spec <TYPE>").`;
15972
16379
  }
15973
16380
  async function workflowDirOf(cwd, dir) {
15974
16381
  if (dir) {
15975
- return path12.resolve(cwd, dir);
16382
+ return path13.resolve(cwd, dir);
15976
16383
  }
15977
16384
  return await configExists(cwd) ? cwd : void 0;
15978
16385
  }
@@ -16386,28 +16793,28 @@ function renderIssues(issues) {
16386
16793
 
16387
16794
  // src/project/variables.ts
16388
16795
  var MAX_VARIABLES = 200;
16389
- function appendKey(path15, key) {
16390
- return /^[A-Za-z_]\w*$/.test(key) ? `${path15}.${key}` : `${path15}['${key.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}']`;
16796
+ function appendKey(path16, key) {
16797
+ return /^[A-Za-z_]\w*$/.test(key) ? `${path16}.${key}` : `${path16}['${key.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}']`;
16391
16798
  }
16392
16799
  function isPrimitive2(value) {
16393
16800
  return value !== void 0 && value !== null && typeof value !== "object";
16394
16801
  }
16395
- function walk(type, raw, path15, token, out) {
16802
+ function walk(type, raw, path16, token, out) {
16396
16803
  if (out.length >= MAX_VARIABLES) {
16397
16804
  return;
16398
16805
  }
16399
- out.push({ variable: token(path15), path: path15, type, ...isPrimitive2(raw) ? { example: raw } : {} });
16806
+ out.push({ variable: token(path16), path: path16, type, ...isPrimitive2(raw) ? { example: raw } : {} });
16400
16807
  if (type.kind === "object") {
16401
16808
  for (const [key, field] of Object.entries(type.fields)) {
16402
16809
  const rawField = raw !== null && typeof raw === "object" && !Array.isArray(raw) ? raw[key] : void 0;
16403
- walk(field.type, rawField, appendKey(path15, key), token, out);
16810
+ walk(field.type, rawField, appendKey(path16, key), token, out);
16404
16811
  }
16405
16812
  } else if (type.kind === "array") {
16406
- walk(type.items, Array.isArray(raw) ? raw[0] : void 0, `${path15}[*]`, token, out);
16813
+ walk(type.items, Array.isArray(raw) ? raw[0] : void 0, `${path16}[*]`, token, out);
16407
16814
  }
16408
16815
  }
16409
16816
  function collectVariables(nodeId, sample, metadata) {
16410
- const token = (path15) => `{outputs.${nodeId}:::${path15}}`;
16817
+ const token = (path16) => `{outputs.${nodeId}:::${path16}}`;
16411
16818
  const variables = [];
16412
16819
  if (sample !== void 0 && sample !== null) {
16413
16820
  walk(typeOfJson(sample), sample, "$", token, variables);
@@ -16739,19 +17146,19 @@ function pathFromKeys(keys) {
16739
17146
  const only = keys[0].trim();
16740
17147
  return only.startsWith("$") ? only : `$${only.startsWith("[") ? "" : "."}${only}`;
16741
17148
  }
16742
- let path15 = "$";
17149
+ let path16 = "$";
16743
17150
  for (const key of keys) {
16744
17151
  if (key === "*") {
16745
- path15 += "[*]";
17152
+ path16 += "[*]";
16746
17153
  } else if (/^\d+$/.test(key)) {
16747
- path15 += `[${key}]`;
17154
+ path16 += `[${key}]`;
16748
17155
  } else if (/^[A-Za-z_]\w*$/.test(key)) {
16749
- path15 += `.${key}`;
17156
+ path16 += `.${key}`;
16750
17157
  } else {
16751
- path15 += `['${key.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}']`;
17158
+ path16 += `['${key.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}']`;
16752
17159
  }
16753
17160
  }
16754
- return path15;
17161
+ return path16;
16755
17162
  }
16756
17163
  function keysAt(type) {
16757
17164
  switch (type.kind) {
@@ -16776,12 +17183,12 @@ function registerPlaceholder(program3) {
16776
17183
  }
16777
17184
  const rows = await readStateFile(dir, OUTPUTS_FILE) ?? [];
16778
17185
  const outputs = indexOutputs(rows);
16779
- const path15 = pathFromKeys(keys);
16780
- if (!parsePath(path15)) {
16781
- throw new CliError(`Cannot read the path "${path15}".`);
17186
+ const path16 = pathFromKeys(keys);
17187
+ if (!parsePath(path16)) {
17188
+ throw new CliError(`Cannot read the path "${path16}".`);
16782
17189
  }
16783
17190
  const isLoop = location.node.type === LOOP_TYPE;
16784
- const token = isLoop ? `{loopActionGroup.${nodeId}:::loopVariables.items${path15 === "$" ? "" : path15.slice(1)}}` : `{outputs.${nodeId}:::${path15}}`;
17191
+ const token = isLoop ? `{loopActionGroup.${nodeId}:::loopVariables.items${path16 === "$" ? "" : path16.slice(1)}}` : `{outputs.${nodeId}:::${path16}}`;
16785
17192
  const [description] = describeInputData(
16786
17193
  { id: "probe", type: "CUSTOM_CODE", inputs: { inputData: { probe: token } } },
16787
17194
  config,
@@ -16791,7 +17198,7 @@ function registerPlaceholder(program3) {
16791
17198
  throw new CliError("Could not evaluate the token.");
16792
17199
  }
16793
17200
  if (description.type.kind === "unknown") {
16794
- const segments = parsePath(path15) ?? [];
17201
+ const segments = parsePath(path16) ?? [];
16795
17202
  let hint = description.note ?? "not found";
16796
17203
  const row = outputs[nodeId];
16797
17204
  if (row?.hasSample && segments.length) {
@@ -16817,7 +17224,7 @@ function registerPlaceholder(program3) {
16817
17224
  emit(options, `${token}
16818
17225
  type: ${type}${example3} (${status})`, {
16819
17226
  token,
16820
- path: path15,
17227
+ path: path16,
16821
17228
  type,
16822
17229
  example: description.example ?? null,
16823
17230
  verified: description.verified,
@@ -16827,10 +17234,10 @@ type: ${type}${example3} (${status})`, {
16827
17234
  }
16828
17235
 
16829
17236
  // src/commands/pull.ts
16830
- import path13 from "node:path";
17237
+ import path14 from "node:path";
16831
17238
  async function targetDir(cwd, explicitDir, workflowId, title) {
16832
17239
  if (explicitDir) {
16833
- return path13.resolve(cwd, explicitDir);
17240
+ return path14.resolve(cwd, explicitDir);
16834
17241
  }
16835
17242
  if (await configExists(cwd)) {
16836
17243
  const existing = await readConfig(cwd);
@@ -16842,13 +17249,13 @@ async function targetDir(cwd, explicitDir, workflowId, title) {
16842
17249
  }
16843
17250
  return cwd;
16844
17251
  }
16845
- return path13.join(cwd, slug(title));
17252
+ return path14.join(cwd, slug(title));
16846
17253
  }
16847
17254
  function registerPull(program3) {
16848
17255
  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) => {
16849
17256
  const options = globalOptions(command);
16850
17257
  const cwd = process.cwd();
16851
- const remembered = await readState(opts.dir ? path13.resolve(cwd, opts.dir) : cwd);
17258
+ const remembered = await readState(opts.dir ? path14.resolve(cwd, opts.dir) : cwd);
16852
17259
  const target = resolveTarget(remembered);
16853
17260
  const client = await authenticatedClient(target);
16854
17261
  if (!workflowId) {
@@ -16861,7 +17268,7 @@ function registerPull(program3) {
16861
17268
  const dir = await targetDir(cwd, opts.dir, workflowId, workflow.title);
16862
17269
  if (await configExists(dir) && !opts.force) {
16863
17270
  throw new CliError(
16864
- `${path13.join(dir, CONFIG_FILE_NAME)} already exists.`,
17271
+ `${path14.join(dir, CONFIG_FILE_NAME)} already exists.`,
16865
17272
  "Pass --force to overwrite it and its code files."
16866
17273
  );
16867
17274
  }
@@ -16883,7 +17290,7 @@ Sample outputs not available, inputData types are unknown: ${error.message}`;
16883
17290
  if (!outputsNote) {
16884
17291
  outputsFile = await writeStateFile(dir, "outputs.json", rows);
16885
17292
  }
16886
- const files = [configFile2, ...codeFiles].map((file) => path13.relative(cwd, file));
17293
+ const files = [configFile2, ...codeFiles].map((file) => path14.relative(cwd, file));
16887
17294
  emitWithVersionNote(
16888
17295
  options,
16889
17296
  config,
@@ -17141,23 +17548,23 @@ function registerPin(program3) {
17141
17548
  }
17142
17549
 
17143
17550
  // src/commands/push.ts
17144
- import path14 from "node:path";
17551
+ import path15 from "node:path";
17145
17552
 
17146
17553
  // src/project/placeholder-check.ts
17147
17554
  var TOKEN = /\{(?:outputs|loopActionGroup)\.[^:{}]+:::[^{}]*\}/g;
17148
- function* stringsIn(value, path15) {
17555
+ function* stringsIn(value, path16) {
17149
17556
  if (typeof value === "string") {
17150
- yield [path15, value];
17557
+ yield [path16, value];
17151
17558
  } else if (Array.isArray(value)) {
17152
17559
  for (const [index, item] of value.entries()) {
17153
- yield* stringsIn(item, `${path15}[${index}]`);
17560
+ yield* stringsIn(item, `${path16}[${index}]`);
17154
17561
  }
17155
17562
  } else if (value !== null && typeof value === "object") {
17156
17563
  for (const [key, item] of Object.entries(value)) {
17157
17564
  if (key === "actions" || key === "paths") {
17158
17565
  continue;
17159
17566
  }
17160
- yield* stringsIn(item, path15 ? `${path15}.${key}` : key);
17567
+ yield* stringsIn(item, path16 ? `${path16}.${key}` : key);
17161
17568
  }
17162
17569
  }
17163
17570
  }
@@ -17268,7 +17675,7 @@ ${unbacked.map(
17268
17675
  draft: result.draft,
17269
17676
  inlined,
17270
17677
  studioUrl,
17271
- dir: path14.resolve(dir),
17678
+ dir: path15.resolve(dir),
17272
17679
  unbackedPlaceholders: unbacked
17273
17680
  }
17274
17681
  );
@@ -17591,7 +17998,7 @@ Pass --workspace <id> to a command, or set SOFTR_WORKSPACE_ID, to work in one of
17591
17998
  // package.json
17592
17999
  var package_default = {
17593
18000
  name: "@softr/cli",
17594
- version: "0.1.5",
18001
+ version: "0.1.6",
17595
18002
  description: "The Softr command line: build, test and publish Softr workflows from local files, manage Softr Databases; more Softr APIs to come",
17596
18003
  keywords: [
17597
18004
  "softr",
@@ -17667,4 +18074,5 @@ registerDatabases(db);
17667
18074
  registerTables(db);
17668
18075
  registerFields(db);
17669
18076
  registerViews(db);
18077
+ registerRecords(db);
17670
18078
  program2.parseAsync(process.argv).catch(reportError);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@softr/cli",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
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",