@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.
Files changed (36) hide show
  1. package/lib/api/apiCallHandler.js +1 -1
  2. package/lib/api/apiClient.js +74 -62
  3. package/lib/commands/_registry.js +279 -0
  4. package/lib/commands/aggregate/_shared.js +21 -0
  5. package/lib/commands/aggregate/_stageBuilders.js +295 -0
  6. package/lib/commands/aggregate/exec.js +51 -0
  7. package/lib/commands/aggregate/index.js +38 -0
  8. package/lib/commands/aggregate/list.js +43 -0
  9. package/lib/commands/aggregate/models.js +43 -0
  10. package/lib/commands/aggregate/run.js +53 -0
  11. package/lib/commands/aggregate/save.js +53 -0
  12. package/lib/commands/aggregate/validate.js +47 -0
  13. package/lib/commands/aggregate/wizard.js +174 -0
  14. package/lib/commands/createAggregateCommand.js +5 -658
  15. package/lib/commands/createDeployCommand.js +5 -642
  16. package/lib/commands/createProgram.js +251 -161
  17. package/lib/commands/createServicesCommand.js +4 -5
  18. package/lib/commands/createWhoAmICommand.js +5 -5
  19. package/lib/commands/deploy/_token.js +8 -0
  20. package/lib/commands/deploy/alerts.js +43 -0
  21. package/lib/commands/deploy/clusters.js +62 -0
  22. package/lib/commands/deploy/dashboard.js +31 -0
  23. package/lib/commands/deploy/deployments.js +216 -0
  24. package/lib/commands/deploy/index.js +31 -0
  25. package/lib/commands/deploy/pipelines.js +82 -0
  26. package/lib/commands/deploy/plans.js +147 -0
  27. package/lib/commands/deploy/pods.js +70 -0
  28. package/lib/commands/deploy/repos.js +63 -0
  29. package/lib/helpers/dateFormat.js +119 -0
  30. package/lib/helpers/formatDocument.js +4 -5
  31. package/lib/helpers/logo.js +86 -13
  32. package/lib/helpers/saveFile.js +4 -9
  33. package/lib/models/getModelsInModules.js +6 -8
  34. package/lib/models/getModuleFromCollection.js +2 -2
  35. package/lib/operations/handleCollectionOperation.js +2 -3
  36. package/package.json +2 -12
