Add function exec-url command

This commit is contained in:
Space-Banane
2026-05-05 12:46:24 +02:00
parent 544f6bd714
commit 681a3cd6df
7 changed files with 191 additions and 18 deletions
+20 -1
View File
@@ -30,6 +30,7 @@ this will check the health, and if not setup, it will prompt you to set up the C
- `shsf delete trigger <functionId> <triggerId>`: Deletes a specific trigger from a function. - `shsf delete trigger <functionId> <triggerId>`: Deletes a specific trigger from a function.
- `shsf get function <id>`: Get details of a specific function by its ID. - `shsf get function <id>`: Get details of a specific function by its ID.
- `shsf get exec-url [--id <id>]`: Get the execution URL for a function. Falls back to `.shsf.json` for the function ID and also prints the alias URL when the function has an `executionAlias`.
- `shsf get namespace <id>`: Get details of a specific namespace by its ID - `shsf get namespace <id>`: Get details of a specific namespace by its ID
- `shsf get trigger <functionId> <triggerId>`: Get details of a specific trigger from a function. - `shsf get trigger <functionId> <triggerId>`: Get details of a specific trigger from a function.
@@ -81,6 +82,12 @@ You can also create a `.shsf.json` mapping file in the repository root so you do
Command-line flags take precedence over values in `.shsf.json`. Command-line flags take precedence over values in `.shsf.json`.
This mapping can also be used with `shsf get exec-url`, so from a mapped function directory you can run:
```bash
shsf get exec-url
```
## Instructions ## Instructions
Use these commands for when you need to interact with shsf from the command line. Its faster than using the ui for almost all ops. Use these commands for when you need to interact with shsf from the command line. Its faster than using the ui for almost all ops.
@@ -96,6 +103,18 @@ After creating a function and receiving an ID (for example, 81), you can share t
[UI_URL]/functions/[ID] [UI_URL]/functions/[ID]
``` ```
To get the function execution URL from the CLI:
```bash
shsf get exec-url --id 81
```
If the function has an execution alias configured, the CLI also prints the alias form:
```text
[API_URL]/exec/[executionAlias]
```
## Update ## Update
Update with your package manager of choice. ALWAYS pnpm: Update with your package manager of choice. ALWAYS pnpm:
```bash ```bash
@@ -386,4 +405,4 @@ require (
) )
``` ```
Supported Go versions: `1.20`, `1.21`, `1.22`, `1.23` Supported Go versions: `1.20`, `1.21`, `1.22`, `1.23`
+16
View File
@@ -46,6 +46,22 @@ Quickly see if the system is up and running:
shsf health shsf health
``` ```
### 🔗 Execution URL
Get a function's execution URL by ID:
```bash
shsf get exec-url --id <id>
```
If your repo has a `.shsf.json` mapping with an `id`, you can omit `--id`:
```bash
shsf get exec-url
```
The command prints the standard execution URL and, when available, the alias URL based on the function's `executionAlias`.
### 🏗️ Local Development ### 🏗️ Local Development
If you're contributing or running from source: If you're contributing or running from source:
+36
View File
@@ -0,0 +1,36 @@
import { describe, expect, it } from "vitest";
import { buildFunctionExecUrls } from "../utils/function_exec_url.js";
describe("buildFunctionExecUrls", () => {
it("builds the ID-based execution URL and trims trailing slashes", () => {
expect(
buildFunctionExecUrls("https://example.com/", {
namespaceId: 4,
executionId: "abc-123",
}),
).toEqual({
executionUrl: "https://example.com/api/exec/4/abc-123",
});
});
it("includes the alias URL when the function has an execution alias", () => {
expect(
buildFunctionExecUrls("https://example.com", {
namespaceId: 7,
executionId: "uuid-456",
executionAlias: "friendly-name",
}),
).toEqual({
executionUrl: "https://example.com/api/exec/7/uuid-456",
aliasUrl: "https://example.com/exec/friendly-name",
});
});
it("throws when required execution URL fields are missing", () => {
expect(() =>
buildFunctionExecUrls("https://example.com", {
executionAlias: "friendly-name",
}),
).toThrow("Function response is missing execution URL fields.");
});
});
+63
View File
@@ -0,0 +1,63 @@
import chalk from "chalk";
import { getApiClient } from "../../api.js";
import { loadConfig } from "../../config.js";
import { resolveFunctionId } from "../../utils/function_commands.js";
import { buildFunctionExecUrls } from "../../utils/function_exec_url.js";
export const getExecUrlDefinition = {
name: "exec-url",
description: "Get a function execution URL by its ID.",
options: [{ name: "--id <id>", description: "Function ID (falls back to .shsf.json default id)" }],
action: async (options: { id?: string }) => {
const functionId = resolveFunctionId(options);
if (!functionId) return;
await getExecUrl(functionId);
},
};
async function getExecUrl(functionId: string): Promise<void> {
const client = await getApiClient();
const config = await loadConfig();
try {
const response = await client.get(`/api/function/${functionId}`);
const functionData = response.data?.data;
if (!functionData) {
console.error(`${chalk.red("✗")} Unexpected response from server.`);
return;
}
if (!functionData.allow_http) {
console.error(`${chalk.red("✗")} HTTP execution is not allowed for function ${chalk.yellow(functionId)}.`);
return;
}
const urls = buildFunctionExecUrls(config.SHSF_INSTANCE, functionData);
console.log(`${chalk.blue("Execution URL")} for function ${chalk.cyan(functionId)}:`);
console.log(chalk.bgGreen.black(` ${urls.executionUrl} `));
if (urls.aliasUrl) {
console.log(`${chalk.blue("Alias URL")} for function ${chalk.cyan(functionId)}:`);
console.log(chalk.bgGreen.black(` ${urls.aliasUrl} `));
}
} catch (error: any) {
if (error.response) {
if (error.response.status === 404) {
console.error(`${chalk.red("✗")} Function with ID ${chalk.yellow(functionId)} not found.`);
} else {
console.error(`${chalk.red("✗")} Failed to fetch function details.`);
console.error(`Status Code: ${chalk.red(error.response.status)}`);
console.error(
`Message: ${chalk.yellow(error.response.data?.message || "Unknown error from server")}`,
);
}
} else if (error.request) {
console.error(`${chalk.red("✗")} No response received from server.`);
} else {
console.error(`${chalk.red("✗")} Error: ${chalk.yellow(error.message)}`);
}
}
}
+2 -17
View File
@@ -1,24 +1,9 @@
import chalk from "chalk"; import chalk from "chalk";
import { getApiClient } from "../api.js"; import { getApiClient } from "../api.js";
import { readMappingFile } from "./push_helpers.js";
import { parseCorsOriginsOption } from "./cors.js"; import { parseCorsOriginsOption } from "./cors.js";
import { resolveFunctionId } from "./function_commands.js";
export function resolveFunctionId(options: { id?: string }): string | null { export { resolveFunctionId } from "./function_commands.js";
if (options.id) {
return options.id;
}
const mapping = readMappingFile();
if (mapping?.id) {
console.log(chalk.blue(`Using mapped id ${mapping.id} from .shsf.json`));
return mapping.id;
}
console.error(
`${chalk.red("✗")} Function ID is required. Use ${chalk.cyan("--id <id>")} or provide an ${chalk.cyan(".shsf.json")} mapping with an ${chalk.cyan("id")}.`,
);
return null;
}
export function validateSingleOrigin(origin: string): { origin?: string; error?: string } { export function validateSingleOrigin(origin: string): { origin?: string; error?: string } {
const parsed = parseCorsOriginsOption(origin); const parsed = parseCorsOriginsOption(origin);
+19
View File
@@ -0,0 +1,19 @@
import chalk from "chalk";
import { readMappingFile } from "./push_helpers.js";
export function resolveFunctionId(options: { id?: string }): string | null {
if (options.id) {
return options.id;
}
const mapping = readMappingFile();
if (mapping?.id) {
console.log(chalk.blue(`Using mapped id ${mapping.id} from .shsf.json`));
return mapping.id;
}
console.error(
`${chalk.red("✗")} Function ID is required. Use ${chalk.cyan("--id <id>")} or provide an ${chalk.cyan(".shsf.json")} mapping with an ${chalk.cyan("id")}.`,
);
return null;
}
+35
View File
@@ -0,0 +1,35 @@
export interface FunctionExecUrlInput {
namespaceId?: number | string;
executionId?: string;
executionAlias?: string | null;
}
export interface FunctionExecUrls {
executionUrl: string;
aliasUrl?: string;
}
function normalizeBaseUrl(instanceUrl: string): string {
return instanceUrl.replace(/\/+$/, "");
}
export function buildFunctionExecUrls(
instanceUrl: string,
functionData: FunctionExecUrlInput,
): FunctionExecUrls {
const baseUrl = normalizeBaseUrl(instanceUrl);
if (functionData.namespaceId === undefined || !functionData.executionId) {
throw new Error("Function response is missing execution URL fields.");
}
const urls: FunctionExecUrls = {
executionUrl: `${baseUrl}/api/exec/${functionData.namespaceId}/${functionData.executionId}`,
};
if (functionData.executionAlias) {
urls.aliasUrl = `${baseUrl}/exec/${functionData.executionAlias}`;
}
return urls;
}