@tolinax/ayoune-cli 2026.7.1 → 2026.7.2
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/lib/commands/createDbCommand.js +48 -100
- package/package.json +1 -2
|
@@ -5,14 +5,6 @@ import { EXIT_GENERAL_ERROR, EXIT_MISUSE } from "../exitCodes.js";
|
|
|
5
5
|
import { cliError } from "../helpers/cliError.js";
|
|
6
6
|
import { handleResponseFormatOptions } from "../helpers/handleResponseFormatOptions.js";
|
|
7
7
|
import { apiCallHandler } from "../api/apiCallHandler.js";
|
|
8
|
-
import { writeToTarget, getDbStats } from "../db/copyEngine.js";
|
|
9
|
-
function formatBytes(bytes) {
|
|
10
|
-
if (bytes === 0)
|
|
11
|
-
return "0 B";
|
|
12
|
-
const units = ["B", "KB", "MB", "GB", "TB"];
|
|
13
|
-
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
|
14
|
-
return (bytes / Math.pow(1024, i)).toFixed(1) + " " + units[i];
|
|
15
|
-
}
|
|
16
8
|
function maskUri(uri) {
|
|
17
9
|
return uri.replace(/:\/\/([^:]+):([^@]+)@/, "://***:***@");
|
|
18
10
|
}
|
|
@@ -21,21 +13,24 @@ export function createDbCommand(program) {
|
|
|
21
13
|
.command("db")
|
|
22
14
|
.description("Copy query results to external databases, manage targets")
|
|
23
15
|
.addHelpText("after", `
|
|
16
|
+
All database operations go through the aggregation service — the CLI never
|
|
17
|
+
connects to any database directly.
|
|
18
|
+
|
|
24
19
|
Examples:
|
|
25
20
|
ay db copy Interactive: pick target, pick query
|
|
26
21
|
ay db copy my-local-db Pick query for target "my-local-db"
|
|
27
|
-
ay db copy my-local-db active-users Fully autonomous
|
|
22
|
+
ay db copy my-local-db active-users Fully autonomous
|
|
28
23
|
ay db targets list List configured targets
|
|
29
24
|
ay db targets add Add a new target
|
|
30
|
-
ay db
|
|
25
|
+
ay db targets test <slug> Test connectivity to a target`);
|
|
31
26
|
// ─── ay db copy [target] [query] ───────────────────────────────
|
|
32
27
|
db.command("copy [target] [query]")
|
|
33
28
|
.description("Run a saved query and write results to a target database")
|
|
34
29
|
.option("--collection <name>", "Override target collection name")
|
|
35
30
|
.option("--write-mode <mode>", "Override write mode (insert, upsert, replace)")
|
|
36
31
|
.addHelpText("after", `
|
|
37
|
-
|
|
38
|
-
|
|
32
|
+
Executes a saved aggregation query via the aggregation service and writes
|
|
33
|
+
the results to a pre-configured DataTarget. Both are resolved by slug.
|
|
39
34
|
|
|
40
35
|
Examples:
|
|
41
36
|
ay db copy Interactive mode
|
|
@@ -43,7 +38,6 @@ Examples:
|
|
|
43
38
|
ay db copy my-local-db active-users Fully autonomous
|
|
44
39
|
ay db copy my-local-db active-users --collection results --write-mode upsert`)
|
|
45
40
|
.action(async (target, query, options) => {
|
|
46
|
-
var _a;
|
|
47
41
|
try {
|
|
48
42
|
const opts = { ...program.opts(), ...options };
|
|
49
43
|
// Step 1: Resolve target
|
|
@@ -66,57 +60,29 @@ Examples:
|
|
|
66
60
|
cliError("Provide query slug or use interactive mode", EXIT_MISUSE);
|
|
67
61
|
queryData = await promptQuery();
|
|
68
62
|
}
|
|
69
|
-
// Step 3:
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
if (
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
// Step 4: Write to target
|
|
81
|
-
const collection = opts.collection || targetData.targetCollection || ((_a = queryData.name) === null || _a === void 0 ? void 0 : _a.toLowerCase().replace(/\s+/g, "_")) || "query_results";
|
|
82
|
-
const writeMode = opts.writeMode || targetData.writeMode || "insert";
|
|
83
|
-
const batchSize = targetData.batchSize || 1000;
|
|
84
|
-
spinner.start({ text: `Writing ${docs.length} docs to ${maskUri(targetData.connectionUri)}/${collection}...`, color: "cyan" });
|
|
85
|
-
const result = await writeToTarget(targetData.connectionUri, targetData.database, collection, docs, writeMode, batchSize);
|
|
63
|
+
// Step 3: Call the aggregation service export endpoint
|
|
64
|
+
spinner.start({ text: `Running "${queryData.name}" → ${targetData.name}...`, color: "magenta" });
|
|
65
|
+
const body = {
|
|
66
|
+
queryId: queryData._id,
|
|
67
|
+
targetId: targetData._id,
|
|
68
|
+
};
|
|
69
|
+
if (opts.collection)
|
|
70
|
+
body.targetCollection = opts.collection;
|
|
71
|
+
if (opts.writeMode)
|
|
72
|
+
body.writeMode = opts.writeMode;
|
|
73
|
+
const res = await apiCallHandler("aggregation", "export", "post", body);
|
|
86
74
|
spinner.stop();
|
|
87
|
-
// Step 5: Update target status via config-api
|
|
88
|
-
try {
|
|
89
|
-
await apiCallHandler("config", `datatargets/${targetData._id}`, "put", {
|
|
90
|
-
lastUsed: new Date().toISOString(),
|
|
91
|
-
lastStatus: result.errors > 0 ? "failed" : "success",
|
|
92
|
-
lastResultCount: result.written,
|
|
93
|
-
lastError: result.errors > 0 ? `${result.errors} write errors` : undefined,
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
|
-
catch (_b) {
|
|
97
|
-
// Non-critical — don't fail the copy if status update fails
|
|
98
|
-
}
|
|
99
75
|
// Output
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
collection,
|
|
105
|
-
writeMode,
|
|
106
|
-
written: result.written,
|
|
107
|
-
errors: result.errors,
|
|
108
|
-
duration: result.duration,
|
|
109
|
-
},
|
|
110
|
-
meta: { written: result.written, errors: result.errors },
|
|
111
|
-
};
|
|
112
|
-
if (opts.responseFormat === "json" || opts.responseFormat === "yaml") {
|
|
113
|
-
handleResponseFormatOptions(opts, wrapped);
|
|
76
|
+
handleResponseFormatOptions(opts, res);
|
|
77
|
+
const p = res.payload || {};
|
|
78
|
+
if (p.written === 0 && p.errors === 0) {
|
|
79
|
+
spinner.success({ text: "Query returned 0 results — nothing written" });
|
|
114
80
|
}
|
|
115
81
|
else {
|
|
116
|
-
const icon =
|
|
117
|
-
console.log(`\n ${icon} ${
|
|
82
|
+
const icon = (p.errors || 0) === 0 ? chalk.green("●") : chalk.yellow("●");
|
|
83
|
+
console.log(`\n ${icon} ${p.written} docs written to ${chalk.cyan(p.collection || "?")}${p.errors ? chalk.red(` (${p.errors} errors)`) : ""} ${chalk.dim(`(query: ${p.queryTime}ms, write: ${p.writeTime}ms)`)}\n`);
|
|
118
84
|
}
|
|
119
|
-
if (
|
|
85
|
+
if (p.errors > 0)
|
|
120
86
|
process.exit(EXIT_GENERAL_ERROR);
|
|
121
87
|
}
|
|
122
88
|
catch (e) {
|
|
@@ -154,7 +120,8 @@ Examples:
|
|
|
154
120
|
.command("add")
|
|
155
121
|
.description("Add a new database target")
|
|
156
122
|
.option("--name <name>", "Target name")
|
|
157
|
-
.option("--
|
|
123
|
+
.option("--type <type>", "Database type (mongodb, postgresql, rest)", "mongodb")
|
|
124
|
+
.option("--uri <uri>", "Connection URI")
|
|
158
125
|
.option("--database <db>", "Database name")
|
|
159
126
|
.option("--collection <col>", "Default target collection name")
|
|
160
127
|
.option("--write-mode <mode>", "Write mode: insert, upsert, replace", "insert")
|
|
@@ -162,18 +129,19 @@ Examples:
|
|
|
162
129
|
var _a;
|
|
163
130
|
try {
|
|
164
131
|
const opts = { ...program.opts(), ...options };
|
|
165
|
-
let { name, uri, database, collection, writeMode } = opts;
|
|
166
|
-
// Interactive prompts for missing fields
|
|
132
|
+
let { name, type, uri, database, collection, writeMode } = opts;
|
|
167
133
|
if (!name || !uri) {
|
|
168
134
|
if (!process.stdin.isTTY)
|
|
169
135
|
cliError("Provide --name and --uri in non-interactive mode", EXIT_MISUSE);
|
|
170
136
|
const answers = await inquirer.prompt([
|
|
171
137
|
...(!name ? [{ type: "input", name: "name", message: "Target name:", validate: (v) => v.length > 0 || "Required" }] : []),
|
|
172
|
-
...(!
|
|
138
|
+
...(!opts.type || opts.type === "mongodb" ? [{ type: "list", name: "dbType", message: "Database type:", choices: ["mongodb", "postgresql", "rest"], default: "mongodb" }] : []),
|
|
139
|
+
...(!uri ? [{ type: "input", name: "uri", message: "Connection URI:", validate: (v) => v.length > 0 || "Required" }] : []),
|
|
173
140
|
...(!database ? [{ type: "input", name: "database", message: "Database name (leave empty for URI default):" }] : []),
|
|
174
|
-
...(!collection ? [{ type: "input", name: "collection", message: "Default collection name (leave empty to derive from query):" }] : []),
|
|
141
|
+
...(!collection ? [{ type: "input", name: "collection", message: "Default collection/table name (leave empty to derive from query):" }] : []),
|
|
175
142
|
]);
|
|
176
143
|
name = name || answers.name;
|
|
144
|
+
type = answers.dbType || type;
|
|
177
145
|
uri = uri || answers.uri;
|
|
178
146
|
database = database || answers.database || undefined;
|
|
179
147
|
collection = collection || answers.collection || undefined;
|
|
@@ -181,7 +149,7 @@ Examples:
|
|
|
181
149
|
spinner.start({ text: "Creating target...", color: "cyan" });
|
|
182
150
|
const body = {
|
|
183
151
|
name,
|
|
184
|
-
type
|
|
152
|
+
type,
|
|
185
153
|
connectionUri: uri,
|
|
186
154
|
database: database || undefined,
|
|
187
155
|
targetCollection: collection || undefined,
|
|
@@ -214,47 +182,28 @@ Examples:
|
|
|
214
182
|
cliError(e.message || "Failed to remove target", EXIT_GENERAL_ERROR);
|
|
215
183
|
}
|
|
216
184
|
});
|
|
217
|
-
//
|
|
218
|
-
|
|
219
|
-
.
|
|
220
|
-
.
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
Examples:
|
|
224
|
-
ay db stats
|
|
225
|
-
ay db stats "mongodb://other/db" -r table`)
|
|
226
|
-
.action(async (uri, options) => {
|
|
185
|
+
// ay db targets test <slug>
|
|
186
|
+
targets
|
|
187
|
+
.command("test <slug>")
|
|
188
|
+
.description("Test connectivity to a target")
|
|
189
|
+
.action(async (slug, options) => {
|
|
227
190
|
try {
|
|
228
191
|
const opts = { ...program.opts(), ...options };
|
|
229
|
-
const
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
}
|
|
233
|
-
if (!dbUri.startsWith("mongodb")) {
|
|
234
|
-
cliError("URI must start with mongodb:// or mongodb+srv://", EXIT_MISUSE);
|
|
235
|
-
}
|
|
236
|
-
spinner.start({ text: `Connecting to ${maskUri(dbUri)}...`, color: "cyan" });
|
|
237
|
-
const stats = await getDbStats(dbUri);
|
|
192
|
+
const target = await resolveTarget(slug);
|
|
193
|
+
spinner.start({ text: `Testing connection to "${slug}"...`, color: "cyan" });
|
|
194
|
+
const res = await apiCallHandler("aggregation", "export/test", "post", { targetId: target._id });
|
|
238
195
|
spinner.stop();
|
|
239
|
-
const
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
};
|
|
243
|
-
if (opts.responseFormat === "json" || opts.responseFormat === "yaml") {
|
|
244
|
-
handleResponseFormatOptions(opts, wrapped);
|
|
196
|
+
const p = res.payload || {};
|
|
197
|
+
if (p.connected) {
|
|
198
|
+
console.log(chalk.green(`\n ● Connected to ${p.database} (${p.collections} collections)\n`));
|
|
245
199
|
}
|
|
246
200
|
else {
|
|
247
|
-
console.log(chalk.
|
|
248
|
-
|
|
249
|
-
console.log(` ${chalk.dim("Collections:")} ${stats.collections.length}\n`);
|
|
250
|
-
for (const col of stats.collections) {
|
|
251
|
-
console.log(` ${chalk.white(col.name)}: ${col.documents.toLocaleString()} docs ${chalk.dim(`(${formatBytes(col.size)})`)}`);
|
|
252
|
-
}
|
|
253
|
-
console.log();
|
|
201
|
+
console.log(chalk.red(`\n ● Connection failed: ${p.error}\n`));
|
|
202
|
+
process.exit(EXIT_GENERAL_ERROR);
|
|
254
203
|
}
|
|
255
204
|
}
|
|
256
205
|
catch (e) {
|
|
257
|
-
cliError(e.message || "
|
|
206
|
+
cliError(e.message || "Connection test failed", EXIT_GENERAL_ERROR);
|
|
258
207
|
}
|
|
259
208
|
});
|
|
260
209
|
}
|
|
@@ -267,7 +216,6 @@ async function resolveTarget(slugOrId) {
|
|
|
267
216
|
cliError(`Target not found: ${slugOrId}`, EXIT_GENERAL_ERROR);
|
|
268
217
|
return res.payload;
|
|
269
218
|
}
|
|
270
|
-
// Resolve by slug
|
|
271
219
|
const res = await apiCallHandler("config", "datatargets", "get", null, { slug: slugOrId, limit: 1 });
|
|
272
220
|
const entries = res === null || res === void 0 ? void 0 : res.payload;
|
|
273
221
|
if (!entries || (Array.isArray(entries) && entries.length === 0)) {
|
|
@@ -299,7 +247,7 @@ async function promptTarget() {
|
|
|
299
247
|
cliError("No targets configured. Add one with: ay db targets add", EXIT_GENERAL_ERROR);
|
|
300
248
|
}
|
|
301
249
|
const choices = targets.map((t) => ({
|
|
302
|
-
name: `${t.name} ${chalk.dim(`(${maskUri(t.connectionUri)})`)}`,
|
|
250
|
+
name: `${t.name} ${chalk.dim(`(${t.type}: ${maskUri(t.connectionUri || "")})`)}`,
|
|
303
251
|
value: t,
|
|
304
252
|
}));
|
|
305
253
|
const { selected } = await inquirer.prompt([
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tolinax/ayoune-cli",
|
|
3
|
-
"version": "2026.7.
|
|
3
|
+
"version": "2026.7.2",
|
|
4
4
|
"description": "CLI for the aYOUne Business-as-a-Service platform",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.js",
|
|
@@ -134,7 +134,6 @@
|
|
|
134
134
|
"lodash": "^4.17.21",
|
|
135
135
|
"mkdirp": "^3.0.1",
|
|
136
136
|
"moment": "^2.30.1",
|
|
137
|
-
"mongodb": "^6.21.0",
|
|
138
137
|
"nanospinner": "^1.1.0",
|
|
139
138
|
"node-localstorage": "^3.0.5",
|
|
140
139
|
"os": "^0.1.2",
|