Skip to content

Commit 23395fe

Browse files
committed
Enhance server logic validation and documentation
- ✏️ Update SKILL.md to clarify async function usage with Dataverse. - ✅ Add validation tests for server logic functions, including checks for async/await usage and error handling. - 📝 Implement detailed error messages for validation failures. -Priyanshu
1 parent 24e209f commit 23395fe

3 files changed

Lines changed: 180 additions & 3 deletions

File tree

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
const test = require('node:test');
2+
const assert = require('node:assert/strict');
3+
const fs = require('fs');
4+
const path = require('path');
5+
const { spawnSync } = require('child_process');
6+
7+
const { createTempProject, writeProjectFile } = require('./test-utils');
8+
9+
const VALIDATOR_PATH = path.join(
10+
__dirname,
11+
'..',
12+
'..',
13+
'skills',
14+
'integrate-serverlogic',
15+
'scripts',
16+
'validate-serverlogic.js'
17+
);
18+
19+
function runValidator(projectRoot) {
20+
const input = JSON.stringify({ cwd: projectRoot });
21+
return spawnSync(process.execPath, [VALIDATOR_PATH], {
22+
input,
23+
encoding: 'utf8',
24+
});
25+
}
26+
27+
function setupProject(projectRoot) {
28+
writeProjectFile(projectRoot, 'powerpages.config.json', '{}');
29+
}
30+
31+
function writeServerLogic(projectRoot, name, jsContent, ymlContent) {
32+
const dir = `.powerpages-site/server-logic/${name}`;
33+
writeProjectFile(projectRoot, `${dir}/${name}.js`, jsContent);
34+
if (ymlContent) {
35+
writeProjectFile(projectRoot, `${dir}/${name}.serverlogic.yml`, ymlContent);
36+
}
37+
}
38+
39+
const VALID_YML = `adx_serverlogic_adx_webrole:
40+
- 11111111-1111-1111-1111-111111111111
41+
description: Test endpoint
42+
display_name: Test
43+
id: 22222222-2222-2222-2222-222222222222
44+
name: test-endpoint`;
45+
46+
const VALID_JS = `function get() {
47+
try {
48+
Server.Logger.Log("test-endpoint GET called");
49+
return JSON.stringify({ status: "success" });
50+
} catch (err) {
51+
Server.Logger.Error("test-endpoint GET failed: " + err.message);
52+
return JSON.stringify({ status: "error", message: err.message });
53+
}
54+
}`;
55+
56+
test('valid server logic passes validation', (t) => {
57+
const projectRoot = createTempProject(t);
58+
setupProject(projectRoot);
59+
writeServerLogic(projectRoot, 'test-endpoint', VALID_JS, VALID_YML);
60+
61+
const result = runValidator(projectRoot);
62+
assert.equal(result.status, 0, result.stderr);
63+
});
64+
65+
test('async function without await is flagged', (t) => {
66+
const projectRoot = createTempProject(t);
67+
setupProject(projectRoot);
68+
const asyncNoAwaitJs = `async function get() {
69+
try {
70+
Server.Logger.Log("test-endpoint GET called");
71+
var records = Server.Connector.Dataverse.RetrieveMultipleRecords("contacts", "?$top=10");
72+
return JSON.stringify({ status: "success", data: records });
73+
} catch (err) {
74+
Server.Logger.Error("test-endpoint GET failed: " + err.message);
75+
return JSON.stringify({ status: "error", message: err.message });
76+
}
77+
}`;
78+
writeServerLogic(projectRoot, 'test-endpoint', asyncNoAwaitJs, VALID_YML);
79+
80+
const result = runValidator(projectRoot);
81+
assert.equal(result.status, 2);
82+
assert.match(result.stderr, /function 'get' is marked async but contains no await/);
83+
});
84+
85+
test('async function with await passes validation', (t) => {
86+
const projectRoot = createTempProject(t);
87+
setupProject(projectRoot);
88+
const asyncWithAwaitJs = `async function post() {
89+
try {
90+
Server.Logger.Log("test-endpoint POST called");
91+
var response = await Server.Connector.HttpClient.PostAsync("https://api.example.com/data", JSON.stringify({ key: "value" }));
92+
return JSON.stringify({ status: "success", data: response });
93+
} catch (err) {
94+
Server.Logger.Error("test-endpoint POST failed: " + err.message);
95+
return JSON.stringify({ status: "error", message: err.message });
96+
}
97+
}`;
98+
writeServerLogic(projectRoot, 'test-endpoint', asyncWithAwaitJs, VALID_YML);
99+
100+
const result = runValidator(projectRoot);
101+
assert.equal(result.status, 0, result.stderr);
102+
});
103+
104+
test('missing js file is flagged', (t) => {
105+
const projectRoot = createTempProject(t);
106+
setupProject(projectRoot);
107+
const dir = path.join(projectRoot, '.powerpages-site', 'server-logic', 'test-endpoint');
108+
fs.mkdirSync(dir, { recursive: true });
109+
writeProjectFile(projectRoot, '.powerpages-site/server-logic/test-endpoint/test-endpoint.serverlogic.yml', VALID_YML);
110+
111+
const result = runValidator(projectRoot);
112+
assert.equal(result.status, 2);
113+
assert.match(result.stderr, /missing \.js file/);
114+
});
115+
116+
test('missing yml file is flagged', (t) => {
117+
const projectRoot = createTempProject(t);
118+
setupProject(projectRoot);
119+
writeServerLogic(projectRoot, 'test-endpoint', VALID_JS, null);
120+
121+
const result = runValidator(projectRoot);
122+
assert.equal(result.status, 2);
123+
assert.match(result.stderr, /missing metadata file/);
124+
});
125+
126+
test('missing try/catch is flagged', (t) => {
127+
const projectRoot = createTempProject(t);
128+
setupProject(projectRoot);
129+
const noTryCatchJs = `function get() {
130+
Server.Logger.Log("test-endpoint GET called");
131+
return JSON.stringify({ status: "success" });
132+
}`;
133+
writeServerLogic(projectRoot, 'test-endpoint', noTryCatchJs, VALID_YML);
134+
135+
const result = runValidator(projectRoot);
136+
assert.equal(result.status, 2);
137+
assert.match(result.stderr, /missing try\/catch/);
138+
});
139+
140+
test('disallowed function name is flagged', (t) => {
141+
const projectRoot = createTempProject(t);
142+
setupProject(projectRoot);
143+
const badFnJs = `function get() {
144+
try {
145+
Server.Logger.Log("test-endpoint GET called");
146+
return JSON.stringify({ status: "success" });
147+
} catch (err) {
148+
Server.Logger.Error("test-endpoint GET failed: " + err.message);
149+
return JSON.stringify({ status: "error", message: err.message });
150+
}
151+
}
152+
153+
function helper() {
154+
return "not allowed";
155+
}`;
156+
writeServerLogic(projectRoot, 'test-endpoint', badFnJs, VALID_YML);
157+
158+
const result = runValidator(projectRoot);
159+
assert.equal(result.status, 2);
160+
assert.match(result.stderr, /found additional top-level functions: helper/);
161+
});
162+
163+
test('yml name mismatch is flagged', (t) => {
164+
const projectRoot = createTempProject(t);
165+
setupProject(projectRoot);
166+
const mismatchYml = VALID_YML.replace('name: test-endpoint', 'name: wrong-name');
167+
writeServerLogic(projectRoot, 'test-endpoint', VALID_JS, mismatchYml);
168+
169+
const result = runValidator(projectRoot);
170+
assert.equal(result.status, 2);
171+
assert.match(result.stderr, /does not match folder name/);
172+
});