@@ -0,0 +1,295 @@
1
+ // Pipeline stage builders for the `ay aggregate wizard` flow.
2
+ //
3
+ // Each `build*Stage` function drives an inquirer wizard that asks the user
4
+ // the questions appropriate for one stage type ($match, $group, $sort, etc.)
5
+ // and returns the assembled MongoDB stage object. They live in their own
6
+ // file because they're large (~200 lines combined), only used by the wizard
7
+ // subcommand, and would otherwise dominate `wizard.ts`.
8
+ //
9
+ // `coerceValue` handles "the user typed `42` but the field is a Number" / "the
10
+ // user typed `2026-04-01` but the field is a Date" — without coercion the
11
+ // pipeline would silently match against strings.
12
+ import inquirer from "inquirer";
13
+ import chalk from "chalk";
14
+ export async function buildStage(stageType, fieldNames, fields) {
15
+ switch (stageType) {
16
+ case "$match":
17
+ return buildMatchStage(fieldNames, fields);
18
+ case "$group":
19
+ return buildGroupStage(fieldNames);
20
+ case "$sort":
21
+ return buildSortStage(fieldNames);
22
+ case "$project":
23
+ return buildProjectStage(fieldNames);
24
+ case "$unwind":
25
+ return buildUnwindStage(fieldNames);
26
+ case "$lookup":
27
+ return buildLookupStage();
28
+ case "$limit":
29
+ return buildNumberStage("$limit", "Maximum documents to return:");
30
+ case "$skip":
31
+ return buildNumberStage("$skip", "Documents to skip:");
32
+ case "$count":
33
+ return buildCountStage();
34
+ case "$addFields":
35
+ return buildRawStage("$addFields");
36
+ case "$raw":
37
+ return buildFullRawStage();
38
+ default:
39
+ return null;
40
+ }
41
+ }
42
+ async function buildMatchStage(fieldNames, fields) {
43
+ const match = {};
44
+ let addMore = true;
45
+ while (addMore) {
46
+ const { field } = await inquirer.prompt([
47
+ { type: "search-list", name: "field", message: "Field to match:", choices: fieldNames },
48
+ ]);
49
+ const fieldMeta = fields.find((f) => f.field === field);
50
+ const isNumber = (fieldMeta === null || fieldMeta === void 0 ? void 0 : fieldMeta.type) === "Number";
51
+ const isDate = (fieldMeta === null || fieldMeta === void 0 ? void 0 : fieldMeta.type) === "Date";
52
+ const isBool = (fieldMeta === null || fieldMeta === void 0 ? void 0 : fieldMeta.type) === "Boolean";
53
+ const { operator } = await inquirer.prompt([
54
+ {
55
+ type: "list",
56
+ name: "operator",
57
+ message: "Operator:",
58
+ choices: [
59
+ { name: "equals", value: "eq" },
60
+ { name: "not equals", value: "$ne" },
61
+ { name: "greater than", value: "$gt" },
62
+ { name: "less than", value: "$lt" },
63
+ { name: "greater or equal", value: "$gte" },
64
+ { name: "less or equal", value: "$lte" },
65
+ { name: "in (comma-separated)", value: "$in" },
66
+ { name: "not in", value: "$nin" },
67
+ { name: "regex", value: "$regex" },
68
+ { name: "exists", value: "$exists" },
69
+ ],
70
+ },
71
+ ]);
72
+ let value;
73
+ if (operator === "$exists") {
74
+ const { exists } = await inquirer.prompt([
75
+ { type: "confirm", name: "exists", message: "Field should exist?", default: true },
76
+ ]);
77
+ value = exists;
78
+ match[field] = { $exists: value };
79
+ }
80
+ else if (isBool) {
81
+ const { boolVal } = await inquirer.prompt([
82
+ { type: "confirm", name: "boolVal", message: `${field} =`, default: true },
83
+ ]);
84
+ match[field] = operator === "eq" ? boolVal : { [operator]: boolVal };
85
+ }
86
+ else if (operator === "$in" || operator === "$nin") {
87
+ const { vals } = await inquirer.prompt([
88
+ { type: "input", name: "vals", message: "Values (comma-separated):" },
89
+ ]);
90
+ const arr = vals.split(",").map((v) => coerceValue(v.trim(), isNumber, isDate));
91
+ match[field] = { [operator]: arr };
92
+ }
93
+ else {
94
+ const { val } = await inquirer.prompt([
95
+ { type: "input", name: "val", message: "Value:" },
96
+ ]);
97
+ const coerced = coerceValue(val, isNumber, isDate);
98
+ match[field] = operator === "eq" ? coerced : { [operator]: coerced };
99
+ }
100
+ const { more } = await inquirer.prompt([
101
+ { type: "confirm", name: "more", message: "Add another match condition?", default: false },
102
+ ]);
103
+ addMore = more;
104
+ }
105
+ return { $match: match };
106
+ }
107
+ async function buildGroupStage(fieldNames) {
108
+ const { idType } = await inquirer.prompt([
109
+ {
110
+ type: "list",
111
+ name: "idType",
112
+ message: "Group by:",
113
+ choices: [
114
+ { name: "Single field", value: "single" },
115
+ { name: "Multiple fields", value: "multi" },
116
+ { name: "null (aggregate all)", value: "null" },
117
+ ],
118
+ },
119
+ ]);
120
+ let _id = null;
121
+ if (idType === "single") {
122
+ const { field } = await inquirer.prompt([
123
+ { type: "search-list", name: "field", message: "Group by field:", choices: fieldNames },
124
+ ]);
125
+ _id = `$${field}`;
126
+ }
127
+ else if (idType === "multi") {
128
+ const { selected } = await inquirer.prompt([
129
+ { type: "checkbox", name: "selected", message: "Select fields:", choices: fieldNames },
130
+ ]);
131
+ _id = {};
132
+ for (const f of selected)
133
+ _id[f] = `$${f}`;
134
+ }
135
+ const group = { _id };
136
+ let addAcc = true;
137
+ while (addAcc) {
138
+ const { outputName } = await inquirer.prompt([
139
+ {
140
+ type: "input",
141
+ name: "outputName",
142
+ message: "Output field name:",
143
+ validate: (v) => v.length > 0,
144
+ },
145
+ ]);
146
+ const { accumulator } = await inquirer.prompt([
147
+ {
148
+ type: "list",
149
+ name: "accumulator",
150
+ message: "Accumulator:",
151
+ choices: ["$sum", "$avg", "$min", "$max", "$count", "$push", "$first", "$last"],
152
+ },
153
+ ]);
154
+ if (accumulator === "$count") {
155
+ group[outputName] = { $count: {} };
156
+ }
157
+ else {
158
+ const { srcField } = await inquirer.prompt([
159
+ {
160
+ type: "search-list",
161
+ name: "srcField",
162
+ message: "Source field:",
163
+ choices: ["1 (constant)", ...fieldNames],
164
+ },
165
+ ]);
166
+ group[outputName] = { [accumulator]: srcField === "1 (constant)" ? 1 : `$${srcField}` };
167
+ }
168
+ const { more } = await inquirer.prompt([
169
+ { type: "confirm", name: "more", message: "Add another accumulator?", default: false },
170
+ ]);
171
+ addAcc = more;
172
+ }
173
+ return { $group: group };
174
+ }
175
+ async function buildSortStage(fieldNames) {
176
+ const sort = {};
177
+ let addMore = true;
178
+ while (addMore) {
179
+ const { field } = await inquirer.prompt([
180
+ { type: "search-list", name: "field", message: "Sort by field:", choices: fieldNames },
181
+ ]);
182
+ const { direction } = await inquirer.prompt([
183
+ {
184
+ type: "list",
185
+ name: "direction",
186
+ message: "Direction:",
187
+ choices: [
188
+ { name: "Ascending (1)", value: 1 },
189
+ { name: "Descending (-1)", value: -1 },
190
+ ],
191
+ },
192
+ ]);
193
+ sort[field] = direction;
194
+ const { more } = await inquirer.prompt([
195
+ { type: "confirm", name: "more", message: "Add another sort field?", default: false },
196
+ ]);
197
+ addMore = more;
198
+ }
199
+ return { $sort: sort };
200
+ }
201
+ async function buildProjectStage(fieldNames) {
202
+ const { mode } = await inquirer.prompt([
203
+ {
204
+ type: "list",
205
+ name: "mode",
206
+ message: "Projection mode:",
207
+ choices: [
208
+ { name: "Include selected fields (1)", value: "include" },
209
+ { name: "Exclude selected fields (0)", value: "exclude" },
210
+ ],
211
+ },
212
+ ]);
213
+ const { selected } = await inquirer.prompt([
214
+ {
215
+ type: "checkbox",
216
+ name: "selected",
217
+ message: `Fields to ${mode}:`,
218
+ choices: fieldNames,
219
+ validate: (v) => v.length > 0 || "Select at least one field",
220
+ },
221
+ ]);
222
+ const project = {};
223
+ for (const f of selected)
224
+ project[f] = mode === "include" ? 1 : 0;
225
+ return { $project: project };
226
+ }
227
+ async function buildUnwindStage(fieldNames) {
228
+ const { field } = await inquirer.prompt([
229
+ { type: "search-list", name: "field", message: "Array field to unwind:", choices: fieldNames },
230
+ ]);
231
+ return { $unwind: `$${field}` };
232
+ }
233
+ async function buildLookupStage() {
234
+ const answers = await inquirer.prompt([
235
+ { type: "input", name: "from", message: "Foreign collection (lowercase):" },
236
+ { type: "input", name: "localField", message: "Local field:" },
237
+ { type: "input", name: "foreignField", message: "Foreign field:", default: "_id" },
238
+ { type: "input", name: "as", message: "Output array field name:" },
239
+ ]);
240
+ return { $lookup: answers };
241
+ }
242
+ async function buildNumberStage(op, message) {
243
+ const { num } = await inquirer.prompt([
244
+ {
245
+ type: "input",
246
+ name: "num",
247
+ message,
248
+ validate: (v) => !isNaN(parseInt(v)) || "Must be a number",
249
+ },
250
+ ]);
251
+ return { [op]: parseInt(num) };
252
+ }
253
+ async function buildCountStage() {
254
+ const { field } = await inquirer.prompt([
255
+ { type: "input", name: "field", message: "Count output field name:", default: "count" },
256
+ ]);
257
+ return { $count: field };
258
+ }
259
+ async function buildRawStage(op) {
260
+ const { json } = await inquirer.prompt([
261
+ { type: "input", name: "json", message: `${op} body (JSON):` },
262
+ ]);
263
+ try {
264
+ return { [op]: JSON.parse(json) };
265
+ }
266
+ catch (_a) {
267
+ console.log(chalk.red(" Invalid JSON — stage skipped"));
268
+ return null;
269
+ }
270
+ }
271
+ async function buildFullRawStage() {
272
+ const { json } = await inquirer.prompt([
273
+ { type: "input", name: "json", message: "Full stage (JSON object):" },
274
+ ]);
275
+ try {
276
+ return JSON.parse(json);
277
+ }
278
+ catch (_a) {
279
+ console.log(chalk.red(" Invalid JSON — stage skipped"));
280
+ return null;
281
+ }
282
+ }
283
+ function coerceValue(val, isNumber, isDate) {
284
+ if (val === "true")
285
+ return true;
286
+ if (val === "false")
287
+ return false;
288
+ if (val === "null")
289
+ return null;
290
+ if (isNumber && !isNaN(Number(val)))
291
+ return Number(val);
292
+ if (isDate && !isNaN(Date.parse(val)))
293
+ return { $date: val };
294
+ return val;
295
+ }
@@ -0,0 +1,51 @@
1
+ // `ay aggregate exec <slugOrId>` — execute a saved query by slug or ObjectId.
2
+ //
3
+ // Two-step lookup when given a slug: first GET /config/queries?slug=... to
4
+ // resolve the query's _id, then POST /aggregation/queries/execute/{id}. When
5
+ // given an ObjectId directly we skip the lookup. Returned `wrapAggResult`
6
+ // shape so output formatting matches `ay agg run`.
7
+ import { apiCallHandler } from "../../api/apiCallHandler.js";
8
+ import { handleResponseFormatOptions } from "../../helpers/handleResponseFormatOptions.js";
9
+ import { spinner } from "../../../index.js";
10
+ import { EXIT_GENERAL_ERROR } from "../../exitCodes.js";
11
+ import { cliError } from "../../helpers/cliError.js";
12
+ import { wrapAggResult } from "./_shared.js";
13
+ export function addExecSubcommand(agg, rootProgram) {
14
+ agg
15
+ .command("exec <slugOrId>")
16
+ .description("Execute a saved query by slug name or ObjectId")
17
+ .addHelpText("after", `
18
+ Examples:
19
+ ay agg exec monthly-revenue -r table
20
+ ay agg exec 507f1f77bcf86cd799439011 -r json --quiet`)
21
+ .action(async (slugOrId, options) => {
22
+ var _a, _b, _c, _d;
23
+ try {
24
+ const opts = { ...rootProgram.opts(), ...options };
25
+ const isObjectId = /^[a-f0-9]{24}$/i.test(slugOrId);
26
+ spinner.start({ text: `Executing query "${slugOrId}"...`, color: "magenta" });
27
+ let queryId = slugOrId;
28
+ if (!isObjectId) {
29
+ // Resolve slug to ID via config-api
30
+ const lookup = await apiCallHandler("config", "queries", "get", null, {
31
+ slug: slugOrId,
32
+ limit: 1,
33
+ responseFormat: "json",
34
+ });
35
+ const entries = lookup === null || lookup === void 0 ? void 0 : lookup.payload;
36
+ if (!entries || (Array.isArray(entries) && entries.length === 0)) {
37
+ cliError(`No query found with slug "${slugOrId}"`, EXIT_GENERAL_ERROR);
38
+ }
39
+ queryId = Array.isArray(entries) ? entries[0]._id : entries._id;
40
+ }
41
+ const res = await apiCallHandler("aggregation", `queries/execute/${queryId}`, "post");
42
+ const wrapped = wrapAggResult(res);
43
+ handleResponseFormatOptions(opts, wrapped);
44
+ 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 : "?";
45
+ spinner.success({ text: `Query returned ${count} results` });
46
+ }
47
+ catch (e) {
48
+ cliError(e.message || "Query execution failed", EXIT_GENERAL_ERROR);
49
+ }
50
+ });
51
+ }
@@ -0,0 +1,38 @@
1
+ // `ay aggregate` parent command — aggregator for the seven aggregation
2
+ // subcommands. Each subcommand lives in its own file in this directory; this
3
+ // file just instantiates the parent and wires the children.
4
+ //
5
+ // Why split: the original `createAggregateCommand.ts` was 720 lines with all
6
+ // seven subcommands inline + the wizard's stage builders. The split makes
7
+ // each subcommand independently readable, makes the wizard's complex
8
+ // stage-builder logic easy to find, and lets `ay agg run` skip the import
9
+ // cost of inquirer + every stage builder it doesn't need.
10
+ import { addRunSubcommand } from "./run.js";
11
+ import { addExecSubcommand } from "./exec.js";
12
+ import { addListSubcommand } from "./list.js";
13
+ import { addSaveSubcommand } from "./save.js";
14
+ import { addValidateSubcommand } from "./validate.js";
15
+ import { addModelsSubcommand } from "./models.js";
16
+ import { addWizardSubcommand } from "./wizard.js";
17
+ export function createAggregateCommand(program) {
18
+ const agg = program
19
+ .command("aggregate")
20
+ .alias("agg")
21
+ .description("Run MongoDB aggregation pipelines, manage saved queries, and explore models")
22
+ .addHelpText("after", `
23
+ Examples:
24
+ ay agg run consumers --pipeline '[{"$count":"total"}]'
25
+ ay agg wizard
26
+ ay agg exec monthly-revenue -r table
27
+ ay agg list
28
+ ay agg save --name "Active Users" --model consumers --pipeline '[{"$match":{"status":"active"}}]'
29
+ ay agg validate --pipeline '[{"$group":{"_id":"$type","count":{"$sum":1}}}]'
30
+ ay agg models consumers`);
31
+ addRunSubcommand(agg, program);
32
+ addExecSubcommand(agg, program);
33
+ addListSubcommand(agg, program);
34
+ addSaveSubcommand(agg, program);
35
+ addValidateSubcommand(agg, program);
36
+ addModelsSubcommand(agg, program);
37
+ addWizardSubcommand(agg, program);
38
+ }
@@ -0,0 +1,43 @@
1
+ // `ay aggregate list` — list saved aggregation queries via config-api.
2
+ //
3
+ // Saved queries live in the `queries` collection on config-api. We filter by
4
+ // `dataSource.source=aggregation` so only aggregation-flavored queries
5
+ // surface (config-api also stores SQL/REST queries which would otherwise
6
+ // pollute the listing).
7
+ import { apiCallHandler } from "../../api/apiCallHandler.js";
8
+ import { handleResponseFormatOptions } from "../../helpers/handleResponseFormatOptions.js";
9
+ import { spinner } from "../../../index.js";
10
+ import { EXIT_GENERAL_ERROR } from "../../exitCodes.js";
11
+ import { cliError } from "../../helpers/cliError.js";
12
+ export function addListSubcommand(agg, rootProgram) {
13
+ agg
14
+ .command("list")
15
+ .alias("ls")
16
+ .description("List saved aggregation queries")
17
+ .option("-l, --limit <number>", "Limit results", parseInt, 50)
18
+ .option("-p, --page <number>", "Page number", parseInt, 1)
19
+ .option("--search <term>", "Search by name")
20
+ .action(async (options) => {
21
+ var _a, _b, _c, _d, _e;
22
+ try {
23
+ const opts = { ...rootProgram.opts(), ...options };
24
+ spinner.start({ text: "Fetching queries...", color: "magenta" });
25
+ const params = {
26
+ page: opts.page,
27
+ limit: opts.limit,
28
+ responseFormat: opts.responseFormat,
29
+ verbosity: opts.verbosity,
30
+ "dataSource.source": "aggregation",
31
+ };
32
+ if (opts.search)
33
+ params.q = opts.search;
34
+ const res = await apiCallHandler("config", "queries", "get", null, params);
35
+ handleResponseFormatOptions(opts, res);
36
+ 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;
37
+ spinner.success({ text: `Found ${total} queries` });
38
+ }
39
+ catch (e) {
40
+ cliError(e.message || "Failed to list queries", EXIT_GENERAL_ERROR);
41
+ }
42
+ });
43
+ }
@@ -0,0 +1,43 @@
1
+ // `ay aggregate models [model]` — discover aggregation models and their fields.
2
+ //
3
+ // Without an argument, lists all models the user has access to. With a model
4
+ // name, returns the field schema (name, type, ref, required) for that model
5
+ // — used by the wizard to populate field-selection prompts, but also useful
6
+ // standalone for ad-hoc exploration.
7
+ import { apiCallHandler } from "../../api/apiCallHandler.js";
8
+ import { handleResponseFormatOptions } from "../../helpers/handleResponseFormatOptions.js";
9
+ import { spinner } from "../../../index.js";
10
+ import { EXIT_GENERAL_ERROR } from "../../exitCodes.js";
11
+ import { cliError } from "../../helpers/cliError.js";
12
+ export function addModelsSubcommand(agg, rootProgram) {
13
+ agg
14
+ .command("models [model]")
15
+ .description("List available models, or show fields for a specific model")
16
+ .addHelpText("after", `
17
+ Examples:
18
+ ay agg models List all available models
19
+ ay agg models consumers Show fields for Consumers`)
20
+ .action(async (model, options) => {
21
+ var _a, _b, _c, _d, _e;
22
+ try {
23
+ const opts = { ...rootProgram.opts(), ...options };
24
+ if (model) {
25
+ spinner.start({ text: `Loading fields for ${model}...`, color: "magenta" });
26
+ const res = await apiCallHandler("aggregation", `models/${model}/fields`, "get");
27
+ handleResponseFormatOptions(opts, res);
28
+ 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;
29
+ spinner.success({ text: `${model}: ${fieldCount} fields` });
30
+ }
31
+ else {
32
+ spinner.start({ text: "Loading models...", color: "magenta" });
33
+ const res = await apiCallHandler("aggregation", "models", "get");
34
+ handleResponseFormatOptions(opts, res);
35
+ const count = (_e = (_d = res.payload) === null || _d === void 0 ? void 0 : _d.length) !== null && _e !== void 0 ? _e : 0;
36
+ spinner.success({ text: `${count} models available` });
37
+ }
38
+ }
39
+ catch (e) {
40
+ cliError(e.message || "Failed to load models", EXIT_GENERAL_ERROR);
41
+ }
42
+ });
43
+ }
@@ -0,0 +1,53 @@
1
+ // `ay aggregate run <model>` — execute a pipeline against a specific model.
2
+ //
3
+ // The pipeline can come from --pipeline (JSON string), --file (path), or
4
+ // stdin. `readPipelineInput` handles all three. We POST to `/aggregation/{model}`
5
+ // with the pipeline as the body and surface optional flags (--global,
6
+ // --replace, --resultAsObject, --random) via query params.
7
+ import { apiCallHandler } from "../../api/apiCallHandler.js";
8
+ import { handleResponseFormatOptions } from "../../helpers/handleResponseFormatOptions.js";
9
+ import { spinner } from "../../../index.js";
10
+ import { EXIT_GENERAL_ERROR } from "../../exitCodes.js";
11
+ import { cliError } from "../../helpers/cliError.js";
12
+ import { readPipelineInput } from "../../helpers/readPipelineInput.js";
13
+ import { wrapAggResult } from "./_shared.js";
14
+ export function addRunSubcommand(agg, rootProgram) {
15
+ agg
16
+ .command("run <model>")
17
+ .description("Execute an aggregation pipeline on a model")
18
+ .option("--pipeline <json>", "Pipeline as JSON array string")
19
+ .option("--file <path>", "Read pipeline from a JSON file")
20
+ .option("--global", "Skip tenant filter (superuser only)")
21
+ .option("--replace", "Enable template variable replacement")
22
+ .option("--resultAsObject", "Return result as single object")
23
+ .option("--random", "Return one random result")
24
+ .addHelpText("after", `
25
+ Examples:
26
+ ay agg run consumers --pipeline '[{"$match":{"status":"active"}},{"$count":"total"}]'
27
+ ay agg run orders --file pipeline.json -r table
28
+ cat pipeline.json | ay agg run products`)
29
+ .action(async (model, options) => {
30
+ var _a, _b;
31
+ try {
32
+ const opts = { ...rootProgram.opts(), ...options };
33
+ const pipeline = await readPipelineInput(opts);
34
+ spinner.start({ text: `Executing pipeline on ${model}...`, color: "magenta" });
35
+ const params = {};
36
+ if (opts.global)
37
+ params.global = true;
38
+ if (opts.replace)
39
+ params.replace = true;
40
+ if (opts.resultAsObject)
41
+ params.resultAsObject = true;
42
+ if (opts.random)
43
+ params.random = true;
44
+ const res = await apiCallHandler("aggregation", model, "post", pipeline, params);
45
+ const wrapped = wrapAggResult(res);
46
+ handleResponseFormatOptions(opts, wrapped);
47
+ spinner.success({ text: `Pipeline returned ${(_b = (_a = wrapped.meta) === null || _a === void 0 ? void 0 : _a.resultCount) !== null && _b !== void 0 ? _b : "?"} results` });
48
+ }
49
+ catch (e) {
50
+ cliError(e.message || "Pipeline execution failed", EXIT_GENERAL_ERROR);
51
+ }
52
+ });
53
+ }
@@ -0,0 +1,53 @@
1
+ // `ay aggregate save` — persist a pipeline as a reusable query in config-api.
2
+ //
3
+ // The query body wraps the pipeline JSON inside the `dataSource` shape that
4
+ // the config-api `queries` collection expects (source = "aggregation",
5
+ // aggregationModel = the target model name, aggregationPipeline = the
6
+ // pipeline serialized as a JSON string). Caching is disabled by default
7
+ // because the wizard / one-off `save` flow shouldn't memoize results
8
+ // silently — users can flip caching on later via the web UI or `ay edit`.
9
+ import { apiCallHandler } from "../../api/apiCallHandler.js";
10
+ import { handleResponseFormatOptions } from "../../helpers/handleResponseFormatOptions.js";
11
+ import { spinner } from "../../../index.js";
12
+ import { EXIT_GENERAL_ERROR } from "../../exitCodes.js";
13
+ import { cliError } from "../../helpers/cliError.js";
14
+ import { readPipelineInput } from "../../helpers/readPipelineInput.js";
15
+ export function addSaveSubcommand(agg, rootProgram) {
16
+ agg
17
+ .command("save")
18
+ .description("Save a pipeline as a reusable query")
19
+ .requiredOption("--name <name>", "Query name")
20
+ .requiredOption("--model <model>", "Aggregation model name")
21
+ .option("--pipeline <json>", "Pipeline as JSON array string")
22
+ .option("--file <path>", "Read pipeline from a JSON file")
23
+ .addHelpText("after", `
24
+ Examples:
25
+ ay agg save --name "Monthly Revenue" --model orders --pipeline '[{"$group":{"_id":null,"total":{"$sum":"$amount"}}}]'
26
+ ay agg save --name "Active Users" --model consumers --file pipeline.json`)
27
+ .action(async (options) => {
28
+ var _a, _b;
29
+ try {
30
+ const opts = { ...rootProgram.opts(), ...options };
31
+ const pipeline = await readPipelineInput(opts);
32
+ spinner.start({ text: "Saving query...", color: "magenta" });
33
+ const body = {
34
+ name: opts.name,
35
+ dataSource: {
36
+ source: "aggregation",
37
+ aggregationModel: opts.model,
38
+ aggregationPipeline: JSON.stringify(pipeline),
39
+ },
40
+ cache: { enabled: false },
41
+ };
42
+ const res = await apiCallHandler("config", "queries", "post", body, {
43
+ responseFormat: opts.responseFormat,
44
+ });
45
+ handleResponseFormatOptions(opts, res);
46
+ 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;
47
+ spinner.success({ text: `Query saved — run with: ay agg exec ${slug}` });
48
+ }
49
+ catch (e) {
50
+ cliError(e.message || "Failed to save query", EXIT_GENERAL_ERROR);
51
+ }
52
+ });
53
+ }
@@ -0,0 +1,47 @@
1
+ // `ay aggregate validate` — server-side validation without execution.
2
+ //
3
+ // Calls `/aggregation/models/validate` which type-checks the pipeline against
4
+ // the model schema and returns `{ valid, stageCount, errors[], warnings[] }`.
5
+ // Useful in CI to lint pipeline JSON files before merging. Exits non-zero if
6
+ // the pipeline is invalid.
7
+ import chalk from "chalk";
8
+ import { apiCallHandler } from "../../api/apiCallHandler.js";
9
+ import { spinner } from "../../../index.js";
10
+ import { EXIT_GENERAL_ERROR } from "../../exitCodes.js";
11
+ import { cliError } from "../../helpers/cliError.js";
12
+ import { readPipelineInput } from "../../helpers/readPipelineInput.js";
13
+ export function addValidateSubcommand(agg, rootProgram) {
14
+ agg
15
+ .command("validate")
16
+ .alias("check")
17
+ .description("Validate a pipeline without executing")
18
+ .option("--pipeline <json>", "Pipeline as JSON array string")
19
+ .option("--file <path>", "Read pipeline from a JSON file")
20
+ .action(async (options) => {
21
+ var _a, _b, _c;
22
+ try {
23
+ const opts = { ...rootProgram.opts(), ...options };
24
+ const pipeline = await readPipelineInput(opts);
25
+ spinner.start({ text: "Validating pipeline...", color: "magenta" });
26
+ const res = await apiCallHandler("aggregation", "models/validate", "post", { pipeline });
27
+ spinner.stop();
28
+ const v = res.payload || res;
29
+ const icon = v.valid ? chalk.green("✓") : chalk.red("✗");
30
+ console.log(`\n ${icon} Pipeline is ${v.valid ? "valid" : "invalid"} (${(_a = v.stageCount) !== null && _a !== void 0 ? _a : pipeline.length} stages)`);
31
+ if ((_b = v.errors) === null || _b === void 0 ? void 0 : _b.length) {
32
+ for (const err of v.errors)
33
+ console.log(chalk.red(` - ${err}`));
34
+ }
35
+ if ((_c = v.warnings) === null || _c === void 0 ? void 0 : _c.length) {
36
+ for (const w of v.warnings)
37
+ console.log(chalk.yellow(` ! ${w}`));
38
+ }
39
+ console.log();
40
+ if (!v.valid)
41
+ process.exit(EXIT_GENERAL_ERROR);
42
+ }
43
+ catch (e) {
44
+ cliError(e.message || "Validation failed", EXIT_GENERAL_ERROR);
45
+ }
46
+ });
47
+ }