Skip to content
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 75 additions & 10 deletions packages/plugins/apps/src/vite/dev-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

/* eslint-disable no-await-in-loop */

import { setExecuteActionImplementation } from '@datadog/action-catalog';
import type { AuthOptionsWithDefaults, Logger } from '@dd/core/types';
import { randomUUID } from 'crypto';
import type { IncomingMessage, ServerResponse } from 'http';
Expand Down Expand Up @@ -124,6 +125,65 @@ async function bundleBackendFunction(
return { func: enrichedFunc, code };
}

function executeAction({
func,
auth,
fqn,
input,
doAuthenticatedRequest,
}: {
func: BackendFunction;
auth: AuthConfig;
fqn: string;
inputs: unknown;
doAuthenticatedRequest: DoAuthenticatedRequest;
}) {
const endpoint = `https://api.${auth.site}/api/v2/app-builder/queries/preview-async`;
const displayName = formatRef(func);

log.debug(`Calling Datadog API: ${endpoint}`);

const body = JSON.stringify({
data: {
type: 'queries',
attributes: {
query: {
id: randomUUID(),
name: displayName,
type: 'action',
properties: {
spec: {
fqn,
inputs,
},
onlyTriggerManually: true,
},
},
template_params: {},
},
},
});

const initialResult = await doAuthenticatedRequest<{ data?: { id?: string } }>({
url: endpoint,
method: 'POST',
type: 'json',
getData: () => ({
data: body,
headers: { 'Content-Type': 'application/json' },
}),
});

const receiptId = initialResult.data?.id;

if (!receiptId) {
throw new Error('No receipt ID returned from Datadog API');
}

log.debug(`Query execution started with receipt: ${receiptId}`);

return pollQueryExecution(receiptId, auth, doAuthenticatedRequest, log);
}
/**
* Execute a script via Datadog's app-builder queries API.
*/
Expand Down Expand Up @@ -320,20 +380,25 @@ async function handleExecuteAction(
log: Logger,
): Promise<void> {
try {
const { func, code, args } = await validateAndBundle(req, functionsByName, bundle);
const { functionName, args = [] } = await parseRequestBody(req);
const func = functionsByName.get(functionName);
Comment on lines +383 to +384

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Restore request validation before importing the function

For a request with a missing or unknown functionName, this lookup yields undefined and the subsequent import/invocation is caught as a generic 500. Previously validateAndBundle explicitly returned 400 for an invalid name and 404 for an unknown function, and the existing execute-action tests rely on those responses. Validate the parsed name and lookup result before attempting the direct import.

Useful? React with 👍 / 👎.


setExecuteActionImplementation(async (actionId, { inputs, connectionId }) => {
return executeAction({
fqn: actionId,
inputs,
connectionId,
});
});
// Loading the module todos.backend.ts
const functions = await import(func?.absolutePath);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Load backend source through Vite instead of native import

For the normal .backend.ts/.backend.tsx inputs accepted by BACKEND_FILE_RE, absolutePath points to the uncompiled source file, but this native dynamic import bypasses Vite's TypeScript transpilation and module resolution. Under the repository's supported Node runtime, invoking any such backend function therefore fails with an unknown-file-extension or unresolved-import error; it also uses Node's module cache rather than Vite's HMR graph. Load the module through Vite's server-side loader or execute a generated bundle instead.

Useful? React with 👍 / 👎.

// Execute the addTodo()
const result = await functions[func?.name](...args);

const displayName = formatRef(func);

log.debug(`Executing action: ${displayName} with args`);

const result = await executeScriptViaDatadog(
code,
func,
args,
auth,
doAuthenticatedRequest,
log,
);

res.statusCode = 200;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ success: true, result } satisfies ExecuteActionResponse));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve the response data wrapper

When a directly invoked backend function returns a normal value such as { value: 42 }, this serializes that raw value as result, while dev-server-transport.ts:72 unconditionally returns executeActionResponse.result.data. Successful local calls will consequently resolve to undefined (or throw when the function returns undefined) instead of their actual value. Wrap the direct result as { data: result } to retain the established ExecuteActionResponse contract.

Useful? React with 👍 / 👎.

Expand Down
Loading