@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.
- package/README.md +8 -0
- package/dist/cli.mjs +506 -98
- 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(
|
|
2665
|
-
this.conflictsWith = this.conflictsWith.concat(
|
|
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(
|
|
3272
|
-
|
|
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(
|
|
4955
|
-
if (
|
|
4956
|
-
this._executableDir =
|
|
4954
|
+
executableDir(path16) {
|
|
4955
|
+
if (path16 === void 0) return this._executableDir;
|
|
4956
|
+
this._executableDir = path16;
|
|
4957
4957
|
return this;
|
|
4958
4958
|
}
|
|
4959
4959
|
/**
|
|
@@ -5310,12 +5310,12 @@ function executionOrder(config) {
|
|
|
5310
5310
|
collect(config.actions, []);
|
|
5311
5311
|
const outgoing = new Map(appearance.map((id) => [id, []]));
|
|
5312
5312
|
const indegree = new Map(appearance.map((id) => [id, 0]));
|
|
5313
|
-
for (const
|
|
5314
|
-
const to =
|
|
5315
|
-
if (!to || !nodes.has(
|
|
5313
|
+
for (const path16 of pathSets.flat()) {
|
|
5314
|
+
const to = path16.toActionId;
|
|
5315
|
+
if (!to || !nodes.has(path16.fromActionId) || !nodes.has(to) || outgoing.get(path16.fromActionId).includes(to)) {
|
|
5316
5316
|
continue;
|
|
5317
5317
|
}
|
|
5318
|
-
outgoing.get(
|
|
5318
|
+
outgoing.get(path16.fromActionId).push(to);
|
|
5319
5319
|
indegree.set(to, indegree.get(to) + 1);
|
|
5320
5320
|
}
|
|
5321
5321
|
const queue = [...nodes.values()].filter((node) => node.kind === "trigger").map((node) => node.id);
|
|
@@ -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,
|
|
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
|
|
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)}${
|
|
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} ${
|
|
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,
|
|
6155
|
-
const response = await send(client, "workflows", method,
|
|
6156
|
+
async function apiRequest(client, method, path16, body) {
|
|
6157
|
+
const response = await send(client, "workflows", method, path16, body);
|
|
6156
6158
|
return await response.json();
|
|
6157
6159
|
}
|
|
6158
|
-
async function apiRequestVoid(client, method,
|
|
6159
|
-
await send(client, "workflows", method,
|
|
6160
|
+
async function apiRequestVoid(client, method, path16, body) {
|
|
6161
|
+
await send(client, "workflows", method, path16, body);
|
|
6160
6162
|
}
|
|
6161
|
-
async function tablesRequest(client, method,
|
|
6162
|
-
const response = await send(client, "tables", method,
|
|
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,
|
|
6166
|
-
await send(client, "tables", method,
|
|
6167
|
+
async function tablesRequestVoid(client, method, path16, body) {
|
|
6168
|
+
await send(client, "tables", method, path16, body);
|
|
6167
6169
|
}
|
|
6168
|
-
async function studioRequest(client, method,
|
|
6169
|
-
const response = await send(client, "studio", method,
|
|
6170
|
+
async function studioRequest(client, method, path16, body) {
|
|
6171
|
+
const response = await send(client, "studio", method, path16, body);
|
|
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,
|
|
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
|
|
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
|
|
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 (
|
|
7360
|
-
lastSegment =
|
|
7361
|
-
parent = findNodeAtLocation(root,
|
|
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,
|
|
7585
|
-
return setProperty(text,
|
|
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(
|
|
13597
|
-
let rest =
|
|
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,
|
|
14098
|
-
const segments = parsePath(
|
|
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 "${
|
|
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
|
|
14649
|
-
const response = await fetch(`${target.studioApiUrl}${
|
|
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 ${
|
|
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
|
|
14986
|
+
let names2;
|
|
14908
14987
|
try {
|
|
14909
|
-
|
|
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
|
-
|
|
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/
|
|
15607
|
-
|
|
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
|
-
|
|
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(
|
|
15686
|
-
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
16390
|
-
return /^[A-Za-z_]\w*$/.test(key) ? `${
|
|
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,
|
|
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(
|
|
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(
|
|
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, `${
|
|
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 = (
|
|
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
|
|
17149
|
+
let path16 = "$";
|
|
16743
17150
|
for (const key of keys) {
|
|
16744
17151
|
if (key === "*") {
|
|
16745
|
-
|
|
17152
|
+
path16 += "[*]";
|
|
16746
17153
|
} else if (/^\d+$/.test(key)) {
|
|
16747
|
-
|
|
17154
|
+
path16 += `[${key}]`;
|
|
16748
17155
|
} else if (/^[A-Za-z_]\w*$/.test(key)) {
|
|
16749
|
-
|
|
17156
|
+
path16 += `.${key}`;
|
|
16750
17157
|
} else {
|
|
16751
|
-
|
|
17158
|
+
path16 += `['${key.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}']`;
|
|
16752
17159
|
}
|
|
16753
17160
|
}
|
|
16754
|
-
return
|
|
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
|
|
16780
|
-
if (!parsePath(
|
|
16781
|
-
throw new CliError(`Cannot read the path "${
|
|
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${
|
|
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(
|
|
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:
|
|
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
|
|
17237
|
+
import path14 from "node:path";
|
|
16831
17238
|
async function targetDir(cwd, explicitDir, workflowId, title) {
|
|
16832
17239
|
if (explicitDir) {
|
|
16833
|
-
return
|
|
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
|
|
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 ?
|
|
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
|
-
`${
|
|
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) =>
|
|
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
|
|
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,
|
|
17555
|
+
function* stringsIn(value, path16) {
|
|
17149
17556
|
if (typeof value === "string") {
|
|
17150
|
-
yield [
|
|
17557
|
+
yield [path16, value];
|
|
17151
17558
|
} else if (Array.isArray(value)) {
|
|
17152
17559
|
for (const [index, item] of value.entries()) {
|
|
17153
|
-
yield* stringsIn(item, `${
|
|
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,
|
|
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:
|
|
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.
|
|
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