@tolinax/ayoune-cli 2026.7.1 → 2026.8.0
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/data/modelsAndRights.js +6 -0
- package/index.js +0 -2
- package/lib/commands/createAggregateCommand.js +2 -0
- package/lib/commands/createDbCommand.js +51 -100
- package/lib/helpers/initializeSettings.js +20 -6
- package/lib/operations/handleEditOperation.js +2 -0
- package/lib/prompts/promptEntry.js +2 -0
- package/lib/prompts/promptFilePath.js +2 -0
- package/package.json +1 -2
- package/lib/db/copyConfigStore.js +0 -88
- package/lib/db/copyEngine.js +0 -87
- package/lib/db/cronMatcher.js +0 -42
- package/lib/db/types.js +0 -1
package/data/modelsAndRights.js
CHANGED
|
@@ -1281,6 +1281,12 @@ const modelsAndRights = [
|
|
|
1281
1281
|
module: "config",
|
|
1282
1282
|
right: "config.datapools",
|
|
1283
1283
|
},
|
|
1284
|
+
{
|
|
1285
|
+
plural: "DataTargets",
|
|
1286
|
+
singular: "DataTarget",
|
|
1287
|
+
module: "config",
|
|
1288
|
+
right: "config.datatargets",
|
|
1289
|
+
},
|
|
1284
1290
|
{
|
|
1285
1291
|
plural: "Decisions",
|
|
1286
1292
|
singular: "Decision",
|
package/index.js
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
#! /usr/bin/env node
|
|
2
2
|
import { Command } from "commander";
|
|
3
3
|
import { createSpinner } from "nanospinner";
|
|
4
|
-
import { initializeSettings } from "./lib/helpers/initializeSettings.js";
|
|
5
4
|
import { createProgram } from "./lib/commands/createProgram.js";
|
|
6
5
|
//Create new command instance
|
|
7
6
|
const program = new Command();
|
|
8
|
-
initializeSettings();
|
|
9
7
|
//Setup spinner - output to stderr so stdout stays clean for piping
|
|
10
8
|
export const spinner = createSpinner("Getting data...", { stream: process.stderr });
|
|
11
9
|
createProgram(program);
|
|
@@ -6,6 +6,7 @@ import { spinner } from "../../index.js";
|
|
|
6
6
|
import { EXIT_GENERAL_ERROR, EXIT_MISUSE } from "../exitCodes.js";
|
|
7
7
|
import { cliError } from "../helpers/cliError.js";
|
|
8
8
|
import { readPipelineInput } from "../helpers/readPipelineInput.js";
|
|
9
|
+
import { initializeSettings } from "../helpers/initializeSettings.js";
|
|
9
10
|
function wrapAggResult(res) {
|
|
10
11
|
if (Array.isArray(res)) {
|
|
11
12
|
return { payload: res, meta: { resultCount: res.length } };
|
|
@@ -252,6 +253,7 @@ Examples:
|
|
|
252
253
|
cliError("Wizard requires an interactive terminal (TTY)", EXIT_MISUSE);
|
|
253
254
|
}
|
|
254
255
|
// Step 1: Select model
|
|
256
|
+
initializeSettings();
|
|
255
257
|
spinner.start({ text: "Loading models...", color: "magenta" });
|
|
256
258
|
const modelsRes = await apiCallHandler("aggregation", "models", "get");
|
|
257
259
|
spinner.stop();
|
|
@@ -5,14 +5,7 @@ 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 {
|
|
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
|
-
}
|
|
8
|
+
import { initializeSettings } from "../helpers/initializeSettings.js";
|
|
16
9
|
function maskUri(uri) {
|
|
17
10
|
return uri.replace(/:\/\/([^:]+):([^@]+)@/, "://***:***@");
|
|
18
11
|
}
|
|
@@ -21,21 +14,24 @@ export function createDbCommand(program) {
|
|
|
21
14
|
.command("db")
|
|
22
15
|
.description("Copy query results to external databases, manage targets")
|
|
23
16
|
.addHelpText("after", `
|
|
17
|
+
All database operations go through the aggregation service — the CLI never
|
|
18
|
+
connects to any database directly.
|
|
19
|
+
|
|
24
20
|
Examples:
|
|
25
21
|
ay db copy Interactive: pick target, pick query
|
|
26
22
|
ay db copy my-local-db Pick query for target "my-local-db"
|
|
27
|
-
ay db copy my-local-db active-users Fully autonomous
|
|
23
|
+
ay db copy my-local-db active-users Fully autonomous
|
|
28
24
|
ay db targets list List configured targets
|
|
29
25
|
ay db targets add Add a new target
|
|
30
|
-
ay db
|
|
26
|
+
ay db targets test <slug> Test connectivity to a target`);
|
|
31
27
|
// ─── ay db copy [target] [query] ───────────────────────────────
|
|
32
28
|
db.command("copy [target] [query]")
|
|
33
29
|
.description("Run a saved query and write results to a target database")
|
|
34
30
|
.option("--collection <name>", "Override target collection name")
|
|
35
31
|
.option("--write-mode <mode>", "Override write mode (insert, upsert, replace)")
|
|
36
32
|
.addHelpText("after", `
|
|
37
|
-
|
|
38
|
-
|
|
33
|
+
Executes a saved aggregation query via the aggregation service and writes
|
|
34
|
+
the results to a pre-configured DataTarget. Both are resolved by slug.
|
|
39
35
|
|
|
40
36
|
Examples:
|
|
41
37
|
ay db copy Interactive mode
|
|
@@ -43,7 +39,6 @@ Examples:
|
|
|
43
39
|
ay db copy my-local-db active-users Fully autonomous
|
|
44
40
|
ay db copy my-local-db active-users --collection results --write-mode upsert`)
|
|
45
41
|
.action(async (target, query, options) => {
|
|
46
|
-
var _a;
|
|
47
42
|
try {
|
|
48
43
|
const opts = { ...program.opts(), ...options };
|
|
49
44
|
// Step 1: Resolve target
|
|
@@ -66,57 +61,29 @@ Examples:
|
|
|
66
61
|
cliError("Provide query slug or use interactive mode", EXIT_MISUSE);
|
|
67
62
|
queryData = await promptQuery();
|
|
68
63
|
}
|
|
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);
|
|
64
|
+
// Step 3: Call the aggregation service export endpoint
|
|
65
|
+
spinner.start({ text: `Running "${queryData.name}" → ${targetData.name}...`, color: "magenta" });
|
|
66
|
+
const body = {
|
|
67
|
+
queryId: queryData._id,
|
|
68
|
+
targetId: targetData._id,
|
|
69
|
+
};
|
|
70
|
+
if (opts.collection)
|
|
71
|
+
body.targetCollection = opts.collection;
|
|
72
|
+
if (opts.writeMode)
|
|
73
|
+
body.writeMode = opts.writeMode;
|
|
74
|
+
const res = await apiCallHandler("aggregation", "export", "post", body);
|
|
86
75
|
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
76
|
// 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);
|
|
77
|
+
handleResponseFormatOptions(opts, res);
|
|
78
|
+
const p = res.payload || {};
|
|
79
|
+
if (p.written === 0 && p.errors === 0) {
|
|
80
|
+
spinner.success({ text: "Query returned 0 results — nothing written" });
|
|
114
81
|
}
|
|
115
82
|
else {
|
|
116
|
-
const icon =
|
|
117
|
-
console.log(`\n ${icon} ${
|
|
83
|
+
const icon = (p.errors || 0) === 0 ? chalk.green("●") : chalk.yellow("●");
|
|
84
|
+
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
85
|
}
|
|
119
|
-
if (
|
|
86
|
+
if (p.errors > 0)
|
|
120
87
|
process.exit(EXIT_GENERAL_ERROR);
|
|
121
88
|
}
|
|
122
89
|
catch (e) {
|
|
@@ -154,7 +121,8 @@ Examples:
|
|
|
154
121
|
.command("add")
|
|
155
122
|
.description("Add a new database target")
|
|
156
123
|
.option("--name <name>", "Target name")
|
|
157
|
-
.option("--
|
|
124
|
+
.option("--type <type>", "Database type (mongodb, postgresql, rest)", "mongodb")
|
|
125
|
+
.option("--uri <uri>", "Connection URI")
|
|
158
126
|
.option("--database <db>", "Database name")
|
|
159
127
|
.option("--collection <col>", "Default target collection name")
|
|
160
128
|
.option("--write-mode <mode>", "Write mode: insert, upsert, replace", "insert")
|
|
@@ -162,18 +130,19 @@ Examples:
|
|
|
162
130
|
var _a;
|
|
163
131
|
try {
|
|
164
132
|
const opts = { ...program.opts(), ...options };
|
|
165
|
-
let { name, uri, database, collection, writeMode } = opts;
|
|
166
|
-
// Interactive prompts for missing fields
|
|
133
|
+
let { name, type, uri, database, collection, writeMode } = opts;
|
|
167
134
|
if (!name || !uri) {
|
|
168
135
|
if (!process.stdin.isTTY)
|
|
169
136
|
cliError("Provide --name and --uri in non-interactive mode", EXIT_MISUSE);
|
|
170
137
|
const answers = await inquirer.prompt([
|
|
171
138
|
...(!name ? [{ type: "input", name: "name", message: "Target name:", validate: (v) => v.length > 0 || "Required" }] : []),
|
|
172
|
-
...(!
|
|
139
|
+
...(!opts.type || opts.type === "mongodb" ? [{ type: "list", name: "dbType", message: "Database type:", choices: ["mongodb", "postgresql", "rest"], default: "mongodb" }] : []),
|
|
140
|
+
...(!uri ? [{ type: "input", name: "uri", message: "Connection URI:", validate: (v) => v.length > 0 || "Required" }] : []),
|
|
173
141
|
...(!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):" }] : []),
|
|
142
|
+
...(!collection ? [{ type: "input", name: "collection", message: "Default collection/table name (leave empty to derive from query):" }] : []),
|
|
175
143
|
]);
|
|
176
144
|
name = name || answers.name;
|
|
145
|
+
type = answers.dbType || type;
|
|
177
146
|
uri = uri || answers.uri;
|
|
178
147
|
database = database || answers.database || undefined;
|
|
179
148
|
collection = collection || answers.collection || undefined;
|
|
@@ -181,7 +150,7 @@ Examples:
|
|
|
181
150
|
spinner.start({ text: "Creating target...", color: "cyan" });
|
|
182
151
|
const body = {
|
|
183
152
|
name,
|
|
184
|
-
type
|
|
153
|
+
type,
|
|
185
154
|
connectionUri: uri,
|
|
186
155
|
database: database || undefined,
|
|
187
156
|
targetCollection: collection || undefined,
|
|
@@ -214,47 +183,28 @@ Examples:
|
|
|
214
183
|
cliError(e.message || "Failed to remove target", EXIT_GENERAL_ERROR);
|
|
215
184
|
}
|
|
216
185
|
});
|
|
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) => {
|
|
186
|
+
// ay db targets test <slug>
|
|
187
|
+
targets
|
|
188
|
+
.command("test <slug>")
|
|
189
|
+
.description("Test connectivity to a target")
|
|
190
|
+
.action(async (slug, options) => {
|
|
227
191
|
try {
|
|
228
192
|
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);
|
|
193
|
+
const target = await resolveTarget(slug);
|
|
194
|
+
spinner.start({ text: `Testing connection to "${slug}"...`, color: "cyan" });
|
|
195
|
+
const res = await apiCallHandler("aggregation", "export/test", "post", { targetId: target._id });
|
|
238
196
|
spinner.stop();
|
|
239
|
-
const
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
};
|
|
243
|
-
if (opts.responseFormat === "json" || opts.responseFormat === "yaml") {
|
|
244
|
-
handleResponseFormatOptions(opts, wrapped);
|
|
197
|
+
const p = res.payload || {};
|
|
198
|
+
if (p.connected) {
|
|
199
|
+
console.log(chalk.green(`\n ● Connected to ${p.database} (${p.collections} collections)\n`));
|
|
245
200
|
}
|
|
246
201
|
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();
|
|
202
|
+
console.log(chalk.red(`\n ● Connection failed: ${p.error}\n`));
|
|
203
|
+
process.exit(EXIT_GENERAL_ERROR);
|
|
254
204
|
}
|
|
255
205
|
}
|
|
256
206
|
catch (e) {
|
|
257
|
-
cliError(e.message || "
|
|
207
|
+
cliError(e.message || "Connection test failed", EXIT_GENERAL_ERROR);
|
|
258
208
|
}
|
|
259
209
|
});
|
|
260
210
|
}
|
|
@@ -267,7 +217,6 @@ async function resolveTarget(slugOrId) {
|
|
|
267
217
|
cliError(`Target not found: ${slugOrId}`, EXIT_GENERAL_ERROR);
|
|
268
218
|
return res.payload;
|
|
269
219
|
}
|
|
270
|
-
// Resolve by slug
|
|
271
220
|
const res = await apiCallHandler("config", "datatargets", "get", null, { slug: slugOrId, limit: 1 });
|
|
272
221
|
const entries = res === null || res === void 0 ? void 0 : res.payload;
|
|
273
222
|
if (!entries || (Array.isArray(entries) && entries.length === 0)) {
|
|
@@ -291,6 +240,7 @@ async function resolveQuery(slugOrId) {
|
|
|
291
240
|
return Array.isArray(entries) ? entries[0] : entries;
|
|
292
241
|
}
|
|
293
242
|
async function promptTarget() {
|
|
243
|
+
initializeSettings();
|
|
294
244
|
spinner.start({ text: "Loading targets...", color: "cyan" });
|
|
295
245
|
const res = await apiCallHandler("config", "datatargets", "get", null, { limit: 100, active: true });
|
|
296
246
|
spinner.stop();
|
|
@@ -299,7 +249,7 @@ async function promptTarget() {
|
|
|
299
249
|
cliError("No targets configured. Add one with: ay db targets add", EXIT_GENERAL_ERROR);
|
|
300
250
|
}
|
|
301
251
|
const choices = targets.map((t) => ({
|
|
302
|
-
name: `${t.name} ${chalk.dim(`(${maskUri(t.connectionUri)})`)}`,
|
|
252
|
+
name: `${t.name} ${chalk.dim(`(${t.type}: ${maskUri(t.connectionUri || "")})`)}`,
|
|
303
253
|
value: t,
|
|
304
254
|
}));
|
|
305
255
|
const { selected } = await inquirer.prompt([
|
|
@@ -308,6 +258,7 @@ async function promptTarget() {
|
|
|
308
258
|
return selected;
|
|
309
259
|
}
|
|
310
260
|
async function promptQuery() {
|
|
261
|
+
initializeSettings();
|
|
311
262
|
spinner.start({ text: "Loading queries...", color: "magenta" });
|
|
312
263
|
const res = await apiCallHandler("config", "queries", "get", null, {
|
|
313
264
|
limit: 100,
|
|
@@ -1,14 +1,28 @@
|
|
|
1
1
|
//Initializes settings for system environment and inquirer
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
import inquirerFileTreeSelection from "inquirer-file-tree-selection-prompt";
|
|
6
|
-
import inquirerTableInput from "inquirer-table-input";
|
|
7
|
-
import InterruptedPrompt from "inquirer-interrupted-prompt";
|
|
2
|
+
// Lazy-loaded: inquirer plugins are heavy (~4000 modules incl. rxjs).
|
|
3
|
+
// Only register them when an interactive prompt is actually needed.
|
|
4
|
+
let _initialized = false;
|
|
8
5
|
export function initializeSettings() {
|
|
6
|
+
if (_initialized)
|
|
7
|
+
return;
|
|
8
|
+
_initialized = true;
|
|
9
|
+
const inquirer = require("inquirer");
|
|
10
|
+
const inquirerFuzzyPath = require("inquirer-fuzzy-path");
|
|
11
|
+
const inquirerSearchList = require("inquirer-search-list");
|
|
12
|
+
const inquirerFileTreeSelection = require("inquirer-file-tree-selection-prompt");
|
|
13
|
+
const inquirerTableInput = require("inquirer-table-input");
|
|
14
|
+
const InterruptedPrompt = require("inquirer-interrupted-prompt");
|
|
9
15
|
inquirer.registerPrompt("fuzzypath", inquirerFuzzyPath);
|
|
10
16
|
inquirer.registerPrompt("search-list", inquirerSearchList);
|
|
11
17
|
inquirer.registerPrompt("file-tree-selection", inquirerFileTreeSelection);
|
|
12
18
|
inquirer.registerPrompt("table-input", inquirerTableInput);
|
|
13
19
|
InterruptedPrompt.fromAll(inquirer);
|
|
14
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* Returns the inquirer instance with all custom prompts registered.
|
|
23
|
+
* Call this instead of importing inquirer directly when using custom prompt types.
|
|
24
|
+
*/
|
|
25
|
+
export function getInquirer() {
|
|
26
|
+
initializeSettings();
|
|
27
|
+
return require("inquirer");
|
|
28
|
+
}
|
|
@@ -2,8 +2,10 @@ import inquirer from "inquirer";
|
|
|
2
2
|
import chalk from "chalk";
|
|
3
3
|
import { apiCallHandler } from "../api/apiCallHandler.js";
|
|
4
4
|
import { spinner } from "../../index.js";
|
|
5
|
+
import { initializeSettings } from "../helpers/initializeSettings.js";
|
|
5
6
|
export async function handleEditOperation(module, collection, editContent) {
|
|
6
7
|
console.log(editContent);
|
|
8
|
+
initializeSettings();
|
|
7
9
|
const { edits } = await inquirer.prompt([
|
|
8
10
|
{
|
|
9
11
|
type: "table-input",
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import inquirer from "inquirer";
|
|
2
|
+
import { initializeSettings } from "../helpers/initializeSettings.js";
|
|
2
3
|
function getEntryLabel(el) {
|
|
3
4
|
const name = el.name || el.title || el.subject || el.label || el.originalname || el.summary;
|
|
4
5
|
if (name) {
|
|
@@ -7,6 +8,7 @@ function getEntryLabel(el) {
|
|
|
7
8
|
return el._id;
|
|
8
9
|
}
|
|
9
10
|
export async function promptEntry(result) {
|
|
11
|
+
initializeSettings();
|
|
10
12
|
const { entry } = await inquirer.prompt([
|
|
11
13
|
{
|
|
12
14
|
type: "search-list",
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import inquirer from "inquirer";
|
|
2
2
|
import os from "os";
|
|
3
3
|
import path from "path";
|
|
4
|
+
import { initializeSettings } from "../helpers/initializeSettings.js";
|
|
4
5
|
export async function promptFilePath(fp) {
|
|
6
|
+
initializeSettings();
|
|
5
7
|
const { filePath } = await inquirer.prompt([
|
|
6
8
|
{
|
|
7
9
|
type: "fuzzypath",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tolinax/ayoune-cli",
|
|
3
|
-
"version": "2026.
|
|
3
|
+
"version": "2026.8.0",
|
|
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",
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
|
|
2
|
-
import path from "path";
|
|
3
|
-
import os from "os";
|
|
4
|
-
import crypto from "crypto";
|
|
5
|
-
const CONFIG_PATH = path.join(os.homedir(), ".config", "ayoune", "db-copies.json");
|
|
6
|
-
const ALGORITHM = "aes-256-cbc";
|
|
7
|
-
function deriveKey() {
|
|
8
|
-
const seed = `ayoune-cli:${os.hostname()}:${os.userInfo().username}`;
|
|
9
|
-
return crypto.createHash("sha256").update(seed).digest();
|
|
10
|
-
}
|
|
11
|
-
function encrypt(text) {
|
|
12
|
-
const key = deriveKey();
|
|
13
|
-
const iv = crypto.randomBytes(16);
|
|
14
|
-
const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
|
|
15
|
-
let encrypted = cipher.update(text, "utf8", "hex");
|
|
16
|
-
encrypted += cipher.final("hex");
|
|
17
|
-
return iv.toString("hex") + ":" + encrypted;
|
|
18
|
-
}
|
|
19
|
-
function decrypt(data) {
|
|
20
|
-
const key = deriveKey();
|
|
21
|
-
const [ivHex, encrypted] = data.split(":");
|
|
22
|
-
if (!ivHex || !encrypted)
|
|
23
|
-
return data;
|
|
24
|
-
try {
|
|
25
|
-
const iv = Buffer.from(ivHex, "hex");
|
|
26
|
-
const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
|
|
27
|
-
let decrypted = decipher.update(encrypted, "hex", "utf8");
|
|
28
|
-
decrypted += decipher.final("utf8");
|
|
29
|
-
return decrypted;
|
|
30
|
-
}
|
|
31
|
-
catch (_a) {
|
|
32
|
-
return data;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
export function maskUri(uri) {
|
|
36
|
-
return uri.replace(/:\/\/([^:]+):([^@]+)@/, "://***:***@");
|
|
37
|
-
}
|
|
38
|
-
export function loadCopyConfigs() {
|
|
39
|
-
if (!existsSync(CONFIG_PATH))
|
|
40
|
-
return [];
|
|
41
|
-
try {
|
|
42
|
-
return JSON.parse(readFileSync(CONFIG_PATH, "utf-8"));
|
|
43
|
-
}
|
|
44
|
-
catch (_a) {
|
|
45
|
-
return [];
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
export function saveCopyConfigs(configs) {
|
|
49
|
-
const dir = path.dirname(CONFIG_PATH);
|
|
50
|
-
if (!existsSync(dir))
|
|
51
|
-
mkdirSync(dir, { recursive: true });
|
|
52
|
-
writeFileSync(CONFIG_PATH, JSON.stringify(configs, null, 2), "utf-8");
|
|
53
|
-
}
|
|
54
|
-
export function addCopyConfig(config) {
|
|
55
|
-
const configs = loadCopyConfigs();
|
|
56
|
-
// Encrypt URIs before saving
|
|
57
|
-
config.fromUri = encrypt(config.fromUri);
|
|
58
|
-
config.toUri = encrypt(config.toUri);
|
|
59
|
-
configs.push(config);
|
|
60
|
-
saveCopyConfigs(configs);
|
|
61
|
-
}
|
|
62
|
-
export function removeCopyConfig(id) {
|
|
63
|
-
const configs = loadCopyConfigs();
|
|
64
|
-
const filtered = configs.filter((c) => c.id !== id);
|
|
65
|
-
if (filtered.length === configs.length)
|
|
66
|
-
return false;
|
|
67
|
-
saveCopyConfigs(filtered);
|
|
68
|
-
return true;
|
|
69
|
-
}
|
|
70
|
-
export function getDecryptedConfig(config) {
|
|
71
|
-
return {
|
|
72
|
-
fromUri: decrypt(config.fromUri),
|
|
73
|
-
toUri: decrypt(config.toUri),
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
export function updateCopyConfigLastRun(id, status, error) {
|
|
77
|
-
const configs = loadCopyConfigs();
|
|
78
|
-
const config = configs.find((c) => c.id === id);
|
|
79
|
-
if (config) {
|
|
80
|
-
config.lastRun = new Date().toISOString();
|
|
81
|
-
config.lastStatus = status;
|
|
82
|
-
if (error)
|
|
83
|
-
config.lastError = error;
|
|
84
|
-
else
|
|
85
|
-
delete config.lastError;
|
|
86
|
-
saveCopyConfigs(configs);
|
|
87
|
-
}
|
|
88
|
-
}
|
package/lib/db/copyEngine.js
DELETED
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
import { MongoClient } from "mongodb";
|
|
2
|
-
/**
|
|
3
|
-
* Writes aggregation query results to a target MongoDB collection.
|
|
4
|
-
* This is the core of the copy flow: query results from the aggregation API
|
|
5
|
-
* are written to an external MongoDB target.
|
|
6
|
-
*/
|
|
7
|
-
export async function writeToTarget(targetUri, database, collection, docs, writeMode, batchSize = 1000) {
|
|
8
|
-
const startTime = Date.now();
|
|
9
|
-
const client = new MongoClient(targetUri);
|
|
10
|
-
let written = 0;
|
|
11
|
-
let errors = 0;
|
|
12
|
-
try {
|
|
13
|
-
await client.connect();
|
|
14
|
-
const db = database ? client.db(database) : client.db();
|
|
15
|
-
const col = db.collection(collection);
|
|
16
|
-
if (writeMode === "replace") {
|
|
17
|
-
try {
|
|
18
|
-
await col.drop();
|
|
19
|
-
}
|
|
20
|
-
catch (_a) {
|
|
21
|
-
// Collection may not exist
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
// Write in batches
|
|
25
|
-
for (let i = 0; i < docs.length; i += batchSize) {
|
|
26
|
-
const batch = docs.slice(i, i + batchSize);
|
|
27
|
-
const result = await writeBatch(col, batch, writeMode === "upsert");
|
|
28
|
-
written += result.written;
|
|
29
|
-
errors += result.errors;
|
|
30
|
-
}
|
|
31
|
-
return { written, errors, duration: Date.now() - startTime };
|
|
32
|
-
}
|
|
33
|
-
finally {
|
|
34
|
-
await client.close();
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
async function writeBatch(collection, docs, upsert) {
|
|
38
|
-
var _a, _b, _c, _d;
|
|
39
|
-
try {
|
|
40
|
-
if (upsert) {
|
|
41
|
-
const ops = docs.map((doc) => ({
|
|
42
|
-
updateOne: {
|
|
43
|
-
filter: { _id: doc._id },
|
|
44
|
-
update: { $set: doc },
|
|
45
|
-
upsert: true,
|
|
46
|
-
},
|
|
47
|
-
}));
|
|
48
|
-
const result = await collection.bulkWrite(ops, { ordered: false });
|
|
49
|
-
return { written: (result.upsertedCount || 0) + (result.modifiedCount || 0), errors: 0 };
|
|
50
|
-
}
|
|
51
|
-
else {
|
|
52
|
-
const result = await collection.insertMany(docs, { ordered: false });
|
|
53
|
-
return { written: result.insertedCount, errors: 0 };
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
catch (e) {
|
|
57
|
-
const written = (_d = (_b = (_a = e.result) === null || _a === void 0 ? void 0 : _a.nInserted) !== null && _b !== void 0 ? _b : (_c = e.result) === null || _c === void 0 ? void 0 : _c.insertedCount) !== null && _d !== void 0 ? _d : 0;
|
|
58
|
-
return { written, errors: docs.length - written };
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
export async function getDbStats(uri) {
|
|
62
|
-
const client = new MongoClient(uri);
|
|
63
|
-
try {
|
|
64
|
-
await client.connect();
|
|
65
|
-
const db = client.db();
|
|
66
|
-
const stats = await db.stats();
|
|
67
|
-
const collList = await db.listCollections().toArray();
|
|
68
|
-
const collections = [];
|
|
69
|
-
for (const col of collList) {
|
|
70
|
-
if (col.name.startsWith("system."))
|
|
71
|
-
continue;
|
|
72
|
-
try {
|
|
73
|
-
const count = await db.collection(col.name).estimatedDocumentCount();
|
|
74
|
-
const colStats = await db.command({ collStats: col.name });
|
|
75
|
-
collections.push({ name: col.name, documents: count, size: colStats.size || 0 });
|
|
76
|
-
}
|
|
77
|
-
catch (_a) {
|
|
78
|
-
collections.push({ name: col.name, documents: 0, size: 0 });
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
collections.sort((a, b) => b.documents - a.documents);
|
|
82
|
-
return { database: stats.db, collections, totalSize: stats.dataSize || 0 };
|
|
83
|
-
}
|
|
84
|
-
finally {
|
|
85
|
-
await client.close();
|
|
86
|
-
}
|
|
87
|
-
}
|
package/lib/db/cronMatcher.js
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import cronParser from "cron-parser";
|
|
2
|
-
/**
|
|
3
|
-
* Determines whether a cron-scheduled job is due for execution.
|
|
4
|
-
* Returns true if the current time has passed the next scheduled run after lastRun.
|
|
5
|
-
*/
|
|
6
|
-
export function isCronDue(expression, lastRun, now = new Date()) {
|
|
7
|
-
try {
|
|
8
|
-
if (!lastRun)
|
|
9
|
-
return true; // Never run before → always due
|
|
10
|
-
const lastRunDate = new Date(lastRun);
|
|
11
|
-
const interval = cronParser.parseExpression(expression, { currentDate: lastRunDate });
|
|
12
|
-
const nextRun = interval.next().toDate();
|
|
13
|
-
return now >= nextRun;
|
|
14
|
-
}
|
|
15
|
-
catch (_a) {
|
|
16
|
-
return false; // Invalid cron expression
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
/**
|
|
20
|
-
* Returns the next scheduled run time for a cron expression.
|
|
21
|
-
*/
|
|
22
|
-
export function getNextRun(expression, after = new Date()) {
|
|
23
|
-
try {
|
|
24
|
-
const interval = cronParser.parseExpression(expression, { currentDate: after });
|
|
25
|
-
return interval.next().toDate();
|
|
26
|
-
}
|
|
27
|
-
catch (_a) {
|
|
28
|
-
return null;
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
/**
|
|
32
|
-
* Validates a cron expression. Returns null if valid, error message if invalid.
|
|
33
|
-
*/
|
|
34
|
-
export function validateCron(expression) {
|
|
35
|
-
try {
|
|
36
|
-
cronParser.parseExpression(expression);
|
|
37
|
-
return null;
|
|
38
|
-
}
|
|
39
|
-
catch (e) {
|
|
40
|
-
return e.message || "Invalid cron expression";
|
|
41
|
-
}
|
|
42
|
-
}
|
package/lib/db/types.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|