Skip to content
Closed
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
3 changes: 3 additions & 0 deletions packages/plugins/apps/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ 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 own `--mode` value for `npm run dev:verify`, read server-side from `server.config.mode` rather than `import.meta.env.MODE`, which has no CommonJS equivalent and breaks Jest's ts-jest transform. */
export const DEV_VERIFY_MODE = 'dev-verify';
export const BACKEND_CODE_EXTENSIONS = [
'.ts',
'.tsx',
Expand Down
6 changes: 6 additions & 0 deletions packages/plugins/apps/src/vite/dev-server.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,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 @@ -232,6 +234,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 @@ -308,6 +311,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 @@ -357,6 +361,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 @@ -413,6 +418,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
68 changes: 67 additions & 1 deletion packages/plugins/apps/src/vite/dev-server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,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';

jest.mock('@dd/core/helpers/oauth-request', () => ({
Expand Down Expand Up @@ -241,6 +241,7 @@ describe('Dev Server Middleware', () => {
mockLongPolling,
'/project',
mockLog,
'development',
);

expect(mockLogFn).toHaveBeenCalledWith(
Expand All @@ -262,6 +263,7 @@ describe('Dev Server Middleware', () => {
mockLongPolling,
'/project',
mockLog,
'development',
);

expect(mockLogFn).not.toHaveBeenCalledWith(expect.anything(), 'warn');
Expand All @@ -279,6 +281,7 @@ describe('Dev Server Middleware', () => {
mockLongPolling,
'/project',
mockLog,
'development',
);

test('Should call next() for non-POST requests', () => {
Expand Down Expand Up @@ -378,6 +381,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,
getApiKeyRequest(),
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 @@ -391,6 +443,7 @@ describe('Dev Server Middleware', () => {
mockLongPolling,
'/project',
mockLog,
'development',
);

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

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

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

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

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

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

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

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

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

Expand Down Expand Up @@ -1150,6 +1212,7 @@ describe('Dev Server Middleware', () => {
mockLongPolling,
'/project',
mockLog,
'development',
);
mockLoadModuleReturning(funcWithConnection, () =>
(
Expand Down Expand Up @@ -1231,6 +1294,7 @@ describe('Dev Server Middleware', () => {
mockLongPolling,
'/project',
mockLog,
'development',
);
mockLoadModuleReturning(funcWithEmptyConnection, () =>
(
Expand Down Expand Up @@ -1485,6 +1549,7 @@ describe('Dev Server Middleware', () => {
mockLongPolling,
'/project',
mockLog,
'development',
);

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

// Simulate HMR: greet is renamed to greetV2 in the same file.
Expand Down
67 changes: 51 additions & 16 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 @@ -522,11 +523,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 — bundles and executes via the existing production round trip (queue + Deno subprocess); also reached from `/__dd/executeAction` when the dev server itself is running in `dev-verify` mode, via `routeToCloudHandler`. */
async function handleExecuteActionViaCloud(
req: IncomingMessage,
res: ServerResponse,
Expand Down Expand Up @@ -559,6 +556,31 @@ async function handleExecuteActionViaCloud(
}
}

/** 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 @@ -584,6 +606,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 Down Expand Up @@ -614,33 +637,45 @@ export function createDevServerMiddleware(
sendError(res, 500, 'Unexpected error');
});
} else if (req.url === '/__dd/executeAction') {
guardAuthenticated(res, doAuthenticatedRequest, (authedRequest) =>
handleExecuteAction(
// Routes server-side on the resolved mode, since the client always calls this one URL regardless of dev/dev-verify mode.
if (mode === DEV_VERIFY_MODE) {
routeToCloudHandler(
req,
res,
functionsByName,
bundle,
auth,
authedRequest,
doAuthenticatedRequest,
longPolling,
loadModule,
getAllowedConnectionIds,
projectRoot,
log,
),
);
} else if (req.url === '/__dd/executeActionViaCloud') {
);
return;
}
guardAuthenticated(res, doAuthenticatedRequest, (authedRequest) =>
handleExecuteActionViaCloud(
handleExecuteAction(
req,
res,
functionsByName,
bundle,
auth,
authedRequest,
longPolling,
loadModule,
getAllowedConnectionIds,
projectRoot,
log,
),
);
} else if (req.url === '/__dd/executeActionViaCloud') {
routeToCloudHandler(
req,
res,
functionsByName,
bundle,
auth,
doAuthenticatedRequest,
longPolling,
log,
);
} else {
next();
}
Expand Down
Loading
Loading