Files
oneuptime/CLI/Commands/ResourceCommands.ts
Nawaz Dhandala d25a97fe17 Refactor components for improved readability and consistency
- Added missing newlines at the end of files in MarkdownContent.tsx and RootCauseCard.tsx
- Reformatted shadowColor and color properties in NotesSection.tsx, SegmentedControl.tsx, MainTabNavigator.tsx, HomeScreen.tsx for better readability
- Enhanced code formatting in SectionHeader.tsx and OnCallStackNavigator.tsx for consistency
- Improved readability of getEntityId function in useAllProjectOnCallPolicies.ts
- Refactored conditional rendering in AlertDetailScreen.tsx, AlertEpisodeDetailScreen.tsx, IncidentDetailScreen.tsx, and IncidentEpisodeDetailScreen.tsx for better clarity
2026-02-15 11:47:32 +00:00

357 lines
12 KiB
TypeScript

import { Command } from "commander";
import DatabaseModels from "Common/Models/DatabaseModels/Index";
import AnalyticsModels from "Common/Models/AnalyticsModels/Index";
import BaseModel from "Common/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel";
import AnalyticsBaseModel from "Common/Models/AnalyticsModels/AnalyticsBaseModel/AnalyticsBaseModel";
import { ResourceInfo, ResolvedCredentials } from "../Types/CLITypes";
import { executeApiRequest, ApiOperation } from "../Core/ApiClient";
import { CLIOptions, getResolvedCredentials } from "../Core/ConfigManager";
import { formatOutput, printSuccess } from "../Core/OutputFormatter";
import { handleError } from "../Core/ErrorHandler";
import { generateAllFieldsSelect } from "../Utils/SelectFieldGenerator";
import { JSONObject, JSONValue } from "Common/Types/JSON";
import * as fs from "fs";
function toKebabCase(str: string): string {
return str
.replace(/([a-z])([A-Z])/g, "$1-$2")
.replace(/[\s_]+/g, "-")
.toLowerCase();
}
function parseJsonArg(value: string): JSONObject {
try {
return JSON.parse(value) as JSONObject;
} catch {
throw new Error(`Invalid JSON: ${value}`);
}
}
export function discoverResources(): ResourceInfo[] {
const resources: ResourceInfo[] = [];
// Database models
for (const ModelClass of DatabaseModels) {
try {
const model: BaseModel = new ModelClass();
const tableName: string = model.tableName || ModelClass.name;
const singularName: string = model.singularName || tableName;
const pluralName: string = model.pluralName || `${singularName}s`;
const apiPath: string | undefined = model.crudApiPath?.toString();
if (tableName && model.enableMCP && apiPath) {
resources.push({
name: toKebabCase(singularName),
singularName,
pluralName,
apiPath,
tableName,
modelType: "database",
});
}
} catch {
// Skip models that fail to instantiate
}
}
// Analytics models
for (const ModelClass of AnalyticsModels) {
try {
const model: AnalyticsBaseModel = new ModelClass();
const tableName: string = model.tableName || ModelClass.name;
const singularName: string = model.singularName || tableName;
const pluralName: string = model.pluralName || `${singularName}s`;
const apiPath: string | undefined = model.crudApiPath?.toString();
if (tableName && model.enableMCP && apiPath) {
resources.push({
name: toKebabCase(singularName),
singularName,
pluralName,
apiPath,
tableName,
modelType: "analytics",
});
}
} catch {
// Skip models that fail to instantiate
}
}
return resources;
}
function getParentOptions(cmd: Command): CLIOptions {
// Walk up to root program to get global options
let current: Command | null = cmd;
while (current?.parent) {
current = current.parent;
}
const opts: Record<string, unknown> = current?.opts() || {};
return {
apiKey: opts["apiKey"] as string | undefined,
url: opts["url"] as string | undefined,
context: opts["context"] as string | undefined,
};
}
function registerListCommand(
resourceCmd: Command,
resource: ResourceInfo,
): void {
resourceCmd
.command("list")
.description(`List ${resource.pluralName}`)
.option("--query <json>", "Filter query as JSON")
.option("--limit <n>", "Max results to return", "10")
.option("--skip <n>", "Number of results to skip", "0")
.option("--sort <json>", "Sort order as JSON")
.option("-o, --output <format>", "Output format: json, table, wide")
.action(
async (options: {
query?: string;
limit: string;
skip: string;
sort?: string;
output?: string;
}) => {
try {
const parentOpts: CLIOptions = getParentOptions(resourceCmd);
const creds: ResolvedCredentials = getResolvedCredentials(parentOpts);
const select: JSONObject = generateAllFieldsSelect(
resource.tableName,
resource.modelType,
);
const result: JSONValue = await executeApiRequest({
apiUrl: creds.apiUrl,
apiKey: creds.apiKey,
apiPath: resource.apiPath,
operation: "list" as ApiOperation,
query: options.query ? parseJsonArg(options.query) : undefined,
select,
skip: parseInt(options.skip, 10),
limit: parseInt(options.limit, 10),
sort: options.sort ? parseJsonArg(options.sort) : undefined,
});
// Extract data array from response
const responseData: JSONValue =
result && typeof result === "object" && !Array.isArray(result)
? ((result as JSONObject)["data"] as JSONValue) || result
: result;
// eslint-disable-next-line no-console
console.log(formatOutput(responseData, options.output));
} catch (error) {
handleError(error);
}
},
);
}
function registerGetCommand(
resourceCmd: Command,
resource: ResourceInfo,
): void {
resourceCmd
.command("get <id>")
.description(`Get a single ${resource.singularName} by ID`)
.option("-o, --output <format>", "Output format: json, table, wide")
.action(async (id: string, options: { output?: string }) => {
try {
const parentOpts: CLIOptions = getParentOptions(resourceCmd);
const creds: ResolvedCredentials = getResolvedCredentials(parentOpts);
const select: JSONObject = generateAllFieldsSelect(
resource.tableName,
resource.modelType,
);
const result: JSONValue = await executeApiRequest({
apiUrl: creds.apiUrl,
apiKey: creds.apiKey,
apiPath: resource.apiPath,
operation: "read" as ApiOperation,
id,
select,
});
// eslint-disable-next-line no-console
console.log(formatOutput(result, options.output));
} catch (error) {
handleError(error);
}
});
}
function registerCreateCommand(
resourceCmd: Command,
resource: ResourceInfo,
): void {
resourceCmd
.command("create")
.description(`Create a new ${resource.singularName}`)
.option("--data <json>", "Resource data as JSON")
.option("--file <path>", "Read resource data from a JSON file")
.option("-o, --output <format>", "Output format: json, table, wide")
.action(
async (options: { data?: string; file?: string; output?: string }) => {
try {
let data: JSONObject;
if (options.file) {
const fileContent: string = fs.readFileSync(options.file, "utf-8");
data = JSON.parse(fileContent) as JSONObject;
} else if (options.data) {
data = parseJsonArg(options.data);
} else {
throw new Error("Either --data or --file is required for create.");
}
const parentOpts: CLIOptions = getParentOptions(resourceCmd);
const creds: ResolvedCredentials = getResolvedCredentials(parentOpts);
const result: JSONValue = await executeApiRequest({
apiUrl: creds.apiUrl,
apiKey: creds.apiKey,
apiPath: resource.apiPath,
operation: "create" as ApiOperation,
data,
});
// eslint-disable-next-line no-console
console.log(formatOutput(result, options.output));
} catch (error) {
handleError(error);
}
},
);
}
function registerUpdateCommand(
resourceCmd: Command,
resource: ResourceInfo,
): void {
resourceCmd
.command("update <id>")
.description(`Update an existing ${resource.singularName}`)
.requiredOption("--data <json>", "Fields to update as JSON")
.option("-o, --output <format>", "Output format: json, table, wide")
.action(async (id: string, options: { data: string; output?: string }) => {
try {
const data: JSONObject = parseJsonArg(options.data);
const parentOpts: CLIOptions = getParentOptions(resourceCmd);
const creds: ResolvedCredentials = getResolvedCredentials(parentOpts);
const result: JSONValue = await executeApiRequest({
apiUrl: creds.apiUrl,
apiKey: creds.apiKey,
apiPath: resource.apiPath,
operation: "update" as ApiOperation,
id,
data,
});
// eslint-disable-next-line no-console
console.log(formatOutput(result, options.output));
} catch (error) {
handleError(error);
}
});
}
function registerDeleteCommand(
resourceCmd: Command,
resource: ResourceInfo,
): void {
resourceCmd
.command("delete <id>")
.description(`Delete a ${resource.singularName}`)
.option("--force", "Skip confirmation")
.action(async (id: string, _options: { force?: boolean }) => {
try {
const parentOpts: CLIOptions = getParentOptions(resourceCmd);
const creds: ResolvedCredentials = getResolvedCredentials(parentOpts);
await executeApiRequest({
apiUrl: creds.apiUrl,
apiKey: creds.apiKey,
apiPath: resource.apiPath,
operation: "delete" as ApiOperation,
id,
});
printSuccess(`${resource.singularName} ${id} deleted successfully.`);
} catch (error) {
handleError(error);
}
});
}
function registerCountCommand(
resourceCmd: Command,
resource: ResourceInfo,
): void {
resourceCmd
.command("count")
.description(`Count ${resource.pluralName}`)
.option("--query <json>", "Filter query as JSON")
.action(async (options: { query?: string }) => {
try {
const parentOpts: CLIOptions = getParentOptions(resourceCmd);
const creds: ResolvedCredentials = getResolvedCredentials(parentOpts);
const result: JSONValue = await executeApiRequest({
apiUrl: creds.apiUrl,
apiKey: creds.apiKey,
apiPath: resource.apiPath,
operation: "count" as ApiOperation,
query: options.query ? parseJsonArg(options.query) : undefined,
});
// Count response is typically { count: number }
if (
result &&
typeof result === "object" &&
!Array.isArray(result) &&
"count" in (result as JSONObject)
) {
// eslint-disable-next-line no-console
console.log((result as JSONObject)["count"]);
} else {
// eslint-disable-next-line no-console
console.log(result);
}
} catch (error) {
handleError(error);
}
});
}
export function registerResourceCommands(program: Command): void {
const resources: ResourceInfo[] = discoverResources();
for (const resource of resources) {
const resourceCmd: Command = program
.command(resource.name)
.description(`Manage ${resource.pluralName} (${resource.modelType})`);
// Database models get full CRUD
if (resource.modelType === "database") {
registerListCommand(resourceCmd, resource);
registerGetCommand(resourceCmd, resource);
registerCreateCommand(resourceCmd, resource);
registerUpdateCommand(resourceCmd, resource);
registerDeleteCommand(resourceCmd, resource);
registerCountCommand(resourceCmd, resource);
}
// Analytics models get create, list, count
if (resource.modelType === "analytics") {
registerListCommand(resourceCmd, resource);
registerCreateCommand(resourceCmd, resource);
registerCountCommand(resourceCmd, resource);
}
}
}