@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.
@@ -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 { 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
- }
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: run query, write to target
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 stats Show aYOUne database stats`);
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
- Runs a saved aggregation query and writes the results to a pre-configured target.
38
- Both target and query are resolved by slug. If omitted, you'll be prompted to select.
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: Execute query via aggregation API
70
- const queryId = queryData._id;
71
- spinner.start({ text: `Running query "${queryData.name}"...`, color: "magenta" });
72
- const aggResult = await apiCallHandler("aggregation", `queries/execute/${queryId}`, "post");
73
- spinner.stop();
74
- const docs = Array.isArray(aggResult) ? aggResult : ((aggResult === null || aggResult === void 0 ? void 0 : aggResult.payload) || []);
75
- if (!docs.length) {
76
- spinner.success({ text: "Query returned 0 results — nothing to write" });
77
- return;
78
- }
79
- console.log(chalk.dim(` Query returned ${docs.length} documents\n`));
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
- const wrapped = {
101
- payload: {
102
- target: targetData.name,
103
- query: queryData.name,
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 = result.errors === 0 ? chalk.green("●") : chalk.yellow("●");
117
- console.log(`\n ${icon} ${result.written} docs written to ${chalk.cyan(collection)}${result.errors ? chalk.red(` (${result.errors} errors)`) : ""} ${chalk.dim(`(${result.duration}ms)`)}\n`);
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 (result.errors > 0)
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("--uri <uri>", "MongoDB connection URI")
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
- ...(!uri ? [{ type: "input", name: "uri", message: "MongoDB connection URI:", validate: (v) => v.startsWith("mongodb") || "Must be a MongoDB URI" }] : []),
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: "mongodb",
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
- // ─── ay db stats [uri] ─────────────────────────────────────────
218
- db.command("stats [uri]")
219
- .description("Show database statistics (defaults to aYOUne database)")
220
- .addHelpText("after", `
221
- Defaults to MONGO_CONNECTSTRING when no URI is given.
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 dbUri = uri || process.env.MONGO_CONNECTSTRING || process.env.MONGODB_URI;
230
- if (!dbUri) {
231
- cliError("No database URI. Set MONGO_CONNECTSTRING env var or pass a URI.", EXIT_MISUSE);
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 wrapped = {
240
- payload: stats,
241
- meta: { database: stats.database, collectionCount: stats.collections.length, totalSize: formatBytes(stats.totalSize) },
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.cyan.bold(`\n Database: ${stats.database}\n`));
248
- console.log(` ${chalk.dim("Total size:")} ${formatBytes(stats.totalSize)}`);
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 || "Failed to get stats", EXIT_GENERAL_ERROR);
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
- import inquirer from "inquirer";
3
- import inquirerFuzzyPath from "inquirer-fuzzy-path";
4
- import inquirerSearchList from "inquirer-search-list";
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.7.1",
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
- }
@@ -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
- }
@@ -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 {};