diff --git a/UI_ENHANCEMENT_SUMMARY.md b/UI_ENHANCEMENT_SUMMARY.md new file mode 100644 index 000000000..58e0044c7 --- /dev/null +++ b/UI_ENHANCEMENT_SUMMARY.md @@ -0,0 +1,167 @@ +# Server Logic Debugging - UI Enhancement Summary + +## Overview +Enhanced the Power Pages Server Logic debugging feature with multiple UI entry points to improve discoverability and user experience. + +## Changes Made + +### 1. CodeLens Provider (`ServerLogicCodeLensProvider.ts`) +**Purpose**: Show inline debug/run actions above functions in server logic files + +**Features**: +- Displays "▶ Debug" and "▶ Run" above the first function in each file +- Falls back to showing at top of file if no functions found +- Only appears for `.js` files in `server-logics/` folder +- Uses VS Code icons: `$(debug-alt)` and `$(run)` + +**Implementation**: +```typescript +export class ServerLogicCodeLensProvider implements vscode.CodeLensProvider { + provideCodeLenses(document: vscode.TextDocument): vscode.CodeLens[] { + // Finds function declarations + // Creates debug and run CodeLens items + // Returns array of CodeLens for rendering + } +} +``` + +### 2. Run Command (`powerpages.runServerLogic`) +**Purpose**: Execute server logic without debugging (no breakpoints) + +**Implementation**: +- Added to `ServerLogicDebugger.ts` +- Similar to debug command but sets `noDebug: true` +- Validates file is in `server-logics/` folder +- Logs telemetry event: `ServerLogicRunCommandExecuted` + +### 3. Package.json Updates + +#### Commands Section +Added new command: +```json +{ + "command": "powerpages.runServerLogic", + "category": "Power Pages", + "title": "Run Server Logic File", + "icon": "$(run)", + "enablement": "resourcePath =~ /server-logics/ && resourceExtname == .js" +} +``` + +#### Editor Toolbar (Already Implemented) +```json +"editor/title": [ + { + "command": "powerpages.debugServerLogic", + "group": "navigation", + "when": "resourcePath =~ /server-logics/ && resourceExtname == .js && !virtualWorkspace" + } +] +``` + +#### Context Menu (Already Implemented) +```json +"editor/context": [ + { + "command": "powerpages.debugServerLogic", + "group": "z_commands", + "when": "resourcePath =~ /server-logics/ && resourceExtname == .js && !virtualWorkspace" + } +] +``` + +### 4. Registration in Extension +Updated `ServerLogicDebugger.ts` to register CodeLens provider: +```typescript +const codeLensProvider = new ServerLogicCodeLensProvider(); +context.subscriptions.push( + vscode.languages.registerCodeLensProvider( + { pattern: '**/server-logics/**/*.js' }, + codeLensProvider + ) +); +``` + +### 5. Telemetry +Added new event to `desktopExtensionTelemetryEventNames.ts`: +- `SERVER_LOGIC_RUN_COMMAND_EXECUTED` + +### 6. Documentation Updates +Updated `README.md` with: +- UI Features section describing toolbar, CodeLens, context menu +- Updated Quick Start with all available methods +- Added "Running Without Debugging" section +- Listed new files created + +## User Experience Flow + +### Discovery +1. **First Time**: Welcome notification appears with "Debug Current File" button +2. **Editor UI**: Debug icon (🐛) visible in toolbar when server logic file is open +3. **CodeLens**: Inline "▶ Debug | Run" appears above functions +4. **Context Menu**: Right-click shows debug option +5. **Command Palette**: Search for "debug" or "run" shows commands +6. **Keyboard**: F5 starts debugging + +### Debugging +1. User clicks any entry point (toolbar/CodeLens/menu/F5) +2. Extension generates runtime loader with mock SDK +3. Node.js debugger launches with `--require` flag +4. Breakpoints hit, variables inspectable +5. Full debugging capabilities available + +### Running Without Debugging +1. Click "▶ Run" CodeLens or use command +2. Code executes quickly without stopping at breakpoints +3. Useful for testing output without debugging overhead + +## Files Modified/Created + +### New Files (3) +1. `src/debugger/server-logic/ServerLogicCodeLensProvider.ts` - CodeLens implementation +2. Documentation updates in README.md +3. This summary document + +### Modified Files (4) +1. `src/debugger/server-logic/ServerLogicDebugger.ts` - Added run command, registered CodeLens +2. `src/debugger/server-logic/index.ts` - Exported CodeLens provider +3. `package.json` - Added run command definition +4. `src/common/OneDSLoggerTelemetry/client/desktopExtensionTelemetryEventNames.ts` - Added telemetry event + +## Testing Checklist + +- [ ] CodeLens appears above functions in `.js` files in `server-logics/` +- [ ] CodeLens does not appear in non-server-logic files +- [ ] Click "Debug" CodeLens starts debugging +- [ ] Click "Run" CodeLens executes without debugging +- [ ] Editor toolbar shows debug icon for server logic files +- [ ] Context menu shows debug option +- [ ] F5 starts debugging +- [ ] Runtime loader generated correctly +- [ ] Mock SDK available in debug session +- [ ] Breakpoints work +- [ ] Run command executes without stopping at breakpoints +- [ ] Telemetry events logged correctly +- [ ] Welcome notification appears first time only + +## Conditional Rendering + +All UI elements conditionally display based on: +- File path contains `server-logics/` +- File extension is `.js` +- Not in web/virtual workspace (`!virtualWorkspace`) + +## Next Steps + +1. **Compile**: Run `npm run compile` to build TypeScript +2. **Test**: Press F5 in extension development host +3. **Validate**: + - Open workspace with `server-logics/` folder + - Open a `.js` file from that folder + - Verify all UI elements appear + - Test debugging and running +4. **User Documentation**: Create user-facing docs with screenshots +5. **Consider Enhancements**: + - Add "Debug All Tests" if multiple server logic files + - Add configuration options for mock data location + - Add output channel for Server.Logger messages diff --git a/package.json b/package.json index 2b5874d87..5cb58844d 100644 --- a/package.json +++ b/package.json @@ -404,6 +404,20 @@ "category": "Copilot In Power Pages", "title": "Explain" }, + { + "command": "powerpages.debugServerLogic", + "category": "Power Pages", + "title": "Debug Current Server Logic File", + "icon": "$(debug-alt)", + "enablement": "resourcePath =~ /server-logics/ && resourceExtname == .js" + }, + { + "command": "powerpages.runServerLogic", + "category": "Power Pages", + "title": "Run Server Logic File", + "icon": "$(run)", + "enablement": "resourcePath =~ /server-logics/ && resourceExtname == .js" + }, { "command": "powerPlatform.previewCurrentActiveUsers", "title": "Current Active Users", @@ -726,6 +740,45 @@ } } } + }, + { + "type": "node", + "label": "Power Pages Server Logic Debugger", + "configurationSnippets": [ + { + "label": "Power Pages: Debug Server Logic", + "description": "Debug a Power Pages server logic file with mock SDK support", + "body": { + "type": "node", + "request": "launch", + "name": "Debug Power Pages Server Logic", + "program": "^\"\\${workspaceFolder}/server-logics/\\${1:MyServerLogic.js}\"", + "skipFiles": [ + "/**" + ], + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen" + } + }, + { + "label": "Power Pages: Debug Server Logic with Custom Mock Data", + "description": "Debug server logic with custom mock data from .vscode/mock-data.json", + "body": { + "type": "node", + "request": "launch", + "name": "Debug Server Logic with Custom Data", + "program": "^\"\\${workspaceFolder}/server-logics/\\${1:MyServerLogic.js}\"", + "env": { + "MOCK_DATA_PATH": "^\"\\${workspaceFolder}/.vscode/mock-data.json\"" + }, + "skipFiles": [ + "/**" + ], + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen" + } + } + ] } ], "snippets": [ diff --git a/src/client/extension.ts b/src/client/extension.ts index 5a1c6fe39..7e28f385c 100644 --- a/src/client/extension.ts +++ b/src/client/extension.ts @@ -54,6 +54,7 @@ import { PROVIDER_ID } from "../common/services/Constants"; import { activateServerApiAutocomplete } from "../common/intellisense"; import { EnableServerLogicChanges } from "../common/ecs-features/ecsFeatureGates"; import { setServerApiTelemetryContext } from "../common/intellisense/ServerApiTelemetryContext"; +import { activateServerLogicDebugger } from "../debugger/server-logic/ServerLogicDebugger"; let client: LanguageClient; let _context: vscode.ExtensionContext; @@ -183,6 +184,9 @@ export async function activate( const basicPanels = RegisterBasicPanels(pacWrapper); _context.subscriptions.push(...basicPanels); + // Activate Server Logic debugger with PAC wrapper for automatic authentication + activateServerLogicDebugger(_context, pacWrapper); + let copilotNotificationShown = false; const workspaceFolders = getWorkspaceFolders(); @@ -246,6 +250,7 @@ export async function activate( activateServerApiAutocomplete(_context, [ { languageId: 'javascript', triggerCharacters: ['.'] } ]); + serverApiAutocompleteInitialized = true; } } diff --git a/src/common/OneDSLoggerTelemetry/client/desktopExtensionTelemetryEventNames.ts b/src/common/OneDSLoggerTelemetry/client/desktopExtensionTelemetryEventNames.ts index 91ddf8b52..c7c1630ad 100644 --- a/src/common/OneDSLoggerTelemetry/client/desktopExtensionTelemetryEventNames.ts +++ b/src/common/OneDSLoggerTelemetry/client/desktopExtensionTelemetryEventNames.ts @@ -5,4 +5,8 @@ export enum desktopTelemetryEventNames { DESKTOP_EXTENSION_INIT_CONTEXT = "DesktopExtensionInitContext", + SERVER_LOGIC_DEBUG_STARTED = "ServerLogicDebugStarted", + SERVER_LOGIC_DEBUG_COMMAND_EXECUTED = "ServerLogicDebugCommandExecuted", + SERVER_LOGIC_RUN_COMMAND_EXECUTED = "ServerLogicRunCommandExecuted", + SERVER_LOGIC_AUTH_ERROR = "ServerLogicAuthError", } diff --git a/src/debugger/server-logic/README.md b/src/debugger/server-logic/README.md new file mode 100644 index 000000000..9b32b25ae --- /dev/null +++ b/src/debugger/server-logic/README.md @@ -0,0 +1,106 @@ +# Server Logic Debugging + +This directory contains the implementation for debugging Power Pages Server Logic files locally with mock SDK support. + +## Features + +- **Mock Server SDK**: Complete mock implementation of `Server.Logger`, `Server.Connector.HttpClient`, `Server.Connector.Dataverse`, `Server.Context`, `Server.User`, and `Server.Environment` +- **Breakpoint Debugging**: Set breakpoints and step through your server logic code +- **Variable Inspection**: Inspect variables and call stack in VS Code debugger +- **Custom Mock Data**: Provide custom mock data via `.vscode/mock-data.json` +- **IntelliSense Support**: Full autocomplete for all Server APIs + +## How It Works + +1. When debugging is initiated, the extension generates a runtime loader file (`.vscode/server-logic-runtime-loader.js`) +2. This loader injects the mock `Server` object into the global scope +3. Node.js debugger attaches with the loader pre-required via `--require` flag +4. Your server logic code runs with full debugging capabilities + +## Usage + +### Quick Start + +1. Open a server logic file from `server-logics/` folder +2. Start debugging using any of these methods: + - Click the **Debug** button in the editor toolbar + - Click **▶ Debug** CodeLens above your function + - Right-click and select "Debug Current Server Logic File" + - Press F5 + - Use Command Palette: `Power Pages: Debug Current Server Logic File` +3. Set breakpoints and debug! + +### Running Without Debugging + +- Click **▶ Run** CodeLens to execute without stopping at breakpoints +- Use Command Palette: `Power Pages: Run Server Logic File` + +### Commands + +- `Power Pages: Debug Current Server Logic File` - Start debugging the active file +- `Power Pages: Run Server Logic File` - Run without debugging + +### Configuration + +Add to your `launch.json`: + +```json +{ + "type": "node", + "request": "launch", + "name": "Debug Server Logic", + "program": "${workspaceFolder}/server-logics/MyLogic.js", + "skipFiles": ["/**"] +} +``` + +### Custom Mock Data + +Create `.vscode/mock-data.json`: + +```json +{ + "User": { + "id": "custom-user-id", + "fullname": "John Doe", + "email": "john.doe@example.com" + }, + "Context": { + "Method": "POST" + }, + "QueryParameters": { + "id": "123" + } +} +``` + +## UI Features + +When viewing a server logic file, you'll see a debug icon (🐛) in the editor toolbar for quick access. + +### CodeLens + +Above functions in your server logic file, you'll see inline actions: + +- **▶ Debug** - Start debugging +- **▶ Run** - Run without debugging + +### Context Menu + +Right-click in any server logic file to access debug commands. + +## Welcome Notification + +First time you open a workspace with server logic files, you'll see a helpful notification with quick actions. + +## Files + +- `ServerLogicMockSdk.ts` - Mock SDK implementation generator +- `ServerLogicDebugger.ts` - Debug configuration provider, commands, and activation logic +- `ServerLogicCodeLensProvider.ts` - CodeLens provider for inline debug/run actions +- `index.ts` - Public exports +- `sample-server-logic.js` - Example file demonstrating all Server API patterns + +## Technical Details + +The debugger uses Node.js's `--require` flag to inject the mock SDK before the user's code runs. This ensures the `Server` global object is available throughout execution, matching the Power Pages runtime behavior. diff --git a/src/debugger/server-logic/ServerLogicCodeLensProvider.ts b/src/debugger/server-logic/ServerLogicCodeLensProvider.ts new file mode 100644 index 000000000..084af36bb --- /dev/null +++ b/src/debugger/server-logic/ServerLogicCodeLensProvider.ts @@ -0,0 +1,61 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +import * as vscode from 'vscode'; + +/** + * Provides CodeLens for server logic files showing debug/run actions + */ +export class ServerLogicCodeLensProvider implements vscode.CodeLensProvider { + + private _onDidChangeCodeLenses: vscode.EventEmitter = new vscode.EventEmitter(); + public readonly onDidChangeCodeLenses: vscode.Event = this._onDidChangeCodeLenses.event; + + /** + * Refresh CodeLens display + */ + public refresh(): void { + this._onDidChangeCodeLenses.fire(); + } + + /** + * Provide CodeLens for the document + */ + public provideCodeLenses( + document: vscode.TextDocument, + _: vscode.CancellationToken + ): vscode.CodeLens[] | Thenable { + + // Only provide CodeLens for server logic files + if (!document.fileName.includes('server-logics') || !document.fileName.endsWith('.js')) { + return []; + } + + const codeLenses: vscode.CodeLens[] = []; + + // Add single CodeLens at the top of the file (line 0) + const range = new vscode.Range(0, 0, 0, 0); + + // Add "Debug" CodeLens + const debugLens = new vscode.CodeLens(range, { + title: `$(debug-alt) ${vscode.l10n.t('Debug')}`, + tooltip: vscode.l10n.t('Debug this server logic file'), + command: 'powerpages.debugServerLogic', + arguments: [] + }); + codeLenses.push(debugLens); + + // Add "Run" CodeLens + const runLens = new vscode.CodeLens(range, { + title: `$(run) ${vscode.l10n.t('Run')}`, + tooltip: vscode.l10n.t('Run this server logic file without debugging'), + command: 'powerpages.runServerLogic', + arguments: [] + }); + codeLenses.push(runLens); + + return codeLenses; + } +} diff --git a/src/debugger/server-logic/ServerLogicDebugger.ts b/src/debugger/server-logic/ServerLogicDebugger.ts new file mode 100644 index 000000000..abeeb9172 --- /dev/null +++ b/src/debugger/server-logic/ServerLogicDebugger.ts @@ -0,0 +1,363 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +import * as vscode from 'vscode'; +import * as path from 'path'; +import * as fs from 'fs'; +import { generateServerMockSdk } from './ServerLogicMockSdk'; +import { oneDSLoggerWrapper } from '../../common/OneDSLoggerTelemetry/oneDSLoggerWrapper'; +import { ServerLogicCodeLensProvider } from './ServerLogicCodeLensProvider'; +import { desktopTelemetryEventNames } from '../../common/OneDSLoggerTelemetry/client/desktopExtensionTelemetryEventNames'; +import { dataverseAuthentication } from '../../common/services/AuthenticationProvider'; +import { PacWrapper } from '../../client/pac/PacWrapper'; + +/** + * Provided debug configuration template for Server Logic debugging + */ +export const providedServerLogicDebugConfig: vscode.DebugConfiguration = { + type: 'node', + request: 'launch', + name: vscode.l10n.t('Debug Power Pages Server Logic'), + program: '${file}', + skipFiles: ['/**'], + console: 'internalConsole' +}; + +/** + * Debug configuration provider for Power Pages Server Logic + */ +export class ServerLogicDebugProvider implements vscode.DebugConfigurationProvider { + + /** + * Provides initial debug configurations + */ + provideDebugConfigurations( + _: vscode.WorkspaceFolder | undefined + ): vscode.ProviderResult { + return [providedServerLogicDebugConfig]; + } + + /** + * Resolves the debug configuration before starting the debug session + */ + async resolveDebugConfiguration( + folder: vscode.WorkspaceFolder | undefined, + config: vscode.DebugConfiguration, + _: vscode.CancellationToken + ): Promise { + + if (!config.type && !config.request && !config.name) { + const editor = vscode.window.activeTextEditor; + if (editor && this.isServerLogicFile(editor.document.uri.fsPath)) { + config = { + ...providedServerLogicDebugConfig, + program: editor.document.uri.fsPath + }; + } else { + vscode.window.showErrorMessage( + vscode.l10n.t('Cannot debug: Please open a server logic file (.js) from the server-logics folder.') + ); + return undefined; + } + } + + if (!folder) { + vscode.window.showErrorMessage(vscode.l10n.t('Server Logic debugging requires an open workspace.')); + return undefined; + } + + try { + const loaderPath = await this.ensureRuntimeLoader(folder); + + config.runtimeArgs = config.runtimeArgs || []; + config.runtimeArgs.unshift('--require', loaderPath); + + if (config.mockDataPath) { + config.env = config.env || {}; + config.env.MOCK_DATA_PATH = config.mockDataPath; + } + + oneDSLoggerWrapper.getLogger().traceInfo( + desktopTelemetryEventNames.SERVER_LOGIC_DEBUG_STARTED, + { + hasCustomMockData: !!config.mockDataPath + } + ); + + return config; + } catch (error) { + vscode.window.showErrorMessage( + vscode.l10n.t('Failed to initialize Server Logic debugger: {0}', error instanceof Error ? error.message : String(error)) + ); + return undefined; + } + } + + /** + * Checks if a file is a server logic file + */ + private isServerLogicFile(filePath: string): boolean { + return filePath.includes('server-logics') && filePath.endsWith('.js'); + } + + /** + * Ensures the runtime loader file exists and .gitignore is configured + */ + private async ensureRuntimeLoader(folder: vscode.WorkspaceFolder): Promise { + const vscodeDir = path.join(folder.uri.fsPath, '.vscode'); + const loaderPath = path.join(vscodeDir, 'server-logic-runtime-loader.js'); + const gitignorePath = path.join(vscodeDir, '.gitignore'); + + if (!fs.existsSync(vscodeDir)) { + fs.mkdirSync(vscodeDir, { recursive: true }); + } + + if (!fs.existsSync(loaderPath)) { + const loaderContent = generateServerMockSdk(); + fs.writeFileSync(loaderPath, loaderContent, 'utf8'); + } + + this.ensureGitignore(gitignorePath); + + return loaderPath; + } + + /** + * Ensures .vscode/.gitignore includes server logic debug files + */ + private ensureGitignore(gitignorePath: string): void { + const requiredEntries = ['server-logic-runtime-loader.js']; + let gitignoreContent = ''; + + if (fs.existsSync(gitignorePath)) { + gitignoreContent = fs.readFileSync(gitignorePath, 'utf8'); + } + + let modified = false; + for (const entry of requiredEntries) { + if (!gitignoreContent.includes(entry)) { + gitignoreContent += `\n${entry}`; + modified = true; + } + } + + if (modified) { + fs.writeFileSync(gitignorePath, gitignoreContent.trim() + '\n', 'utf8'); + } + } +} + +/** + * Helper function to get Dataverse credentials from PAC auth + * Returns the org URL and access token from the currently authenticated profile + */ +async function getDataverseCredentials(pacWrapper?: PacWrapper): Promise<{ orgUrl: string; accessToken: string } | undefined> { + if (!pacWrapper) { + return undefined; + } + + try { + // Get the active org from PAC CLI + const activeOrgResult = await pacWrapper.activeOrg(); + + if (activeOrgResult?.Status !== 'Success' || !activeOrgResult.Results?.OrgUrl) { + return undefined; + } + + const orgUrl = activeOrgResult.Results.OrgUrl; + + // Get an access token for the Dataverse org + const authResult = await dataverseAuthentication(orgUrl); + + if (!authResult.accessToken) { + return undefined; + } + + return { + orgUrl, + accessToken: authResult.accessToken + }; + } catch (error) { + oneDSLoggerWrapper.getLogger().traceError( + desktopTelemetryEventNames.SERVER_LOGIC_AUTH_ERROR, + 'Failed to get Dataverse credentials', + error instanceof Error ? error : new Error(String(error)) + ); + return undefined; + } +} + +/** + * Validates the current editor has a server logic file open + * @returns The file path if valid, undefined otherwise + */ +function validateServerLogicFile(): string | undefined { + const editor = vscode.window.activeTextEditor; + if (!editor) { + vscode.window.showErrorMessage(vscode.l10n.t('No active editor found.')); + return undefined; + } + + const filePath = editor.document.uri.fsPath; + if (!filePath.includes('server-logics') || !filePath.endsWith('.js')) { + vscode.window.showWarningMessage( + vscode.l10n.t('Please open a server logic file (.js) from the server-logics folder.') + ); + return undefined; + } + + return filePath; +} + +/** + * Starts a debug session for the server logic file + * @param filePath Path to the server logic file + * @param pacWrapper PacWrapper for authentication + * @param noDebug Whether to run without debugging + */ +async function startServerLogicSession( + filePath: string, + pacWrapper: PacWrapper | undefined, + noDebug: boolean +): Promise<{ hasCredentials: boolean }> { + const env: Record = {}; + + // Try to get credentials from PAC auth + const credentials = await getDataverseCredentials(pacWrapper); + + if (credentials) { + env.DATAVERSE_URL = credentials.orgUrl; + env.DATAVERSE_TOKEN = credentials.accessToken; + } + + const sessionName = noDebug + ? vscode.l10n.t('Run Server Logic') + : vscode.l10n.t('Debug Current Server Logic'); + + await vscode.debug.startDebugging( + vscode.workspace.getWorkspaceFolder(vscode.window.activeTextEditor!.document.uri), + { + type: 'node', + request: 'launch', + name: sessionName, + program: filePath, + skipFiles: ['/**'], + console: 'internalConsole', + ...(noDebug && { noDebug: true }), + env + } + ); + + return { hasCredentials: !!credentials }; +} + +/** + * Activates the Server Logic debugger + * @param context The extension context + * @param pacWrapper Optional PacWrapper instance for automatic authentication + */ +export function activateServerLogicDebugger(context: vscode.ExtensionContext, pacWrapper?: PacWrapper): void { + const provider = new ServerLogicDebugProvider(); + context.subscriptions.push( + vscode.debug.registerDebugConfigurationProvider('node', provider) + ); + + const codeLensProvider = new ServerLogicCodeLensProvider(); + context.subscriptions.push( + vscode.languages.registerCodeLensProvider( + { pattern: '**/server-logics/**/*.js' }, + codeLensProvider + ) + ); + + context.subscriptions.push( + vscode.commands.registerCommand( + 'powerpages.debugServerLogic', + async () => { + const filePath = validateServerLogicFile(); + if (!filePath) { + return; + } + + const result = await startServerLogicSession(filePath, pacWrapper, false); + + oneDSLoggerWrapper.getLogger().traceInfo( + desktopTelemetryEventNames.SERVER_LOGIC_DEBUG_COMMAND_EXECUTED, + { + fileName: path.basename(filePath), + hasDataverseCredentials: result.hasCredentials + } + ); + } + ) + ); + + context.subscriptions.push( + vscode.commands.registerCommand( + 'powerpages.runServerLogic', + async () => { + const filePath = validateServerLogicFile(); + if (!filePath) { + return; + } + + const result = await startServerLogicSession(filePath, pacWrapper, true); + + oneDSLoggerWrapper.getLogger().traceInfo( + desktopTelemetryEventNames.SERVER_LOGIC_RUN_COMMAND_EXECUTED, + { + fileName: path.basename(filePath), + hasDataverseCredentials: result.hasCredentials + } + ); + } + ) + ); + + const workspaceFolders = vscode.workspace.workspaceFolders; + if (workspaceFolders && workspaceFolders.length > 0) { + const serverLogicsPath = path.join(workspaceFolders[0].uri.fsPath, 'server-logics'); + if (fs.existsSync(serverLogicsPath)) { + showServerLogicWelcomeNotification(); + } + } +} + +/** + * Shows a welcome notification for server logic debugging + */ +function showServerLogicWelcomeNotification(): void { + const dontShowAgainKey = 'powerPages.serverLogic.dontShowWelcome'; + const dontShowAgain = vscode.workspace.getConfiguration().get(dontShowAgainKey, false); + + if (dontShowAgain) { + return; + } + + const debugButton = vscode.l10n.t('Debug Current File'); + const learnMoreButton = vscode.l10n.t('Learn More'); + const dontShowButton = vscode.l10n.t("Don't Show Again"); + + vscode.window.showInformationMessage( + vscode.l10n.t('🎯 Power Pages Server Logic detected! You can now debug your server logic files with breakpoints and IntelliSense.'), + debugButton, + learnMoreButton, + dontShowButton + ).then(selection => { + if (selection === debugButton) { + vscode.commands.executeCommand('powerpages.debugServerLogic'); + } else if (selection === learnMoreButton) { + vscode.env.openExternal( + vscode.Uri.parse('https://learn.microsoft.com/power-pages/configure/server-side-scripting') + ); + } else if (selection === dontShowButton) { + vscode.workspace.getConfiguration().update( + dontShowAgainKey, + true, + vscode.ConfigurationTarget.Global + ); + } + }); +} diff --git a/src/debugger/server-logic/ServerLogicMockSdk.ts b/src/debugger/server-logic/ServerLogicMockSdk.ts new file mode 100644 index 000000000..0e4ae2110 --- /dev/null +++ b/src/debugger/server-logic/ServerLogicMockSdk.ts @@ -0,0 +1,1048 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +/** + * Generates the Mock SDK Runtime Loader for Power Pages Server Logic debugging. + */ +export function generateServerMockSdk(): string { + return ` +/** + * Power Pages Server Logic - Mock SDK Runtime Loader + * + * This file provides mock implementations of the Server.* APIs for local debugging. + * + * HOW TO USE: + * 1. Edit the mock data below to match your test scenarios + * 2. Customize Server.Context with your test inputs (query params, body, headers) + * 3. Set breakpoints in your server logic file and press F5 to debug + * + * DIRECT DATAVERSE CALLS: + * When authenticated to Power Platform (via PAC CLI), Dataverse calls will + * automatically use your authenticated credentials. No additional configuration needed! + * + * If not authenticated, mock responses will be returned instead. + * This file is auto-generated and added to .gitignore. + */ + +const DataverseConfig = { + getUrl: function() { + return process.env.DATAVERSE_URL || ''; + }, + + getToken: function() { + return process.env.DATAVERSE_TOKEN || ''; + }, + + getApiVersion: function() { + return process.env.DATAVERSE_API_VERSION || 'v9.2'; + }, + + isConfigured: function() { + return this.getUrl() && this.getToken(); + }, + + getApiUrl: function() { + const baseUrl = this.getUrl().endsWith('/') ? this.getUrl().slice(0, -1) : this.getUrl(); + return \`\${baseUrl}/api/data/\${this.getApiVersion()}\`; + }, + + getHeaders: function() { + return { + 'Authorization': \`Bearer \${this.getToken()}\`, + 'OData-MaxVersion': '4.0', + 'OData-Version': '4.0', + 'Accept': 'application/json', + 'Content-Type': 'application/json; charset=utf-8', + 'Prefer': 'return=representation' + }; + } +}; + +/** + * Helper function to make HTTP requests to Dataverse + */ +async function makeDataverseRequest(method, endpoint, body = null) { + const https = require('https'); + const urlLib = require('url'); + + const apiUrl = DataverseConfig.getApiUrl(); + const fullUrl = \`\${apiUrl}/\${endpoint}\`; + const parsedUrl = urlLib.parse(fullUrl); + + return new Promise((resolve, reject) => { + const headers = DataverseConfig.getHeaders(); + if (body) { + headers['Content-Length'] = Buffer.byteLength(body); + } + + const options = { + hostname: parsedUrl.hostname, + port: parsedUrl.port || 443, + path: parsedUrl.path, + method: method, + headers: headers + }; + + Server.Logger.Log(\`[DATAVERSE] Making \${method} request to: \${fullUrl}\`); + + const req = https.request(options, (res) => { + let data = ''; + + res.on('data', (chunk) => { + data += chunk; + }); + + res.on('end', () => { + const responseHeaders = {}; + for (const [key, value] of Object.entries(res.headers)) { + responseHeaders[key] = Array.isArray(value) ? value.join(', ') : value; + } + + // Extract entity ID from OData-EntityId header if present + let entityId = null; + if (responseHeaders['odata-entityid']) { + const match = responseHeaders['odata-entityid'].match(/\\(([^)]+)\\)/); + if (match) { + entityId = match[1]; + } + } + + const response = { + StatusCode: res.statusCode, + Body: data || '', + IsSuccessStatusCode: res.statusCode >= 200 && res.statusCode < 300, + ReasonPhrase: res.statusMessage || '', + ServerError: false, + ServerErrorMessage: null, + Headers: responseHeaders + }; + + if (entityId) { + response.Headers.entityId = entityId; + } + + Server.Logger.Log(\`[DATAVERSE] Request completed with status: \${res.statusCode}\`); + resolve(response); + }); + }); + + req.on('error', (error) => { + Server.Logger.Error(\`[DATAVERSE] Request failed: \${error.message}\`); + const errorResponse = { + StatusCode: 0, + Body: null, + IsSuccessStatusCode: false, + ReasonPhrase: null, + ServerError: true, + ServerErrorMessage: \`Error executing \${method} request to '\${fullUrl}': \${error.message}\`, + Headers: {} + }; + resolve(errorResponse); + }); + + if (body) { + req.write(body); + } + req.end(); + }); +} + +const Server = { + Logger: { + Log: function (message) { + console.log(\`[LOG] \${new Date().toISOString()} - \${message}\`); + }, + Error: function (message) { + console.error(\`[ERROR] \${new Date().toISOString()} - \${message}\`); + }, + Warning: function (message) { + console.warn(\`[WARNING] \${new Date().toISOString()} - \${message}\`); + }, + Info: function (message) { + console.info(\`[INFO] \${new Date().toISOString()} - \${message}\`); + } + }, + + Context: { + QueryParameters: { + "id": "12345-test-guid" + }, + Headers: { + "Authorization": "Bearer mock-token", + "Content-Type": "application/json", + "User-Agent": "MockClient/1.0" + }, + Body: JSON.stringify({ + name: "Test Account", + telephone1: "555-0100" + }), + Method: "POST", // GET, POST, PUT, PATCH, DELETE + Url: "https://mock-server.example.com/api/test" + }, + + Connector: { + HttpClient: { + GetAsync: async function (url, headers = {}) { + Server.Logger.Log(\`[MOCK] HttpClient.GetAsync called with URL: \${url}\`); + + const isNodeJs = typeof process !== 'undefined' && + process.versions != null && + process.versions.node != null; + + if (isNodeJs) { + Server.Logger.Log(\`[NODE.JS] Making actual GET request to: \${url}\`); + + try { + const https = require('https'); + const http = require('http'); + const urlLib = require('url'); + + const parsedUrl = urlLib.parse(url); + const protocol = parsedUrl.protocol === 'https:' ? https : http; + + return new Promise((resolve, reject) => { + const options = { + hostname: parsedUrl.hostname, + port: parsedUrl.port, + path: parsedUrl.path, + method: 'GET', + headers: headers + }; + + const req = protocol.request(options, (res) => { + let data = ''; + + res.on('data', (chunk) => { + data += chunk; + }); + + res.on('end', () => { + const responseHeaders = {}; + for (const [key, value] of Object.entries(res.headers)) { + responseHeaders[key] = Array.isArray(value) ? value.join(', ') : value; + } + + const response = { + StatusCode: res.statusCode, + Body: data, + IsSuccessStatusCode: res.statusCode >= 200 && res.statusCode < 300, + ReasonPhrase: res.statusMessage || '', + ServerError: false, + ServerErrorMessage: null, + Headers: responseHeaders + }; + Server.Logger.Log(\`[NODE.JS] GET request completed with status: \${res.statusCode}\`); + resolve(JSON.stringify(response)); + }); + }); + + req.on('error', (error) => { + Server.Logger.Error(\`[NODE.JS] GET request failed: \${error.message}\`); + + const errorResponse = { + StatusCode: 0, + Body: null, + IsSuccessStatusCode: false, + ReasonPhrase: null, + ServerError: true, + ServerErrorMessage: \`Error executing GET request to '\${url}': \${error.message}\`, + Headers: {} + }; + resolve(JSON.stringify(errorResponse)); + }); + + req.end(); + }); + } catch (error) { + Server.Logger.Error(\`[NODE.JS] Error making GET request: \${error.message}\`); + + const errorResponse = { + StatusCode: 0, + Body: null, + IsSuccessStatusCode: false, + ReasonPhrase: null, + ServerError: true, + ServerErrorMessage: \`Error executing GET request to '\${url}': \${error.message}\`, + Headers: {} + }; + return JSON.stringify(errorResponse); + } + } else { + Server.Logger.Log(\`[BROWSER] Returning mock response\`); + + await new Promise(resolve => setTimeout(resolve, 100)); + + const mockResponse = { + StatusCode: 200, + Body: "{}", + IsSuccessStatusCode: true, + ReasonPhrase: "OK", + ServerError: false, + ServerErrorMessage: null, + Headers: { + "Content-Type": "application/json", + "X-Mock-Response": "true" + } + }; + + return JSON.stringify(mockResponse); + } + }, + + PostAsync: async function (url, body, headers = {}, contentType = "application/json") { + Server.Logger.Log(\`[MOCK] HttpClient.PostAsync called with URL: \${url}\`); + Server.Logger.Log(\`[MOCK] Body: \${body}\`); + + const isNodeJs = typeof process !== 'undefined' && + process.versions != null && + process.versions.node != null; + + if (isNodeJs) { + Server.Logger.Log(\`[NODE.JS] Making actual POST request to: \${url}\`); + + try { + const https = require('https'); + const http = require('http'); + const urlLib = require('url'); + + const parsedUrl = urlLib.parse(url); + const protocol = parsedUrl.protocol === 'https:' ? https : http; + + return new Promise((resolve, reject) => { + const requestHeaders = { + ...headers, + 'Content-Type': contentType, + 'Content-Length': Buffer.byteLength(body || '') + }; + + const options = { + hostname: parsedUrl.hostname, + port: parsedUrl.port, + path: parsedUrl.path, + method: 'POST', + headers: requestHeaders + }; + + const req = protocol.request(options, (res) => { + let data = ''; + + res.on('data', (chunk) => { + data += chunk; + }); + + res.on('end', () => { + const responseHeaders = {}; + for (const [key, value] of Object.entries(res.headers)) { + responseHeaders[key] = Array.isArray(value) ? value.join(', ') : value; + } + + const response = { + StatusCode: res.statusCode, + Body: data, + IsSuccessStatusCode: res.statusCode >= 200 && res.statusCode < 300, + ReasonPhrase: res.statusMessage || '', + ServerError: false, + ServerErrorMessage: null, + Headers: responseHeaders + }; + Server.Logger.Log(\`[NODE.JS] POST request completed with status: \${res.statusCode}\`); + resolve(JSON.stringify(response)); + }); + }); + + req.on('error', (error) => { + Server.Logger.Error(\`[NODE.JS] POST request failed: \${error.message}\`); + const errorResponse = { + StatusCode: 0, + Body: null, + IsSuccessStatusCode: false, + ReasonPhrase: null, + ServerError: true, + ServerErrorMessage: \`Error executing POST request to '\${url}': \${error.message}\`, + Headers: {} + }; + resolve(JSON.stringify(errorResponse)); + }); + + if (body) { + req.write(body); + } + req.end(); + }); + } catch (error) { + Server.Logger.Error(\`[NODE.JS] Error making POST request: \${error.message}\`); + const errorResponse = { + StatusCode: 0, + Body: null, + IsSuccessStatusCode: false, + ReasonPhrase: null, + ServerError: true, + ServerErrorMessage: \`Error executing POST request to '\${url}': \${error.message}\`, + Headers: {} + }; + return JSON.stringify(errorResponse); + } + } else { + await new Promise(resolve => setTimeout(resolve, 100)); + + const mockResponse = { + StatusCode: 201, + Body: "{}", + IsSuccessStatusCode: true, + ReasonPhrase: "Created", + ServerError: false, + ServerErrorMessage: null, + Headers: { + "Content-Type": contentType, + "X-Mock-Response": "true" + } + }; + + return JSON.stringify(mockResponse); + } + }, + + PatchAsync: async function (url, body, headers = {}, contentType = "application/json") { + Server.Logger.Log(\`[MOCK] HttpClient.PatchAsync called with URL: \${url}\`); + Server.Logger.Log(\`[MOCK] Body: \${body}\`); + + const isNodeJs = typeof process !== 'undefined' && + process.versions != null && + process.versions.node != null; + + if (isNodeJs) { + Server.Logger.Log(\`[NODE.JS] Making actual PATCH request to: \${url}\`); + + try { + const https = require('https'); + const http = require('http'); + const urlLib = require('url'); + + const parsedUrl = urlLib.parse(url); + const protocol = parsedUrl.protocol === 'https:' ? https : http; + + return new Promise((resolve, reject) => { + const requestHeaders = { + ...headers, + 'Content-Type': contentType, + 'Content-Length': Buffer.byteLength(body || '') + }; + + const options = { + hostname: parsedUrl.hostname, + port: parsedUrl.port, + path: parsedUrl.path, + method: 'PATCH', + headers: requestHeaders + }; + + const req = protocol.request(options, (res) => { + let data = ''; + + res.on('data', (chunk) => { + data += chunk; + }); + + res.on('end', () => { + const responseHeaders = {}; + for (const [key, value] of Object.entries(res.headers)) { + responseHeaders[key] = Array.isArray(value) ? value.join(', ') : value; + } + + const response = { + StatusCode: res.statusCode, + Body: data, + IsSuccessStatusCode: res.statusCode >= 200 && res.statusCode < 300, + ReasonPhrase: res.statusMessage || '', + ServerError: false, + ServerErrorMessage: null, + Headers: responseHeaders + }; + Server.Logger.Log(\`[NODE.JS] PATCH request completed with status: \${res.statusCode}\`); + resolve(JSON.stringify(response)); + }); + }); + + req.on('error', (error) => { + Server.Logger.Error(\`[NODE.JS] PATCH request failed: \${error.message}\`); + const errorResponse = { + StatusCode: 0, + Body: null, + IsSuccessStatusCode: false, + ReasonPhrase: null, + ServerError: true, + ServerErrorMessage: \`Error executing PATCH request to '\${url}': \${error.message}\`, + Headers: {} + }; + resolve(JSON.stringify(errorResponse)); + }); + + if (body) { + req.write(body); + } + req.end(); + }); + } catch (error) { + Server.Logger.Error(\`[NODE.JS] Error making PATCH request: \${error.message}\`); + const errorResponse = { + StatusCode: 0, + Body: null, + IsSuccessStatusCode: false, + ReasonPhrase: null, + ServerError: true, + ServerErrorMessage: \`Error executing PATCH request to '\${url}': \${error.message}\`, + Headers: {} + }; + return JSON.stringify(errorResponse); + } + } else { + await new Promise(resolve => setTimeout(resolve, 100)); + + const mockResponse = { + StatusCode: 200, + Body: "{}", + IsSuccessStatusCode: true, + ReasonPhrase: "OK", + ServerError: false, + ServerErrorMessage: null, + Headers: { + "Content-Type": contentType, + "X-Mock-Response": "true" + } + }; + + return JSON.stringify(mockResponse); + } + }, + + PutAsync: async function (url, body, headers = {}, contentType = "application/json") { + Server.Logger.Log(\`[MOCK] HttpClient.PutAsync called with URL: \${url}\`); + Server.Logger.Log(\`[MOCK] Body: \${body}\`); + + const isNodeJs = typeof process !== 'undefined' && + process.versions != null && + process.versions.node != null; + + if (isNodeJs) { + Server.Logger.Log(\`[NODE.JS] Making actual PUT request to: \${url}\`); + + try { + const https = require('https'); + const http = require('http'); + const urlLib = require('url'); + + const parsedUrl = urlLib.parse(url); + const protocol = parsedUrl.protocol === 'https:' ? https : http; + + return new Promise((resolve, reject) => { + const requestHeaders = { + ...headers, + 'Content-Type': contentType, + 'Content-Length': Buffer.byteLength(body || '') + }; + + const options = { + hostname: parsedUrl.hostname, + port: parsedUrl.port, + path: parsedUrl.path, + method: 'PUT', + headers: requestHeaders + }; + + const req = protocol.request(options, (res) => { + let data = ''; + + res.on('data', (chunk) => { + data += chunk; + }); + + res.on('end', () => { + const responseHeaders = {}; + for (const [key, value] of Object.entries(res.headers)) { + responseHeaders[key] = Array.isArray(value) ? value.join(', ') : value; + } + + const response = { + StatusCode: res.statusCode, + Body: data, + IsSuccessStatusCode: res.statusCode >= 200 && res.statusCode < 300, + ReasonPhrase: res.statusMessage || '', + ServerError: false, + ServerErrorMessage: null, + Headers: responseHeaders + }; + Server.Logger.Log(\`[NODE.JS] PUT request completed with status: \${res.statusCode}\`); + resolve(JSON.stringify(response)); + }); + }); + + req.on('error', (error) => { + Server.Logger.Error(\`[NODE.JS] PUT request failed: \${error.message}\`); + const errorResponse = { + StatusCode: 0, + Body: null, + IsSuccessStatusCode: false, + ReasonPhrase: null, + ServerError: true, + ServerErrorMessage: \`Error executing PUT request to '\${url}': \${error.message}\`, + Headers: {} + }; + resolve(JSON.stringify(errorResponse)); + }); + + if (body) { + req.write(body); + } + req.end(); + }); + } catch (error) { + Server.Logger.Error(\`[NODE.JS] Error making PUT request: \${error.message}\`); + const errorResponse = { + StatusCode: 0, + Body: null, + IsSuccessStatusCode: false, + ReasonPhrase: null, + ServerError: true, + ServerErrorMessage: \`Error executing PUT request to '\${url}': \${error.message}\`, + Headers: {} + }; + return JSON.stringify(errorResponse); + } + } else { + await new Promise(resolve => setTimeout(resolve, 100)); + + const mockResponse = { + StatusCode: 200, + Body: "{}", + IsSuccessStatusCode: true, + ReasonPhrase: "OK", + ServerError: false, + ServerErrorMessage: null, + Headers: { + "Content-Type": contentType, + "X-Mock-Response": "true" + } + }; + + return JSON.stringify(mockResponse); + } + }, + + DeleteAsync: async function (url, headers = {}, contentType = "application/json") { + Server.Logger.Log(\`[MOCK] HttpClient.DeleteAsync called with URL: \${url}\`); + + const isNodeJs = typeof process !== 'undefined' && + process.versions != null && + process.versions.node != null; + + if (isNodeJs) { + Server.Logger.Log(\`[NODE.JS] Making actual DELETE request to: \${url}\`); + + try { + const https = require('https'); + const http = require('http'); + const urlLib = require('url'); + + const parsedUrl = urlLib.parse(url); + const protocol = parsedUrl.protocol === 'https:' ? https : http; + + return new Promise((resolve, reject) => { + const options = { + hostname: parsedUrl.hostname, + port: parsedUrl.port, + path: parsedUrl.path, + method: 'DELETE', + headers: headers + }; + + const req = protocol.request(options, (res) => { + let data = ''; + + res.on('data', (chunk) => { + data += chunk; + }); + + res.on('end', () => { + const responseHeaders = {}; + for (const [key, value] of Object.entries(res.headers)) { + responseHeaders[key] = Array.isArray(value) ? value.join(', ') : value; + } + + const response = { + StatusCode: res.statusCode, + Body: data, + IsSuccessStatusCode: res.statusCode >= 200 && res.statusCode < 300, + ReasonPhrase: res.statusMessage || '', + ServerError: false, + ServerErrorMessage: null, + Headers: responseHeaders + }; + Server.Logger.Log(\`[NODE.JS] DELETE request completed with status: \${res.statusCode}\`); + resolve(JSON.stringify(response)); + }); + }); + + req.on('error', (error) => { + Server.Logger.Error(\`[NODE.JS] DELETE request failed: \${error.message}\`); + const errorResponse = { + StatusCode: 0, + Body: null, + IsSuccessStatusCode: false, + ReasonPhrase: null, + ServerError: true, + ServerErrorMessage: \`Error executing DELETE request to '\${url}': \${error.message}\`, + Headers: {} + }; + resolve(JSON.stringify(errorResponse)); + }); + + req.end(); + }); + } catch (error) { + Server.Logger.Error(\`[NODE.JS] Error making DELETE request: \${error.message}\`); + const errorResponse = { + StatusCode: 0, + Body: null, + IsSuccessStatusCode: false, + ReasonPhrase: null, + ServerError: true, + ServerErrorMessage: \`Error executing DELETE request to '\${url}': \${error.message}\`, + Headers: {} + }; + return JSON.stringify(errorResponse); + } + } else { + await new Promise(resolve => setTimeout(resolve, 100)); + + const mockResponse = { + StatusCode: 204, + Body: "", + IsSuccessStatusCode: true, + ReasonPhrase: "No Content", + ServerError: false, + ServerErrorMessage: null, + Headers: { + "X-Mock-Response": "true" + } + }; + + return JSON.stringify(mockResponse); + } + } + }, + + Dataverse: { + CreateRecord: async function (entitySetName, payload) { + Server.Logger.Log(\`Dataverse.CreateRecord called for entity: \${entitySetName}\`); + + try { + // Check if direct Dataverse calls are configured + if (DataverseConfig.isConfigured()) { + const response = await makeDataverseRequest('POST', entitySetName, payload); + return JSON.stringify(response); + } + + // Return mock response if not configured + const response = { + StatusCode: 204, + Body: "", + IsSuccessStatusCode: true, + ReasonPhrase: "No Content", + ServerError: false, + ServerErrorMessage: null, + Headers: { + "Location": "", + "entityId": "xxxxxxxx-74cc-f011-8545-7ced8d3b4d9e", + "X-Mock-Response": "true" + } + }; + + return JSON.stringify(response); + } catch (error) { + const errorResponse = { + StatusCode: 500, + Body: null, + IsSuccessStatusCode: false, + ReasonPhrase: "Internal Server Error", + ServerError: true, + ServerErrorMessage: \`Error executing POST request to '\${entitySetName}': \${error.message}\`, + Headers: {} + }; + return JSON.stringify(errorResponse); + } + }, + + RetrieveRecord: async function (entitySetName, id, options = null, skipCache = false) { + Server.Logger.Log(\`Dataverse.RetrieveRecord called for entity: \${entitySetName}, id: \${id}\`); + + try { + // Check if direct Dataverse calls are configured + if (DataverseConfig.isConfigured()) { + let endpoint = \`\${entitySetName}(\${id})\`; + if (options) { + endpoint += options; + } + const response = await makeDataverseRequest('GET', endpoint); + return JSON.stringify(response); + } + + // Return mock response if not configured + const response = { + StatusCode: 200, + Body: "{}", + IsSuccessStatusCode: true, + ReasonPhrase: "OK", + ServerError: false, + ServerErrorMessage: null, + Headers: { + "OData-Version": "4.0", + "X-Mock-Response": "true" + } + }; + + return JSON.stringify(response); + } catch (error) { + const errorResponse = { + StatusCode: 500, + Body: null, + IsSuccessStatusCode: false, + ReasonPhrase: "Internal Server Error", + ServerError: true, + ServerErrorMessage: \`Error executing GET request to '\${entitySetName}(\${id})': \${error.message}\`, + Headers: {} + }; + return JSON.stringify(errorResponse); + } + }, + + RetrieveMultipleRecords: async function (entitySetName, options = null, skipCache = false) { + Server.Logger.Log(\`Dataverse.RetrieveMultipleRecords called for entity: \${entitySetName}\`); + + try { + // Check if direct Dataverse calls are configured + if (DataverseConfig.isConfigured()) { + let endpoint = entitySetName; + if (options) { + endpoint += options; + } + const response = await makeDataverseRequest('GET', endpoint); + return JSON.stringify(response); + } + + // Return mock response if not configured + const response = { + StatusCode: 200, + Body: "{}", + IsSuccessStatusCode: true, + ReasonPhrase: "OK", + ServerError: false, + ServerErrorMessage: null, + Headers: { + "OData-Version": "4.0", + "X-Mock-Response": "true" + } + }; + + return JSON.stringify(response); + } catch (error) { + const errorResponse = { + StatusCode: 500, + Body: null, + IsSuccessStatusCode: false, + ReasonPhrase: "Internal Server Error", + ServerError: true, + ServerErrorMessage: \`Error executing GET request to '\${entitySetName}': \${error.message}\`, + Headers: {} + }; + return JSON.stringify(errorResponse); + } + }, + + UpdateRecord: async function (entitySetName, id, payload) { + Server.Logger.Log(\`Dataverse.UpdateRecord called for entity: \${entitySetName}, id: \${id}\`); + + try { + // Check if direct Dataverse calls are configured + if (DataverseConfig.isConfigured()) { + const endpoint = \`\${entitySetName}(\${id})\`; + const response = await makeDataverseRequest('PATCH', endpoint, payload); + return JSON.stringify(response); + } + + // Return mock response if not configured + const response = { + StatusCode: 204, + Body: "", + IsSuccessStatusCode: true, + ReasonPhrase: "No Content", + ServerError: false, + ServerErrorMessage: null, + Headers: { + "Location": "", + "entityId": "xxxxxxxx-74cc-f011-8545-7ced8d3b4d9e", + "X-Mock-Response": "true" + } + }; + + return JSON.stringify(response); + } catch (error) { + const errorResponse = { + StatusCode: 500, + Body: null, + IsSuccessStatusCode: false, + ReasonPhrase: "Internal Server Error", + ServerError: true, + ServerErrorMessage: \`Error executing PATCH request to '\${entitySetName}(\${id})': \${error.message}\`, + Headers: {} + }; + return JSON.stringify(errorResponse); + } + }, + + DeleteRecord: async function (entitySetName, id) { + Server.Logger.Log(\`Dataverse.DeleteRecord called for entity: \${entitySetName}, id: \${id}\`); + + try { + // Check if direct Dataverse calls are configured + if (DataverseConfig.isConfigured()) { + const endpoint = \`\${entitySetName}(\${id})\`; + const response = await makeDataverseRequest('DELETE', endpoint); + return JSON.stringify(response); + } + + // Return mock response if not configured + const response = { + StatusCode: 204, + Body: "", + IsSuccessStatusCode: true, + ReasonPhrase: "No Content", + ServerError: false, + ServerErrorMessage: null, + Headers: { + "X-Mock-Response": "true" + } + }; + + return JSON.stringify(response); + } catch (error) { + const errorResponse = { + StatusCode: 500, + Body: null, + IsSuccessStatusCode: false, + ReasonPhrase: "Internal Server Error", + ServerError: true, + ServerErrorMessage: \`Error executing DELETE request to '\${entitySetName}(\${id})': \${error.message}\`, + Headers: {} + }; + return JSON.stringify(errorResponse); + } + }, + + InvokeCustomApi: async function (method, url, payload = null) { + Server.Logger.Log(\`Dataverse.InvokeCustomApi called with method: \${method}, url: \${url}\`); + + try { + // Validate method (only GET and POST supported) + const upperMethod = method?.toUpperCase(); + if (upperMethod !== 'GET' && upperMethod !== 'POST') { + const errorResponse = { + StatusCode: 400, + Body: null, + IsSuccessStatusCode: false, + ReasonPhrase: \`The HTTP method '\${method}' is not supported for invoking custom APIs. Only 'GET' and 'POST' are supported.\`, + ServerError: true, + ServerErrorMessage: \`Error executing \${method} request to '\${url}'\`, + Headers: {} + }; + return JSON.stringify(errorResponse); + } + + // Check if direct Dataverse calls are configured + if (DataverseConfig.isConfigured()) { + const response = await makeDataverseRequest(upperMethod, url, upperMethod === 'POST' ? payload : null); + return JSON.stringify(response); + } + + // Return mock response if not configured + const response = { + StatusCode: 200, + Body: "", + IsSuccessStatusCode: true, + ReasonPhrase: "OK", + ServerError: false, + ServerErrorMessage: null, + Headers: { + "X-Mock-Response": "true" + } + }; + + return JSON.stringify(response); + } catch (error) { + const errorResponse = { + StatusCode: 500, + Body: null, + IsSuccessStatusCode: false, + ReasonPhrase: "Internal Server Error", + ServerError: true, + ServerErrorMessage: \`Error executing \${method} request to '\${url}': \${error.message}\`, + Headers: {} + }; + return JSON.stringify(errorResponse); + } + } + } + }, + + User: { + fullname: "Mock Test User", + email: "mockuser@example.com", + Roles: ["System Administrator", "Portal User"], + contactid: "contact-mock-guid-12345", + }, + + SiteSetting: { + Get: function (name) { + Server.Logger.Log(\`[MOCK] SiteSetting.Get called for: \${name}\`); + + return \`mock-value-for-\${name}\`; + } + }, + + Website: { + adx_name: "Mock Website Name", + adx_websiteid: "website-mock-guid-67890" + }, + + EnvironmentVariable: { + Get: function (variableName) { + Server.Logger.Log(\`[MOCK] Environment.GetVariable called for: \${variableName}\`); + + return \`mock-value-for-\${variableName}\`; + } + } +}; + + +// Make available globally for browser/script environments +if (typeof global !== 'undefined') { + global.Server = Server; + global.DataverseConfig = DataverseConfig; +} + +// Log Dataverse configuration status +if (DataverseConfig.isConfigured()) { + console.log('\\n[PowerPages] ✅ Server Logic Mock SDK loaded successfully'); + console.log('[PowerPages] 🔗 Dataverse direct calls ENABLED'); + console.log(\`[PowerPages] 📍 Dataverse URL: \${DataverseConfig.getUrl()}\`); + console.log('[PowerPages] 📝 All Server.* APIs are now available for debugging\\n'); +} else { + console.log('\\n[PowerPages] ✅ Server Logic Mock SDK loaded successfully'); + console.log('[PowerPages] ⚠️ Dataverse direct calls DISABLED (mock mode)'); + console.log('[PowerPages] 💡 Authenticate via PAC CLI to enable direct Dataverse calls:'); + console.log('[PowerPages] pac auth create --environment https://yourorg.crm.dynamics.com'); + console.log('[PowerPages] 📝 All Server.* APIs are now available for debugging (with mock responses)\\n'); +} +`; +} diff --git a/src/debugger/server-logic/index.ts b/src/debugger/server-logic/index.ts new file mode 100644 index 000000000..345ddf380 --- /dev/null +++ b/src/debugger/server-logic/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +export { activateServerLogicDebugger } from './ServerLogicDebugger'; +export { ServerLogicCodeLensProvider } from './ServerLogicCodeLensProvider'; +export { generateServerMockSdk } from './ServerLogicMockSdk'; diff --git a/src/debugger/server-logic/sample-server-logic.js b/src/debugger/server-logic/sample-server-logic.js new file mode 100644 index 000000000..da6ec5b7c --- /dev/null +++ b/src/debugger/server-logic/sample-server-logic.js @@ -0,0 +1,124 @@ +/** +* Power Pages Server Logic +* +* Quick References: +* - Server.Logger → diagnostics logging +* Example: Server.Logger.Log("message") +* Example: Server.Logger.Error("error message") +* +* - Server.Context → query params, headers, body +* Example: Server.Context.QueryParameters["id"], Server.Context.Headers["Authorization"], Server.Context.Body +* +* - Server.Connector.HttpClient → external API calls +* Example: await Server.Connector.HttpClient.GetAsync("/1", {"Content-Type":"application/json"}); +* Example: await Server.Connector.HttpClient.PostAsync("", "{"name":"New Object"}", {"Authorization": "Bearer "},"application/json"); +* Example: await Server.Connector.HttpClient.PatchAsync("/1", "{"capacity":"1 TB"}", {"Authorization": "Bearer "},"application/json"); +* Example: await Server.Connector.HttpClient.DeleteAsync("/1", {"Authorization": "Bearer "},"application/json"); +* +* - Server.Connector.Dataverse → CRUD in Dataverse & CustomApi +* Example: Server.Connector.Dataverse.CreateRecord("accounts", "{"name":"Contoso Ltd."}"); +* Example: Server.Connector.Dataverse.RetrieveRecord("accounts", "accountid-guid", "?$select=name,telephone1"); +* Example: Server.Connector.Dataverse.RetrieveMultipleRecords("accounts", "?$select=name&$top=10"); +* Example: Server.Connector.Dataverse.UpdateRecord("accounts", "accountid-guid", "{"telephone1":"123-456-7890"}"); +* Example: Server.Connector.Dataverse.DeleteRecord("accounts", "accountid-guid"); +* Example: Server.Connector.Dataverse.InvokeCustomApi("GET", "new_CustomApiName", null); +* +* - Server.User → signed-in user info +* Example: Server.User.fullname, Server.User.Roles, Server.User.Token +* +* 🔗 Dataverse Calls: Authenticate via PAC CLI for direct Dataverse access during debugging. +* Run: pac auth create --environment https://yourorg.crm.dynamics.com +* +* Full details: see https://go.microsoft.com/fwlink/?linkid=2334908 +*/ + +function get() { + try { + + if (!Server.Context.QueryParameters["id"]) { + const errorMsg = "Missing required query parameter: id"; + Server.Logger.Error(errorMsg); + return JSON.stringify({ status: "error", method: "GET", message: errorMsg }); + } + + Server.Logger.Log("GET called"); // Logger reference + const id = Server.Context.QueryParameters["id"]; // Context reference + + // 🔹 Quick HttpClient GET example + // const response = await Server.Connector.HttpClient.GetAsync("https://api.nuget.org/v3/index.json", {"Content-Type":"application/json"}); + // return JSON.parse(response.Body); + + + return JSON.stringify({ status: "success", method: "GET", id: id }); + } catch (err) { + Server.Logger.Error("GET failed: " + err.message); + return JSON.stringify({ status: "error", method: "GET", message: err.message }); + } +} + + +function post() { + try { + Server.Logger.Log("POST called"); + const data = Server.Context.Body; + + // 🔹 Quick Dataverse Create example + // const response = Server.Connector.Dataverse.CreateRecord("accounts", JSON.stringify({ name: "New Account", telephone1: "123-456-7890" })); + + return JSON.stringify({ status: "success", method: "POST", data: data }); + } catch (err) { + Server.Logger.Error("POST failed: " + err.message); + return JSON.stringify({ status: "error", method: "POST", message: err.message }); + } +} + + +function put() { + try { + Server.Logger.Log("PUT called"); + const id = Server.Context.QueryParameters["id"]; + const data = Server.Context.Body; + + // 🔹 Quick Dataverse Update example + // var response = Server.Connector.Dataverse.UpdateRecord("accounts", id, data); + + return JSON.stringify({ status: "success", method: "PUT", id: id, data: data }); + } catch (err) { + Server.Logger.Error("PUT failed: " + err.message); + return JSON.stringify({ status: "error", method: "PUT", message: err.message }); + } +} + + +async function patch() { + try { + Server.Logger.Log("PATCH called"); + const id = Server.Context.QueryParameters["id"]; + const data = Server.Context.Body; + + // 🔹 Quick HttpClient PATCH example + // await Server.Connector.HttpClient.PatchAsync("" + id, JSON.stringify({ capacity: "1 TB" }), {"Authorization": "Bearer "},"application/json"); + + return JSON.stringify({ status: "success", method: "PATCH", id: id, data: data }); + } catch (err) { + Server.Logger.Error("PATCH failed: " + err.message); + return JSON.stringify({ status: "error", method: "PATCH", message: err.message }); + } +} + + +function del() { + try { + // "delete" keyword should not be used in script file. + Server.Logger.Log("DEL called"); + const id = Server.Context.QueryParameters["id"]; + + // 🔹 Quick Dataverse Del example + // var response = Server.Connector.Dataverse.DeleteRecord("accounts", id); + + return JSON.stringify({ status: "success", method: "DEL", id: id }); + } catch (err) { + Server.Logger.Error("Deletion failed: " + err.message); + return JSON.stringify({ status: "error", method: "DEL", message: err.message }); + } +}