@tolinax/ayoune-cli 2026.8.2 → 2026.9.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/lib/api/apiCallHandler.js +1 -1
- package/lib/api/apiClient.js +74 -62
- package/lib/commands/_registry.js +279 -0
- package/lib/commands/aggregate/_shared.js +21 -0
- package/lib/commands/aggregate/_stageBuilders.js +295 -0
- package/lib/commands/aggregate/exec.js +51 -0
- package/lib/commands/aggregate/index.js +38 -0
- package/lib/commands/aggregate/list.js +43 -0
- package/lib/commands/aggregate/models.js +43 -0
- package/lib/commands/aggregate/run.js +53 -0
- package/lib/commands/aggregate/save.js +53 -0
- package/lib/commands/aggregate/validate.js +47 -0
- package/lib/commands/aggregate/wizard.js +174 -0
- package/lib/commands/createAggregateCommand.js +5 -658
- package/lib/commands/createDeployCommand.js +5 -642
- package/lib/commands/createProgram.js +251 -161
- package/lib/commands/createServicesCommand.js +4 -5
- package/lib/commands/createWhoAmICommand.js +5 -5
- package/lib/commands/deploy/_token.js +8 -0
- package/lib/commands/deploy/alerts.js +43 -0
- package/lib/commands/deploy/clusters.js +62 -0
- package/lib/commands/deploy/dashboard.js +31 -0
- package/lib/commands/deploy/deployments.js +216 -0
- package/lib/commands/deploy/index.js +31 -0
- package/lib/commands/deploy/pipelines.js +82 -0
- package/lib/commands/deploy/plans.js +147 -0
- package/lib/commands/deploy/pods.js +70 -0
- package/lib/commands/deploy/repos.js +63 -0
- package/lib/helpers/dateFormat.js +119 -0
- package/lib/helpers/formatDocument.js +4 -5
- package/lib/helpers/logo.js +86 -13
- package/lib/helpers/saveFile.js +4 -9
- package/lib/models/getModelsInModules.js +6 -8
- package/lib/models/getModuleFromCollection.js +2 -2
- package/lib/operations/handleCollectionOperation.js +2 -3
- package/package.json +2 -12
|
@@ -1,658 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
import { EXIT_GENERAL_ERROR, EXIT_MISUSE } from "../exitCodes.js";
|
|
7
|
-
import { cliError } from "../helpers/cliError.js";
|
|
8
|
-
import { readPipelineInput } from "../helpers/readPipelineInput.js";
|
|
9
|
-
import { initializeSettings } from "../helpers/initializeSettings.js";
|
|
10
|
-
function wrapAggResult(res) {
|
|
11
|
-
if (Array.isArray(res)) {
|
|
12
|
-
return { payload: res, meta: { resultCount: res.length } };
|
|
13
|
-
}
|
|
14
|
-
if ((res === null || res === void 0 ? void 0 : res.payload) !== undefined)
|
|
15
|
-
return res;
|
|
16
|
-
return { payload: res, meta: {} };
|
|
17
|
-
}
|
|
18
|
-
export function createAggregateCommand(program) {
|
|
19
|
-
const agg = program
|
|
20
|
-
.command("aggregate")
|
|
21
|
-
.alias("agg")
|
|
22
|
-
.description("Run MongoDB aggregation pipelines, manage saved queries, and explore models")
|
|
23
|
-
.addHelpText("after", `
|
|
24
|
-
Examples:
|
|
25
|
-
ay agg run consumers --pipeline '[{"$count":"total"}]'
|
|
26
|
-
ay agg wizard
|
|
27
|
-
ay agg exec monthly-revenue -r table
|
|
28
|
-
ay agg list
|
|
29
|
-
ay agg save --name "Active Users" --model consumers --pipeline '[{"$match":{"status":"active"}}]'
|
|
30
|
-
ay agg validate --pipeline '[{"$group":{"_id":"$type","count":{"$sum":1}}}]'
|
|
31
|
-
ay agg models consumers`);
|
|
32
|
-
// ─── ay agg run <model> ─────────────────────────────────────────
|
|
33
|
-
agg
|
|
34
|
-
.command("run <model>")
|
|
35
|
-
.description("Execute an aggregation pipeline on a model")
|
|
36
|
-
.option("--pipeline <json>", "Pipeline as JSON array string")
|
|
37
|
-
.option("--file <path>", "Read pipeline from a JSON file")
|
|
38
|
-
.option("--global", "Skip tenant filter (superuser only)")
|
|
39
|
-
.option("--replace", "Enable template variable replacement")
|
|
40
|
-
.option("--resultAsObject", "Return result as single object")
|
|
41
|
-
.option("--random", "Return one random result")
|
|
42
|
-
.addHelpText("after", `
|
|
43
|
-
Examples:
|
|
44
|
-
ay agg run consumers --pipeline '[{"$match":{"status":"active"}},{"$count":"total"}]'
|
|
45
|
-
ay agg run orders --file pipeline.json -r table
|
|
46
|
-
cat pipeline.json | ay agg run products`)
|
|
47
|
-
.action(async (model, options) => {
|
|
48
|
-
var _a, _b;
|
|
49
|
-
try {
|
|
50
|
-
const opts = { ...program.opts(), ...options };
|
|
51
|
-
const pipeline = await readPipelineInput(opts);
|
|
52
|
-
spinner.start({ text: `Executing pipeline on ${model}...`, color: "magenta" });
|
|
53
|
-
const params = {};
|
|
54
|
-
if (opts.global)
|
|
55
|
-
params.global = true;
|
|
56
|
-
if (opts.replace)
|
|
57
|
-
params.replace = true;
|
|
58
|
-
if (opts.resultAsObject)
|
|
59
|
-
params.resultAsObject = true;
|
|
60
|
-
if (opts.random)
|
|
61
|
-
params.random = true;
|
|
62
|
-
const res = await apiCallHandler("aggregation", model, "post", pipeline, params);
|
|
63
|
-
const wrapped = wrapAggResult(res);
|
|
64
|
-
handleResponseFormatOptions(opts, wrapped);
|
|
65
|
-
spinner.success({ text: `Pipeline returned ${(_b = (_a = wrapped.meta) === null || _a === void 0 ? void 0 : _a.resultCount) !== null && _b !== void 0 ? _b : "?"} results` });
|
|
66
|
-
}
|
|
67
|
-
catch (e) {
|
|
68
|
-
cliError(e.message || "Pipeline execution failed", EXIT_GENERAL_ERROR);
|
|
69
|
-
}
|
|
70
|
-
});
|
|
71
|
-
// ─── ay agg exec <slugOrId> ─────────────────────────────────────
|
|
72
|
-
agg
|
|
73
|
-
.command("exec <slugOrId>")
|
|
74
|
-
.description("Execute a saved query by slug name or ObjectId")
|
|
75
|
-
.addHelpText("after", `
|
|
76
|
-
Examples:
|
|
77
|
-
ay agg exec monthly-revenue -r table
|
|
78
|
-
ay agg exec 507f1f77bcf86cd799439011 -r json --quiet`)
|
|
79
|
-
.action(async (slugOrId, options) => {
|
|
80
|
-
var _a, _b, _c, _d;
|
|
81
|
-
try {
|
|
82
|
-
const opts = { ...program.opts(), ...options };
|
|
83
|
-
const isObjectId = /^[a-f0-9]{24}$/i.test(slugOrId);
|
|
84
|
-
spinner.start({ text: `Executing query "${slugOrId}"...`, color: "magenta" });
|
|
85
|
-
let queryId = slugOrId;
|
|
86
|
-
if (!isObjectId) {
|
|
87
|
-
// Resolve slug to ID via config-api
|
|
88
|
-
const lookup = await apiCallHandler("config", "queries", "get", null, {
|
|
89
|
-
slug: slugOrId,
|
|
90
|
-
limit: 1,
|
|
91
|
-
responseFormat: "json",
|
|
92
|
-
});
|
|
93
|
-
const entries = lookup === null || lookup === void 0 ? void 0 : lookup.payload;
|
|
94
|
-
if (!entries || (Array.isArray(entries) && entries.length === 0)) {
|
|
95
|
-
cliError(`No query found with slug "${slugOrId}"`, EXIT_GENERAL_ERROR);
|
|
96
|
-
}
|
|
97
|
-
queryId = Array.isArray(entries) ? entries[0]._id : entries._id;
|
|
98
|
-
}
|
|
99
|
-
const res = await apiCallHandler("aggregation", `queries/execute/${queryId}`, "post");
|
|
100
|
-
const wrapped = wrapAggResult(res);
|
|
101
|
-
handleResponseFormatOptions(opts, wrapped);
|
|
102
|
-
const count = (_d = (_b = (_a = wrapped.meta) === null || _a === void 0 ? void 0 : _a.resultCount) !== null && _b !== void 0 ? _b : (_c = wrapped.payload) === null || _c === void 0 ? void 0 : _c.length) !== null && _d !== void 0 ? _d : "?";
|
|
103
|
-
spinner.success({ text: `Query returned ${count} results` });
|
|
104
|
-
}
|
|
105
|
-
catch (e) {
|
|
106
|
-
cliError(e.message || "Query execution failed", EXIT_GENERAL_ERROR);
|
|
107
|
-
}
|
|
108
|
-
});
|
|
109
|
-
// ─── ay agg list ────────────────────────────────────────────────
|
|
110
|
-
agg
|
|
111
|
-
.command("list")
|
|
112
|
-
.alias("ls")
|
|
113
|
-
.description("List saved aggregation queries")
|
|
114
|
-
.option("-l, --limit <number>", "Limit results", parseInt, 50)
|
|
115
|
-
.option("-p, --page <number>", "Page number", parseInt, 1)
|
|
116
|
-
.option("--search <term>", "Search by name")
|
|
117
|
-
.action(async (options) => {
|
|
118
|
-
var _a, _b, _c, _d, _e;
|
|
119
|
-
try {
|
|
120
|
-
const opts = { ...program.opts(), ...options };
|
|
121
|
-
spinner.start({ text: "Fetching queries...", color: "magenta" });
|
|
122
|
-
const params = {
|
|
123
|
-
page: opts.page,
|
|
124
|
-
limit: opts.limit,
|
|
125
|
-
responseFormat: opts.responseFormat,
|
|
126
|
-
verbosity: opts.verbosity,
|
|
127
|
-
"dataSource.source": "aggregation",
|
|
128
|
-
};
|
|
129
|
-
if (opts.search)
|
|
130
|
-
params.q = opts.search;
|
|
131
|
-
const res = await apiCallHandler("config", "queries", "get", null, params);
|
|
132
|
-
handleResponseFormatOptions(opts, res);
|
|
133
|
-
const total = (_e = (_c = (_b = (_a = res.meta) === null || _a === void 0 ? void 0 : _a.pageInfo) === null || _b === void 0 ? void 0 : _b.totalEntries) !== null && _c !== void 0 ? _c : (_d = res.payload) === null || _d === void 0 ? void 0 : _d.length) !== null && _e !== void 0 ? _e : 0;
|
|
134
|
-
spinner.success({ text: `Found ${total} queries` });
|
|
135
|
-
}
|
|
136
|
-
catch (e) {
|
|
137
|
-
cliError(e.message || "Failed to list queries", EXIT_GENERAL_ERROR);
|
|
138
|
-
}
|
|
139
|
-
});
|
|
140
|
-
// ─── ay agg save ────────────────────────────────────────────────
|
|
141
|
-
agg
|
|
142
|
-
.command("save")
|
|
143
|
-
.description("Save a pipeline as a reusable query")
|
|
144
|
-
.requiredOption("--name <name>", "Query name")
|
|
145
|
-
.requiredOption("--model <model>", "Aggregation model name")
|
|
146
|
-
.option("--pipeline <json>", "Pipeline as JSON array string")
|
|
147
|
-
.option("--file <path>", "Read pipeline from a JSON file")
|
|
148
|
-
.addHelpText("after", `
|
|
149
|
-
Examples:
|
|
150
|
-
ay agg save --name "Monthly Revenue" --model orders --pipeline '[{"$group":{"_id":null,"total":{"$sum":"$amount"}}}]'
|
|
151
|
-
ay agg save --name "Active Users" --model consumers --file pipeline.json`)
|
|
152
|
-
.action(async (options) => {
|
|
153
|
-
var _a, _b;
|
|
154
|
-
try {
|
|
155
|
-
const opts = { ...program.opts(), ...options };
|
|
156
|
-
const pipeline = await readPipelineInput(opts);
|
|
157
|
-
spinner.start({ text: "Saving query...", color: "magenta" });
|
|
158
|
-
const body = {
|
|
159
|
-
name: opts.name,
|
|
160
|
-
dataSource: {
|
|
161
|
-
source: "aggregation",
|
|
162
|
-
aggregationModel: opts.model,
|
|
163
|
-
aggregationPipeline: JSON.stringify(pipeline),
|
|
164
|
-
},
|
|
165
|
-
cache: { enabled: false },
|
|
166
|
-
};
|
|
167
|
-
const res = await apiCallHandler("config", "queries", "post", body, {
|
|
168
|
-
responseFormat: opts.responseFormat,
|
|
169
|
-
});
|
|
170
|
-
handleResponseFormatOptions(opts, res);
|
|
171
|
-
const slug = ((_a = res.payload) === null || _a === void 0 ? void 0 : _a.slug) || ((_b = res.payload) === null || _b === void 0 ? void 0 : _b.name) || opts.name;
|
|
172
|
-
spinner.success({ text: `Query saved — run with: ay agg exec ${slug}` });
|
|
173
|
-
}
|
|
174
|
-
catch (e) {
|
|
175
|
-
cliError(e.message || "Failed to save query", EXIT_GENERAL_ERROR);
|
|
176
|
-
}
|
|
177
|
-
});
|
|
178
|
-
// ─── ay agg validate ───────────────────────────────────────────
|
|
179
|
-
agg
|
|
180
|
-
.command("validate")
|
|
181
|
-
.alias("check")
|
|
182
|
-
.description("Validate a pipeline without executing")
|
|
183
|
-
.option("--pipeline <json>", "Pipeline as JSON array string")
|
|
184
|
-
.option("--file <path>", "Read pipeline from a JSON file")
|
|
185
|
-
.action(async (options) => {
|
|
186
|
-
var _a, _b, _c;
|
|
187
|
-
try {
|
|
188
|
-
const opts = { ...program.opts(), ...options };
|
|
189
|
-
const pipeline = await readPipelineInput(opts);
|
|
190
|
-
spinner.start({ text: "Validating pipeline...", color: "magenta" });
|
|
191
|
-
const res = await apiCallHandler("aggregation", "models/validate", "post", { pipeline });
|
|
192
|
-
spinner.stop();
|
|
193
|
-
const v = res.payload || res;
|
|
194
|
-
const icon = v.valid ? chalk.green("✓") : chalk.red("✗");
|
|
195
|
-
console.log(`\n ${icon} Pipeline is ${v.valid ? "valid" : "invalid"} (${(_a = v.stageCount) !== null && _a !== void 0 ? _a : pipeline.length} stages)`);
|
|
196
|
-
if ((_b = v.errors) === null || _b === void 0 ? void 0 : _b.length) {
|
|
197
|
-
for (const err of v.errors)
|
|
198
|
-
console.log(chalk.red(` - ${err}`));
|
|
199
|
-
}
|
|
200
|
-
if ((_c = v.warnings) === null || _c === void 0 ? void 0 : _c.length) {
|
|
201
|
-
for (const w of v.warnings)
|
|
202
|
-
console.log(chalk.yellow(` ! ${w}`));
|
|
203
|
-
}
|
|
204
|
-
console.log();
|
|
205
|
-
if (!v.valid)
|
|
206
|
-
process.exit(EXIT_GENERAL_ERROR);
|
|
207
|
-
}
|
|
208
|
-
catch (e) {
|
|
209
|
-
cliError(e.message || "Validation failed", EXIT_GENERAL_ERROR);
|
|
210
|
-
}
|
|
211
|
-
});
|
|
212
|
-
// ─── ay agg models [model] ─────────────────────────────────────
|
|
213
|
-
agg
|
|
214
|
-
.command("models [model]")
|
|
215
|
-
.description("List available models, or show fields for a specific model")
|
|
216
|
-
.addHelpText("after", `
|
|
217
|
-
Examples:
|
|
218
|
-
ay agg models List all available models
|
|
219
|
-
ay agg models consumers Show fields for Consumers`)
|
|
220
|
-
.action(async (model, options) => {
|
|
221
|
-
var _a, _b, _c, _d, _e;
|
|
222
|
-
try {
|
|
223
|
-
const opts = { ...program.opts(), ...options };
|
|
224
|
-
if (model) {
|
|
225
|
-
spinner.start({ text: `Loading fields for ${model}...`, color: "magenta" });
|
|
226
|
-
const res = await apiCallHandler("aggregation", `models/${model}/fields`, "get");
|
|
227
|
-
handleResponseFormatOptions(opts, res);
|
|
228
|
-
const fieldCount = (_c = (_b = (_a = res.payload) === null || _a === void 0 ? void 0 : _a.fields) === null || _b === void 0 ? void 0 : _b.length) !== null && _c !== void 0 ? _c : 0;
|
|
229
|
-
spinner.success({ text: `${model}: ${fieldCount} fields` });
|
|
230
|
-
}
|
|
231
|
-
else {
|
|
232
|
-
spinner.start({ text: "Loading models...", color: "magenta" });
|
|
233
|
-
const res = await apiCallHandler("aggregation", "models", "get");
|
|
234
|
-
handleResponseFormatOptions(opts, res);
|
|
235
|
-
const count = (_e = (_d = res.payload) === null || _d === void 0 ? void 0 : _d.length) !== null && _e !== void 0 ? _e : 0;
|
|
236
|
-
spinner.success({ text: `${count} models available` });
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
catch (e) {
|
|
240
|
-
cliError(e.message || "Failed to load models", EXIT_GENERAL_ERROR);
|
|
241
|
-
}
|
|
242
|
-
});
|
|
243
|
-
// ─── ay agg wizard ─────────────────────────────────────────────
|
|
244
|
-
agg
|
|
245
|
-
.command("wizard")
|
|
246
|
-
.alias("wiz")
|
|
247
|
-
.description("Interactive pipeline builder wizard")
|
|
248
|
-
.action(async (options) => {
|
|
249
|
-
var _a, _b, _c, _d, _e;
|
|
250
|
-
try {
|
|
251
|
-
const opts = { ...program.opts(), ...options };
|
|
252
|
-
if (!process.stdin.isTTY) {
|
|
253
|
-
cliError("Wizard requires an interactive terminal (TTY)", EXIT_MISUSE);
|
|
254
|
-
}
|
|
255
|
-
// Step 1: Select model
|
|
256
|
-
initializeSettings();
|
|
257
|
-
spinner.start({ text: "Loading models...", color: "magenta" });
|
|
258
|
-
const modelsRes = await apiCallHandler("aggregation", "models", "get");
|
|
259
|
-
spinner.stop();
|
|
260
|
-
const modelChoices = (modelsRes.payload || []).map((m) => ({
|
|
261
|
-
name: `${m.name} ${chalk.dim(`(${m.module})`)}`,
|
|
262
|
-
value: m.name,
|
|
263
|
-
}));
|
|
264
|
-
const { selectedModel } = await inquirer.prompt([
|
|
265
|
-
{
|
|
266
|
-
type: "search-list",
|
|
267
|
-
name: "selectedModel",
|
|
268
|
-
message: "Select a model:",
|
|
269
|
-
choices: modelChoices,
|
|
270
|
-
},
|
|
271
|
-
]);
|
|
272
|
-
// Step 2: Fetch and display fields
|
|
273
|
-
spinner.start({ text: `Loading fields for ${selectedModel}...`, color: "magenta" });
|
|
274
|
-
const fieldsRes = await apiCallHandler("aggregation", `models/${selectedModel}/fields`, "get");
|
|
275
|
-
spinner.stop();
|
|
276
|
-
const fields = ((_a = fieldsRes.payload) === null || _a === void 0 ? void 0 : _a.fields) || [];
|
|
277
|
-
const fieldNames = fields.map((f) => f.field);
|
|
278
|
-
console.log(chalk.cyan(`\n Fields for ${selectedModel}:\n`));
|
|
279
|
-
for (const f of fields) {
|
|
280
|
-
const ref = f.ref ? chalk.dim(` -> ${f.ref}`) : "";
|
|
281
|
-
const req = f.required ? chalk.yellow(" *") : "";
|
|
282
|
-
console.log(` ${chalk.white(f.field)} ${chalk.dim(`(${f.type})`)}${ref}${req}`);
|
|
283
|
-
}
|
|
284
|
-
console.log();
|
|
285
|
-
// Step 3: Build stages iteratively
|
|
286
|
-
const pipeline = [];
|
|
287
|
-
let addMore = true;
|
|
288
|
-
while (addMore) {
|
|
289
|
-
const { stageType } = await inquirer.prompt([
|
|
290
|
-
{
|
|
291
|
-
type: "list",
|
|
292
|
-
name: "stageType",
|
|
293
|
-
message: `Add stage ${pipeline.length + 1}:`,
|
|
294
|
-
choices: [
|
|
295
|
-
{ name: "$match — Filter documents", value: "$match" },
|
|
296
|
-
{ name: "$group — Group and aggregate", value: "$group" },
|
|
297
|
-
{ name: "$sort — Sort results", value: "$sort" },
|
|
298
|
-
{ name: "$project — Include/exclude fields", value: "$project" },
|
|
299
|
-
{ name: "$unwind — Deconstruct array field", value: "$unwind" },
|
|
300
|
-
{ name: "$lookup — Join with another collection", value: "$lookup" },
|
|
301
|
-
{ name: "$limit — Limit results", value: "$limit" },
|
|
302
|
-
{ name: "$skip — Skip results", value: "$skip" },
|
|
303
|
-
{ name: "$count — Count documents", value: "$count" },
|
|
304
|
-
{ name: "$addFields — Add computed fields", value: "$addFields" },
|
|
305
|
-
new inquirer.Separator(),
|
|
306
|
-
{ name: "Raw JSON stage", value: "$raw" },
|
|
307
|
-
new inquirer.Separator(),
|
|
308
|
-
{ name: chalk.green("Done — preview and execute"), value: "$done" },
|
|
309
|
-
],
|
|
310
|
-
},
|
|
311
|
-
]);
|
|
312
|
-
if (stageType === "$done") {
|
|
313
|
-
addMore = false;
|
|
314
|
-
break;
|
|
315
|
-
}
|
|
316
|
-
const stage = await buildStage(stageType, fieldNames, fields);
|
|
317
|
-
if (stage) {
|
|
318
|
-
pipeline.push(stage);
|
|
319
|
-
console.log(chalk.dim(` Added: ${JSON.stringify(stage)}\n`));
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
if (pipeline.length === 0) {
|
|
323
|
-
cliError("Pipeline is empty — add at least one stage", EXIT_MISUSE);
|
|
324
|
-
}
|
|
325
|
-
// Step 4: Preview
|
|
326
|
-
console.log(chalk.cyan("\n Pipeline:\n"));
|
|
327
|
-
console.log(chalk.dim(" " + JSON.stringify(pipeline, null, 2).split("\n").join("\n ")));
|
|
328
|
-
console.log();
|
|
329
|
-
spinner.start({ text: "Running preview (first 5 results)...", color: "magenta" });
|
|
330
|
-
const previewPipeline = [...pipeline, { $limit: 5 }];
|
|
331
|
-
const previewRes = await apiCallHandler("aggregation", selectedModel, "post", previewPipeline);
|
|
332
|
-
spinner.stop();
|
|
333
|
-
const previewWrapped = wrapAggResult(previewRes);
|
|
334
|
-
handleResponseFormatOptions({ ...opts, responseFormat: "yaml" }, previewWrapped);
|
|
335
|
-
// Step 5: Action choice
|
|
336
|
-
const { action } = await inquirer.prompt([
|
|
337
|
-
{
|
|
338
|
-
type: "list",
|
|
339
|
-
name: "action",
|
|
340
|
-
message: "What would you like to do?",
|
|
341
|
-
choices: [
|
|
342
|
-
{ name: "Execute full pipeline", value: "execute" },
|
|
343
|
-
{ name: "Save as reusable query", value: "save" },
|
|
344
|
-
{ name: "Execute and save", value: "both" },
|
|
345
|
-
{ name: "Print pipeline JSON", value: "print" },
|
|
346
|
-
{ name: "Cancel", value: "cancel" },
|
|
347
|
-
],
|
|
348
|
-
},
|
|
349
|
-
]);
|
|
350
|
-
if (action === "execute" || action === "both") {
|
|
351
|
-
spinner.start({ text: "Executing full pipeline...", color: "magenta" });
|
|
352
|
-
const res = await apiCallHandler("aggregation", selectedModel, "post", pipeline);
|
|
353
|
-
const wrapped = wrapAggResult(res);
|
|
354
|
-
handleResponseFormatOptions(opts, wrapped);
|
|
355
|
-
spinner.success({ text: `Pipeline returned ${(_c = (_b = wrapped.meta) === null || _b === void 0 ? void 0 : _b.resultCount) !== null && _c !== void 0 ? _c : "?"} results` });
|
|
356
|
-
}
|
|
357
|
-
if (action === "save" || action === "both") {
|
|
358
|
-
const { queryName } = await inquirer.prompt([
|
|
359
|
-
{
|
|
360
|
-
type: "input",
|
|
361
|
-
name: "queryName",
|
|
362
|
-
message: "Query name:",
|
|
363
|
-
validate: (v) => v.length > 0 || "Name is required",
|
|
364
|
-
},
|
|
365
|
-
]);
|
|
366
|
-
spinner.start({ text: "Saving query...", color: "magenta" });
|
|
367
|
-
const body = {
|
|
368
|
-
name: queryName,
|
|
369
|
-
dataSource: {
|
|
370
|
-
source: "aggregation",
|
|
371
|
-
aggregationModel: selectedModel,
|
|
372
|
-
aggregationPipeline: JSON.stringify(pipeline),
|
|
373
|
-
},
|
|
374
|
-
cache: { enabled: false },
|
|
375
|
-
};
|
|
376
|
-
const saveRes = await apiCallHandler("config", "queries", "post", body, {
|
|
377
|
-
responseFormat: "json",
|
|
378
|
-
});
|
|
379
|
-
const slug = ((_d = saveRes.payload) === null || _d === void 0 ? void 0 : _d.slug) || ((_e = saveRes.payload) === null || _e === void 0 ? void 0 : _e.name) || queryName;
|
|
380
|
-
spinner.success({ text: `Query saved — run with: ay agg exec ${slug}` });
|
|
381
|
-
}
|
|
382
|
-
if (action === "print") {
|
|
383
|
-
console.log(JSON.stringify(pipeline, null, 2));
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
catch (e) {
|
|
387
|
-
cliError(e.message || "Wizard failed", EXIT_GENERAL_ERROR);
|
|
388
|
-
}
|
|
389
|
-
});
|
|
390
|
-
}
|
|
391
|
-
// ─── Stage Builders ────────────────────────────────────────────────
|
|
392
|
-
async function buildStage(stageType, fieldNames, fields) {
|
|
393
|
-
switch (stageType) {
|
|
394
|
-
case "$match":
|
|
395
|
-
return buildMatchStage(fieldNames, fields);
|
|
396
|
-
case "$group":
|
|
397
|
-
return buildGroupStage(fieldNames);
|
|
398
|
-
case "$sort":
|
|
399
|
-
return buildSortStage(fieldNames);
|
|
400
|
-
case "$project":
|
|
401
|
-
return buildProjectStage(fieldNames);
|
|
402
|
-
case "$unwind":
|
|
403
|
-
return buildUnwindStage(fieldNames);
|
|
404
|
-
case "$lookup":
|
|
405
|
-
return buildLookupStage();
|
|
406
|
-
case "$limit":
|
|
407
|
-
return buildNumberStage("$limit", "Maximum documents to return:");
|
|
408
|
-
case "$skip":
|
|
409
|
-
return buildNumberStage("$skip", "Documents to skip:");
|
|
410
|
-
case "$count":
|
|
411
|
-
return buildCountStage();
|
|
412
|
-
case "$addFields":
|
|
413
|
-
return buildRawStage("$addFields");
|
|
414
|
-
case "$raw":
|
|
415
|
-
return buildFullRawStage();
|
|
416
|
-
default:
|
|
417
|
-
return null;
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
async function buildMatchStage(fieldNames, fields) {
|
|
421
|
-
const match = {};
|
|
422
|
-
let addMore = true;
|
|
423
|
-
while (addMore) {
|
|
424
|
-
const { field } = await inquirer.prompt([
|
|
425
|
-
{ type: "search-list", name: "field", message: "Field to match:", choices: fieldNames },
|
|
426
|
-
]);
|
|
427
|
-
const fieldMeta = fields.find((f) => f.field === field);
|
|
428
|
-
const isNumber = (fieldMeta === null || fieldMeta === void 0 ? void 0 : fieldMeta.type) === "Number";
|
|
429
|
-
const isDate = (fieldMeta === null || fieldMeta === void 0 ? void 0 : fieldMeta.type) === "Date";
|
|
430
|
-
const isBool = (fieldMeta === null || fieldMeta === void 0 ? void 0 : fieldMeta.type) === "Boolean";
|
|
431
|
-
const { operator } = await inquirer.prompt([
|
|
432
|
-
{
|
|
433
|
-
type: "list",
|
|
434
|
-
name: "operator",
|
|
435
|
-
message: "Operator:",
|
|
436
|
-
choices: [
|
|
437
|
-
{ name: "equals", value: "eq" },
|
|
438
|
-
{ name: "not equals", value: "$ne" },
|
|
439
|
-
{ name: "greater than", value: "$gt" },
|
|
440
|
-
{ name: "less than", value: "$lt" },
|
|
441
|
-
{ name: "greater or equal", value: "$gte" },
|
|
442
|
-
{ name: "less or equal", value: "$lte" },
|
|
443
|
-
{ name: "in (comma-separated)", value: "$in" },
|
|
444
|
-
{ name: "not in", value: "$nin" },
|
|
445
|
-
{ name: "regex", value: "$regex" },
|
|
446
|
-
{ name: "exists", value: "$exists" },
|
|
447
|
-
],
|
|
448
|
-
},
|
|
449
|
-
]);
|
|
450
|
-
let value;
|
|
451
|
-
if (operator === "$exists") {
|
|
452
|
-
const { exists } = await inquirer.prompt([
|
|
453
|
-
{ type: "confirm", name: "exists", message: "Field should exist?", default: true },
|
|
454
|
-
]);
|
|
455
|
-
value = exists;
|
|
456
|
-
match[field] = { $exists: value };
|
|
457
|
-
}
|
|
458
|
-
else if (isBool) {
|
|
459
|
-
const { boolVal } = await inquirer.prompt([
|
|
460
|
-
{ type: "confirm", name: "boolVal", message: `${field} =`, default: true },
|
|
461
|
-
]);
|
|
462
|
-
match[field] = operator === "eq" ? boolVal : { [operator]: boolVal };
|
|
463
|
-
}
|
|
464
|
-
else if (operator === "$in" || operator === "$nin") {
|
|
465
|
-
const { vals } = await inquirer.prompt([
|
|
466
|
-
{ type: "input", name: "vals", message: "Values (comma-separated):" },
|
|
467
|
-
]);
|
|
468
|
-
const arr = vals.split(",").map((v) => coerceValue(v.trim(), isNumber, isDate));
|
|
469
|
-
match[field] = { [operator]: arr };
|
|
470
|
-
}
|
|
471
|
-
else {
|
|
472
|
-
const { val } = await inquirer.prompt([
|
|
473
|
-
{ type: "input", name: "val", message: "Value:" },
|
|
474
|
-
]);
|
|
475
|
-
const coerced = coerceValue(val, isNumber, isDate);
|
|
476
|
-
match[field] = operator === "eq" ? coerced : { [operator]: coerced };
|
|
477
|
-
}
|
|
478
|
-
const { more } = await inquirer.prompt([
|
|
479
|
-
{ type: "confirm", name: "more", message: "Add another match condition?", default: false },
|
|
480
|
-
]);
|
|
481
|
-
addMore = more;
|
|
482
|
-
}
|
|
483
|
-
return { $match: match };
|
|
484
|
-
}
|
|
485
|
-
async function buildGroupStage(fieldNames) {
|
|
486
|
-
const { idType } = await inquirer.prompt([
|
|
487
|
-
{
|
|
488
|
-
type: "list",
|
|
489
|
-
name: "idType",
|
|
490
|
-
message: "Group by:",
|
|
491
|
-
choices: [
|
|
492
|
-
{ name: "Single field", value: "single" },
|
|
493
|
-
{ name: "Multiple fields", value: "multi" },
|
|
494
|
-
{ name: "null (aggregate all)", value: "null" },
|
|
495
|
-
],
|
|
496
|
-
},
|
|
497
|
-
]);
|
|
498
|
-
let _id = null;
|
|
499
|
-
if (idType === "single") {
|
|
500
|
-
const { field } = await inquirer.prompt([
|
|
501
|
-
{ type: "search-list", name: "field", message: "Group by field:", choices: fieldNames },
|
|
502
|
-
]);
|
|
503
|
-
_id = `$${field}`;
|
|
504
|
-
}
|
|
505
|
-
else if (idType === "multi") {
|
|
506
|
-
const { selected } = await inquirer.prompt([
|
|
507
|
-
{ type: "checkbox", name: "selected", message: "Select fields:", choices: fieldNames },
|
|
508
|
-
]);
|
|
509
|
-
_id = {};
|
|
510
|
-
for (const f of selected)
|
|
511
|
-
_id[f] = `$${f}`;
|
|
512
|
-
}
|
|
513
|
-
const group = { _id };
|
|
514
|
-
let addAcc = true;
|
|
515
|
-
while (addAcc) {
|
|
516
|
-
const { outputName } = await inquirer.prompt([
|
|
517
|
-
{ type: "input", name: "outputName", message: "Output field name:", validate: (v) => v.length > 0 },
|
|
518
|
-
]);
|
|
519
|
-
const { accumulator } = await inquirer.prompt([
|
|
520
|
-
{
|
|
521
|
-
type: "list",
|
|
522
|
-
name: "accumulator",
|
|
523
|
-
message: "Accumulator:",
|
|
524
|
-
choices: ["$sum", "$avg", "$min", "$max", "$count", "$push", "$first", "$last"],
|
|
525
|
-
},
|
|
526
|
-
]);
|
|
527
|
-
if (accumulator === "$count") {
|
|
528
|
-
group[outputName] = { $count: {} };
|
|
529
|
-
}
|
|
530
|
-
else {
|
|
531
|
-
const { srcField } = await inquirer.prompt([
|
|
532
|
-
{ type: "search-list", name: "srcField", message: "Source field:", choices: ["1 (constant)", ...fieldNames] },
|
|
533
|
-
]);
|
|
534
|
-
group[outputName] = { [accumulator]: srcField === "1 (constant)" ? 1 : `$${srcField}` };
|
|
535
|
-
}
|
|
536
|
-
const { more } = await inquirer.prompt([
|
|
537
|
-
{ type: "confirm", name: "more", message: "Add another accumulator?", default: false },
|
|
538
|
-
]);
|
|
539
|
-
addAcc = more;
|
|
540
|
-
}
|
|
541
|
-
return { $group: group };
|
|
542
|
-
}
|
|
543
|
-
async function buildSortStage(fieldNames) {
|
|
544
|
-
const sort = {};
|
|
545
|
-
let addMore = true;
|
|
546
|
-
while (addMore) {
|
|
547
|
-
const { field } = await inquirer.prompt([
|
|
548
|
-
{ type: "search-list", name: "field", message: "Sort by field:", choices: fieldNames },
|
|
549
|
-
]);
|
|
550
|
-
const { direction } = await inquirer.prompt([
|
|
551
|
-
{
|
|
552
|
-
type: "list",
|
|
553
|
-
name: "direction",
|
|
554
|
-
message: "Direction:",
|
|
555
|
-
choices: [
|
|
556
|
-
{ name: "Ascending (1)", value: 1 },
|
|
557
|
-
{ name: "Descending (-1)", value: -1 },
|
|
558
|
-
],
|
|
559
|
-
},
|
|
560
|
-
]);
|
|
561
|
-
sort[field] = direction;
|
|
562
|
-
const { more } = await inquirer.prompt([
|
|
563
|
-
{ type: "confirm", name: "more", message: "Add another sort field?", default: false },
|
|
564
|
-
]);
|
|
565
|
-
addMore = more;
|
|
566
|
-
}
|
|
567
|
-
return { $sort: sort };
|
|
568
|
-
}
|
|
569
|
-
async function buildProjectStage(fieldNames) {
|
|
570
|
-
const { mode } = await inquirer.prompt([
|
|
571
|
-
{
|
|
572
|
-
type: "list",
|
|
573
|
-
name: "mode",
|
|
574
|
-
message: "Projection mode:",
|
|
575
|
-
choices: [
|
|
576
|
-
{ name: "Include selected fields (1)", value: "include" },
|
|
577
|
-
{ name: "Exclude selected fields (0)", value: "exclude" },
|
|
578
|
-
],
|
|
579
|
-
},
|
|
580
|
-
]);
|
|
581
|
-
const { selected } = await inquirer.prompt([
|
|
582
|
-
{
|
|
583
|
-
type: "checkbox",
|
|
584
|
-
name: "selected",
|
|
585
|
-
message: `Fields to ${mode}:`,
|
|
586
|
-
choices: fieldNames,
|
|
587
|
-
validate: (v) => v.length > 0 || "Select at least one field",
|
|
588
|
-
},
|
|
589
|
-
]);
|
|
590
|
-
const project = {};
|
|
591
|
-
for (const f of selected)
|
|
592
|
-
project[f] = mode === "include" ? 1 : 0;
|
|
593
|
-
return { $project: project };
|
|
594
|
-
}
|
|
595
|
-
async function buildUnwindStage(fieldNames) {
|
|
596
|
-
const { field } = await inquirer.prompt([
|
|
597
|
-
{ type: "search-list", name: "field", message: "Array field to unwind:", choices: fieldNames },
|
|
598
|
-
]);
|
|
599
|
-
return { $unwind: `$${field}` };
|
|
600
|
-
}
|
|
601
|
-
async function buildLookupStage() {
|
|
602
|
-
const answers = await inquirer.prompt([
|
|
603
|
-
{ type: "input", name: "from", message: "Foreign collection (lowercase):" },
|
|
604
|
-
{ type: "input", name: "localField", message: "Local field:" },
|
|
605
|
-
{ type: "input", name: "foreignField", message: "Foreign field:", default: "_id" },
|
|
606
|
-
{ type: "input", name: "as", message: "Output array field name:" },
|
|
607
|
-
]);
|
|
608
|
-
return { $lookup: answers };
|
|
609
|
-
}
|
|
610
|
-
async function buildNumberStage(op, message) {
|
|
611
|
-
const { num } = await inquirer.prompt([
|
|
612
|
-
{ type: "input", name: "num", message, validate: (v) => !isNaN(parseInt(v)) || "Must be a number" },
|
|
613
|
-
]);
|
|
614
|
-
return { [op]: parseInt(num) };
|
|
615
|
-
}
|
|
616
|
-
async function buildCountStage() {
|
|
617
|
-
const { field } = await inquirer.prompt([
|
|
618
|
-
{ type: "input", name: "field", message: "Count output field name:", default: "count" },
|
|
619
|
-
]);
|
|
620
|
-
return { $count: field };
|
|
621
|
-
}
|
|
622
|
-
async function buildRawStage(op) {
|
|
623
|
-
const { json } = await inquirer.prompt([
|
|
624
|
-
{ type: "input", name: "json", message: `${op} body (JSON):` },
|
|
625
|
-
]);
|
|
626
|
-
try {
|
|
627
|
-
return { [op]: JSON.parse(json) };
|
|
628
|
-
}
|
|
629
|
-
catch (_a) {
|
|
630
|
-
console.log(chalk.red(" Invalid JSON — stage skipped"));
|
|
631
|
-
return null;
|
|
632
|
-
}
|
|
633
|
-
}
|
|
634
|
-
async function buildFullRawStage() {
|
|
635
|
-
const { json } = await inquirer.prompt([
|
|
636
|
-
{ type: "input", name: "json", message: "Full stage (JSON object):" },
|
|
637
|
-
]);
|
|
638
|
-
try {
|
|
639
|
-
return JSON.parse(json);
|
|
640
|
-
}
|
|
641
|
-
catch (_a) {
|
|
642
|
-
console.log(chalk.red(" Invalid JSON — stage skipped"));
|
|
643
|
-
return null;
|
|
644
|
-
}
|
|
645
|
-
}
|
|
646
|
-
function coerceValue(val, isNumber, isDate) {
|
|
647
|
-
if (val === "true")
|
|
648
|
-
return true;
|
|
649
|
-
if (val === "false")
|
|
650
|
-
return false;
|
|
651
|
-
if (val === "null")
|
|
652
|
-
return null;
|
|
653
|
-
if (isNumber && !isNaN(Number(val)))
|
|
654
|
-
return Number(val);
|
|
655
|
-
if (isDate && !isNaN(Date.parse(val)))
|
|
656
|
-
return { $date: val };
|
|
657
|
-
return val;
|
|
658
|
-
}
|
|
1
|
+
// Back-compat shim. The aggregate command tree was split into
|
|
2
|
+
// `./aggregate/*` for readability — this file just re-exports the parent
|
|
3
|
+
// factory so the lazy command registry (`_registry.ts`) and any external
|
|
4
|
+
// importers don't need to know about the directory layout.
|
|
5
|
+
export { createAggregateCommand } from "./aggregate/index.js";
|