plugins/power-pages/skills/integrate-serverlogic/SKILL.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -325,7 +325,7 @@ Repeat this step for each approved server logic item. Create or update `<PROJECT
325325
4. **Each function logs**: Use `Server.Logger.Log()` at entry and `Server.Logger.Error()` in catch blocks.
326326
5. **No imports or requires**: No `import`, `require`, or external dependencies.
327327
6. **No browser APIs**: No `fetch`, `XMLHttpRequest`, `setTimeout`, `setInterval`, `console.log`, or DOM APIs.
328-
7. **Async when needed**: Mark functions as `async` only when they use `await` (HttpClient or Dataverse calls).
328+
7. **Async when needed**: Mark functions as `async` only when they use `await` (HttpClient calls). Dataverse connector methods (`Server.Connector.Dataverse.*`) are **synchronous** — do NOT use `async`/`await` with them.
329329

330330
#### Code Template
331331

@@ -334,7 +334,7 @@ Repeat this step for each approved server logic item. Create or update `<PROJECT
334334
// Purpose: <description>
335335
// API URL: https://<site-url>/_api/serverlogics/<name>
336336
337-
async function get() {
337+
function get() {
338338
try {
339339
Server.Logger.Log("<name> GET called");
340340

plugins/power-pages/skills/integrate-serverlogic/scripts/validate-serverlogic.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,17 +129,22 @@ runValidation((cwd) => {
129129
}
130130

131131
// Check: each function has try/catch (scan until next top-level function or end of file)
132+
// Check: async functions must contain await (unnecessary async causes runtime errors with synchronous Dataverse calls)
132133
for (const fn of foundFunctions) {
133-
const fnRegex = new RegExp(`(?:async\\s+)?function\\s+${fn}\\s*\\([^)]*\\)\\s*\\{`, 'g');
134+
const fnRegex = new RegExp(`(async\\s+)?function\\s+${fn}\\s*\\([^)]*\\)\\s*\\{`, 'g');
134135
const match = fnRegex.exec(content);
135136
if (match) {
137+
const isAsync = !!match[1];
136138
const bodyStart = match.index + match[0].length;
137139
const nextFnMatch = content.slice(bodyStart).match(/\n(?:async\s+)?function\s+[a-zA-Z]/);
138140
const bodyEnd = nextFnMatch ? bodyStart + nextFnMatch.index : content.length;
139141
const fnBody = content.slice(bodyStart, bodyEnd);
140142
if (!/\btry\s*\{/.test(fnBody)) {
141143
errors.push(`${dirName}.js: function '${fn}' is missing try/catch error handling`);
142144
}
145+
if (isAsync && !/\bawait\b/.test(fnBody)) {
146+
errors.push(`${dirName}.js: function '${fn}' is marked async but contains no await — remove async to avoid runtime errors (Dataverse calls are synchronous, only HttpClient requires async/await)`);
147+
}
143148
}
144149
}
145150

0 commit comments

Comments
 (0)