Skip to content
Draft
Show file tree
Hide file tree
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
4 changes: 4 additions & 0 deletions packages/plugins/apps/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ export const LOCAL_EXECUTION_LOAD_SUFFIX = '?dd-local-exec';
export const BACKEND_FILE_WITH_QUERY_RE = new RegExp(
`${BACKEND_FILE_RE.source.slice(0, -1)}(\\?.*)?$`,
);

/** Vite's `--mode` for `npm run dev:verify`; read via `server.config.mode` since `import.meta.env.MODE` breaks Jest's CommonJS transform. */
export const DEV_VERIFY_MODE = 'dev-verify';

export const BACKEND_CODE_EXTENSIONS = [
'.ts',
'.tsx',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ describe('Dev Server Middleware — real end-to-end local execution', () => {
mockLongPolling,
FIXTURE_ROOT,
getMockLogger(),
'development',
);

const req = createMockRequest('/__dd/executeAction', {
Expand Down Expand Up @@ -193,6 +194,7 @@ describe('Dev Server Middleware — real end-to-end local execution', () => {
mockLongPolling,
FIXTURE_ROOT,
getMockLogger(),
'development',
);

const req = createMockRequest('/__dd/executeAction', {
Expand Down Expand Up @@ -229,6 +231,7 @@ describe('Dev Server Middleware — real end-to-end local execution', () => {
mockLongPolling,
FIXTURE_ROOT,
getMockLogger(),
'development',
);

const req = createMockRequest('/__dd/executeAction', {
Expand Down Expand Up @@ -299,6 +302,7 @@ describe('Dev Server Middleware — real end-to-end local execution', () => {
mockLongPolling,
FIXTURE_ROOT,
getMockLogger(),
'development',
);

const req = createMockRequest('/__dd/executeAction', {
Expand Down Expand Up @@ -343,6 +347,7 @@ describe('Dev Server Middleware — real end-to-end local execution', () => {
mockLongPolling,
FIXTURE_ROOT,
getMockLogger(),
'development',
);

// The connection-ID collector is under test here, not the preview-async round trip
Expand Down Expand Up @@ -396,6 +401,7 @@ describe('Dev Server Middleware — real end-to-end local execution', () => {
mockLongPolling,
FIXTURE_ROOT,
getMockLogger(),
'development',
);

const apiScope = nock('https://api.datadoghq.com')
Expand Down
67 changes: 66 additions & 1 deletion packages/plugins/apps/src/vite/dev-server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { parseAst } from 'rollup/parseAst';

import { encodeQueryName } from '../backend/encodeQueryName';
import type { BackendFunction } from '../backend/types';
import { LOCAL_EXECUTION_LOAD_SUFFIX } from '../constants';
import { DEV_VERIFY_MODE, LOCAL_EXECUTION_LOAD_SUFFIX } from '../constants';
import type { AppsOptionsWithDefaults } from '../types';

/** Shape of the `$.Actions` dynamic proxy — a nested property path (e.g. `$.Actions.slack.chat.postMessage`) callable at any depth; types `globalThis.$` in tests without an `any` cast. */
Expand Down Expand Up @@ -239,6 +239,7 @@ describe('Dev Server Middleware', () => {
mockLongPolling,
'/project',
mockLog,
'development',
);

test('Should call next() for non-POST requests', () => {
Expand Down Expand Up @@ -338,6 +339,55 @@ describe('Dev Server Middleware', () => {
expect(body.result).toEqual({ data: { result: 'hello' } });
expect(apiScope.isDone()).toBe(true);
});

test('Should route /__dd/executeAction to the cloud path when the dev server was started in dev-verify mode', async () => {
const verifyModeMiddleware = createDevServerMiddleware(
mockViteBuild,
mockLoadModule,
() => mockFunctions,
async () => [],
mockAuth,
testAuthenticatedRequest,
mockLongPolling,
'/project',
mockLog,
DEV_VERIFY_MODE,
);

mockBuildWithParsedBackend();

const apiScope = nock(DD_API_ORIGIN)
.post('/api/v2/app-builder/queries/preview-async')
.reply(200, { data: { id: 'receipt-456' } })
.get('/api/v2/app-builder/queries/execution-long-polling/receipt-456')
.reply(200, {
data: {
attributes: {
done: true,
outputs: { data: { result: 'via cloud' } },
},
},
});

const req = createMockRequest('/__dd/executeAction', {
functionName: encodeQueryName(mockFunctions[0]),
args: ['world'],
});
const res = createMockResponse();
const next = jest.fn();

verifyModeMiddleware(req, res, next);
expect(next).not.toHaveBeenCalled();

await res.done;

expect(res.statusCode).toBe(200);
const body = JSON.parse(res.getBody());
expect(body.success).toBe(true);
expect(body.result).toEqual({ data: { result: 'via cloud' } });
expect(apiScope.isDone()).toBe(true);
expect(mockLoadModule).not.toHaveBeenCalled();
});
});

describe('debugBundle handler', () => {
Expand All @@ -351,6 +401,7 @@ describe('Dev Server Middleware', () => {
mockLongPolling,
'/project',
mockLog,
'development',
);

test('Should return 400 for missing functionRef', async () => {
Expand Down Expand Up @@ -464,6 +515,7 @@ describe('Dev Server Middleware', () => {
mockLongPolling,
'/project',
mockLog,
'development',
);

test('Should return 400 for missing functionRef', async () => {
Expand Down Expand Up @@ -499,6 +551,7 @@ describe('Dev Server Middleware', () => {
mockLongPolling,
'/project',
mockLog,
'development',
);

const req = createMockRequest('/__dd/executeActionViaCloud', {
Expand Down Expand Up @@ -622,6 +675,7 @@ describe('Dev Server Middleware', () => {
mockLongPolling,
'/project',
mockLog,
'development',
);

const apiScope = nock(DD_API_ORIGIN, {
Expand Down Expand Up @@ -664,6 +718,7 @@ describe('Dev Server Middleware', () => {
mockLongPolling,
'/project',
mockLog,
'development',
);

const req = createMockRequest('/__dd/executeActionViaCloud', {
Expand Down Expand Up @@ -753,6 +808,7 @@ describe('Dev Server Middleware', () => {
mockLongPolling,
'/project',
mockLog,
'development',
);

type PreviewAsyncBody = {
Expand Down Expand Up @@ -917,6 +973,7 @@ describe('Dev Server Middleware', () => {
{ ...mockLongPolling, maxRetries: 1 },
'/project',
mockLog,
'development',
);

const apiScope = nock(DD_API_ORIGIN)
Expand Down Expand Up @@ -956,6 +1013,7 @@ describe('Dev Server Middleware', () => {
{ ...mockLongPolling, timeoutMs: 100 },
'/project',
mockLog,
'development',
);

const apiScope = nock(DD_API_ORIGIN)
Expand Down Expand Up @@ -1023,6 +1081,7 @@ describe('Dev Server Middleware', () => {
mockLongPolling,
'/project',
mockLog,
'development',
);

test('Should return 400 for missing functionRef', async () => {
Expand Down Expand Up @@ -1077,6 +1136,7 @@ describe('Dev Server Middleware', () => {
mockLongPolling,
'/project',
mockLog,
'development',
);
mockLoadModuleReturning(mockFunctions[0], () => 'pure result, no $.Actions call');

Expand Down Expand Up @@ -1111,6 +1171,7 @@ describe('Dev Server Middleware', () => {
mockLongPolling,
'/project',
mockLog,
'development',
);
mockLoadModuleReturning(funcWithConnection, () =>
testDollarActions().slack.chat.postMessage({
Expand Down Expand Up @@ -1189,6 +1250,7 @@ describe('Dev Server Middleware', () => {
mockLongPolling,
'/project',
mockLog,
'development',
);
mockLoadModuleReturning(funcWithEmptyConnection, () =>
testDollarActions().slack.chat.postMessage({
Expand Down Expand Up @@ -1438,6 +1500,7 @@ describe('Dev Server Middleware', () => {
mockLongPolling,
'/project',
mockLog,
'development',
);

const req = createMockRequest('/__dd/executeAction', {
Expand Down Expand Up @@ -1471,6 +1534,7 @@ describe('Dev Server Middleware', () => {
mockLongPolling,
'/project',
mockLog,
'development',
);

const req = createMockRequest('/__dd/executeAction', {
Expand Down Expand Up @@ -1508,6 +1572,7 @@ describe('Dev Server Middleware', () => {
mockLongPolling,
'/project',
mockLog,
'development',
);

// Simulate HMR: greet is renamed to greetV2 in the same file.
Expand Down
64 changes: 45 additions & 19 deletions packages/plugins/apps/src/vite/dev-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { encodeQueryName } from '../backend/encodeQueryName';
import type { ExecuteActionRequest, ExecuteActionResponse } from '../backend/protocol';
import type { BackendFunction, BackendOutputs } from '../backend/types';
import { generateDevVirtualEntryContent } from '../backend/virtual-entry';
import { DEV_VERIFY_MODE } from '../constants';
import type { LongPollingOptions } from '../types';

import { createBackendConnectionIdCollector } from './backend-connection-id-collector';
Expand Down Expand Up @@ -504,11 +505,7 @@ async function handleExecuteAction(
}
}

/**
* Handles POST /__dd/executeActionViaCloud — bundles a backend function and executes it via
* the production round trip (queue + Deno subprocess), kept as its own endpoint
* (`npm run dev:verify`) for pre-publish parity checks rather than a mode flag.
*/
/** Handle POST /__dd/executeActionViaCloud: bundle and execute via the production round trip (queue + Deno subprocess). */
async function handleExecuteActionViaCloud(
req: IncomingMessage,
res: ServerResponse,
Expand Down Expand Up @@ -537,10 +534,36 @@ async function handleExecuteActionViaCloud(

sendSuccess(res, result);
} catch (error: unknown) {
handleHttpError(res, error, log, 'executeActionViaCloud');
// Labeled by the actual URL hit, not a fixed name, since dev-verify mode also reaches this via /__dd/executeAction.
handleHttpError(res, error, log, req.url ?? 'executeActionViaCloud');
}
}

/** Shared by both routes that reach the cloud round trip, so a fix to auth-checking or error handling can't drift between them. */
function routeToCloudHandler(
req: IncomingMessage,
res: ServerResponse,
functionsByName: Map<string, BackendFunction>,
bundle: BundleFn,
auth: AuthConfig,
doAuthenticatedRequest: DoAuthenticatedRequest | undefined,
longPolling: LongPollingConfig,
log: Logger,
): void {
guardAuthenticated(res, doAuthenticatedRequest, (authedRequest) =>
handleExecuteActionViaCloud(
req,
res,
functionsByName,
bundle,
auth,
authedRequest,
longPolling,
log,
),
);
}

/**
* Build a lookup map from encoded query names to BackendFunction objects.
*/
Expand All @@ -563,6 +586,7 @@ export function createDevServerMiddleware(
longPolling: LongPollingConfig,
projectRoot: string,
log: Logger,
mode: string,
): (req: IncomingMessage, res: ServerResponse, next: () => void) => void {
const bundle = (func: BackendFunction) =>
bundleBackendFunction(viteBuild, func, projectRoot, log);
Expand All @@ -586,6 +610,21 @@ export function createDevServerMiddleware(
handleDebugBundle(req, res, functionsByName, bundle).catch(() => {
sendError(res, 500, 'Unexpected error');
});
} else if (
req.url === '/__dd/executeActionViaCloud' ||
(req.url === '/__dd/executeAction' && mode === DEV_VERIFY_MODE)
) {
// The client always POSTs to /__dd/executeAction regardless of mode, so dev-verify is routed here server-side instead.
routeToCloudHandler(
req,
res,
functionsByName,
bundle,
auth,
doAuthenticatedRequest,
longPolling,
log,
);
} else if (req.url === '/__dd/executeAction') {
guardAuthenticated(res, doAuthenticatedRequest, (authedRequest) =>
handleExecuteAction(
Expand All @@ -601,19 +640,6 @@ export function createDevServerMiddleware(
log,
),
);
} else if (req.url === '/__dd/executeActionViaCloud') {
guardAuthenticated(res, doAuthenticatedRequest, (authedRequest) =>
handleExecuteActionViaCloud(
req,
res,
functionsByName,
bundle,
auth,
authedRequest,
longPolling,
log,
),
);
} else {
next();
}
Expand Down
Loading
Loading