jsgui3-server 0.0.138 → 0.0.140
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +87 -0
- package/README.md +12 -0
- package/docs/GUIDE_TO_AGENTIC_WORKFLOWS_BY_GROK.md +19 -0
- package/docs/advanced-usage-examples.md +1360 -0
- package/docs/agent-development-guide.md +386 -0
- package/docs/api-reference.md +916 -0
- package/docs/broken-functionality-tracker.md +285 -0
- package/docs/bundling-system-deep-dive.md +525 -0
- package/docs/cli-reference.md +393 -0
- package/docs/comprehensive-documentation.md +1403 -0
- package/docs/configuration-reference.md +808 -0
- package/docs/controls-development.md +859 -0
- package/docs/documentation-review/CURRENT_REVIEW.md +95 -0
- package/docs/function-publishers-json-apis.md +847 -0
- package/docs/getting-started-with-json.md +518 -0
- package/docs/minification-compression-sourcemaps-status.md +482 -0
- package/docs/minification-compression-sourcemaps-test-results.md +205 -0
- package/docs/publishers-guide.md +313 -0
- package/docs/resources-guide.md +615 -0
- package/docs/serve-helpers.md +406 -0
- package/docs/simple-server-api-design.md +13 -0
- package/docs/system-architecture.md +275 -0
- package/docs/troubleshooting.md +698 -0
- package/examples/json/README.md +115 -0
- package/examples/json/basic-api/README.md +345 -0
- package/examples/json/basic-api/server.js +199 -0
- package/examples/json/simple-api/README.md +125 -0
- package/examples/json/simple-api/diagnostic-report.json +73 -0
- package/examples/json/simple-api/diagnostic-test.js +433 -0
- package/examples/json/simple-api/server-debug.md +58 -0
- package/examples/json/simple-api/server.js +91 -0
- package/examples/json/simple-api/test.js +215 -0
- package/http/responders/static/Static_Route_HTTP_Responder.js +1 -2
- package/package.json +19 -8
- package/publishers/helpers/assigners/static-compressed-response-buffers/Single_Control_Webpage_Server_Static_Compressed_Response_Buffers_Assigner.js +65 -12
- package/publishers/helpers/preparers/static/bundle/Static_Routes_Responses_Webpage_Bundle_Preparer.js +6 -1
- package/publishers/http-function-publisher.js +59 -38
- package/publishers/http-webpage-publisher.js +48 -1
- package/resources/processors/bundlers/js/esbuild/Advanced_JS_Bundler_Using_ESBuild.js +38 -146
- package/resources/processors/bundlers/js/esbuild/Core_JS_Non_Minifying_Bundler_Using_ESBuild.js +54 -5
- package/resources/processors/bundlers/js/esbuild/Core_JS_Single_File_Minifying_Bundler_Using_ESBuild.js +36 -4
- package/serve-factory.js +36 -9
- package/server.js +10 -4
- package/test-report.json +0 -0
- package/tests/README.md +250 -0
- package/tests/assigners.test.js +316 -0
- package/tests/bundlers.test.js +329 -0
- package/tests/configuration-validation.test.js +530 -0
- package/tests/content-analysis.test.js +641 -0
- package/tests/end-to-end.test.js +496 -0
- package/tests/error-handling.test.js +746 -0
- package/tests/performance.test.js +653 -0
- package/tests/publishers.test.js +395 -0
- package/tests/temp_invalid.js +7 -0
- package/tests/temp_invalid_utf8.js +1 -0
- package/tests/temp_malformed.js +10 -0
- package/tests/test-runner.js +261 -0
|
@@ -0,0 +1,847 @@
|
|
|
1
|
+
# Function Publishers and JSON APIs
|
|
2
|
+
|
|
3
|
+
## When to Read
|
|
4
|
+
|
|
5
|
+
This document explains how to use JSGUI3 Server's function publishers to create JSON APIs. Read this when:
|
|
6
|
+
- You want to create RESTful JSON APIs with JSGUI3 Server
|
|
7
|
+
- You need to understand how function publishers work
|
|
8
|
+
- You're building backend services that return JSON data
|
|
9
|
+
- You want to integrate with frontend applications via HTTP APIs
|
|
10
|
+
- You need to handle different content types and HTTP methods
|
|
11
|
+
|
|
12
|
+
**Note:** For basic usage with `Server.serve()`, see [README.md](../README.md). For advanced examples, see [docs/advanced-usage-examples.md](docs/advanced-usage-examples.md).
|
|
13
|
+
|
|
14
|
+
## Overview
|
|
15
|
+
|
|
16
|
+
JSGUI3 Server provides a powerful function publishing system that automatically creates HTTP endpoints from JavaScript functions. These publishers handle:
|
|
17
|
+
|
|
18
|
+
- **Automatic JSON serialization/deserialization**
|
|
19
|
+
- **Content-type negotiation** (JSON, text, promises)
|
|
20
|
+
- **HTTP method routing** (GET, POST, etc.)
|
|
21
|
+
- **Error handling and propagation**
|
|
22
|
+
- **Async/await support**
|
|
23
|
+
|
|
24
|
+
## Function Publisher Architecture
|
|
25
|
+
|
|
26
|
+
### Core Components
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
HTTP Request → Function Publisher → Your Function → JSON Response
|
|
30
|
+
↓ ↓ ↓ ↓
|
|
31
|
+
Content-Type → Parse Input → Execute → Serialize Output
|
|
32
|
+
application/json JSON Function JSON/text
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Publisher Types
|
|
36
|
+
|
|
37
|
+
1. **Function Publisher** (`http-function-publisher.js`): Direct function-to-HTTP mapping
|
|
38
|
+
2. **Resource Publisher** (`http-resource-publisher.js`): RESTful resource operations
|
|
39
|
+
3. **API Integration**: Built into `Server.serve()` via the `api` option
|
|
40
|
+
|
|
41
|
+
## Basic Function Publishing
|
|
42
|
+
|
|
43
|
+
### Simple API Endpoint
|
|
44
|
+
|
|
45
|
+
```javascript
|
|
46
|
+
const Server = require('jsgui3-server');
|
|
47
|
+
|
|
48
|
+
Server.serve({
|
|
49
|
+
ctrl: require('./client').controls.MyApp,
|
|
50
|
+
|
|
51
|
+
// Function publishers automatically create /api/* routes
|
|
52
|
+
api: {
|
|
53
|
+
// GET/POST /api/hello
|
|
54
|
+
'hello': (name) => `Hello ${name || 'World'}!`,
|
|
55
|
+
|
|
56
|
+
// GET/POST /api/time
|
|
57
|
+
'time': () => new Date().toISOString(),
|
|
58
|
+
|
|
59
|
+
// GET/POST /api/random
|
|
60
|
+
'random': () => Math.random()
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
port: 3000
|
|
64
|
+
});
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**What happens:**
|
|
68
|
+
- Routes are automatically prefixed with `/api/`
|
|
69
|
+
- Functions receive parsed request body as first parameter
|
|
70
|
+
- Return values are automatically JSON-serialized
|
|
71
|
+
- HTTP status codes are set appropriately
|
|
72
|
+
|
|
73
|
+
### Testing the API
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
# Test the endpoints
|
|
77
|
+
curl http://localhost:3000/api/hello
|
|
78
|
+
# {"result":"Hello World!"}
|
|
79
|
+
|
|
80
|
+
curl -X POST http://localhost:3000/api/hello \
|
|
81
|
+
-H "Content-Type: application/json" \
|
|
82
|
+
-d '{"name":"Alice"}'
|
|
83
|
+
# {"result":"Hello Alice!"}
|
|
84
|
+
|
|
85
|
+
curl http://localhost:3000/api/time
|
|
86
|
+
# {"result":"2025-11-02T01:00:00.000Z"}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Input Handling
|
|
90
|
+
|
|
91
|
+
### Content Type Support
|
|
92
|
+
|
|
93
|
+
The function publisher automatically handles different content types:
|
|
94
|
+
|
|
95
|
+
#### JSON Input (`application/json`)
|
|
96
|
+
```javascript
|
|
97
|
+
api: {
|
|
98
|
+
'calculate': (data) => {
|
|
99
|
+
// data is already parsed JSON object
|
|
100
|
+
return {
|
|
101
|
+
sum: data.a + data.b,
|
|
102
|
+
product: data.a * data.b
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
**Request:**
|
|
109
|
+
```bash
|
|
110
|
+
curl -X POST http://localhost:3000/api/calculate \
|
|
111
|
+
-H "Content-Type: application/json" \
|
|
112
|
+
-d '{"a": 5, "b": 3}'
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
**Response:**
|
|
116
|
+
```json
|
|
117
|
+
{
|
|
118
|
+
"sum": 8,
|
|
119
|
+
"product": 15
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
#### Text Input (`text/plain`)
|
|
124
|
+
```javascript
|
|
125
|
+
api: {
|
|
126
|
+
'echo': (text) => `Echo: ${text}`
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
**Request:**
|
|
131
|
+
```bash
|
|
132
|
+
curl -X POST http://localhost:3000/api/echo \
|
|
133
|
+
-H "Content-Type: text/plain" \
|
|
134
|
+
-d "Hello World"
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
**Response:**
|
|
138
|
+
```json
|
|
139
|
+
{
|
|
140
|
+
"result": "Echo: Hello World"
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
#### No Body (GET requests)
|
|
145
|
+
```javascript
|
|
146
|
+
api: {
|
|
147
|
+
'status': () => ({
|
|
148
|
+
uptime: process.uptime(),
|
|
149
|
+
memory: process.memoryUsage(),
|
|
150
|
+
timestamp: new Date()
|
|
151
|
+
})
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
**Request:**
|
|
156
|
+
```bash
|
|
157
|
+
curl http://localhost:3000/api/status
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## Output Handling
|
|
161
|
+
|
|
162
|
+
### Return Type Detection
|
|
163
|
+
|
|
164
|
+
Functions can return different types, automatically handled by content-type detection:
|
|
165
|
+
|
|
166
|
+
#### Objects/Arrays → JSON
|
|
167
|
+
```javascript
|
|
168
|
+
api: {
|
|
169
|
+
'user': () => ({
|
|
170
|
+
id: 123,
|
|
171
|
+
name: "John Doe",
|
|
172
|
+
email: "john@example.com"
|
|
173
|
+
}),
|
|
174
|
+
|
|
175
|
+
'users': () => [
|
|
176
|
+
{ id: 1, name: "Alice" },
|
|
177
|
+
{ id: 2, name: "Bob" }
|
|
178
|
+
]
|
|
179
|
+
}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
#### Strings → Text
|
|
183
|
+
```javascript
|
|
184
|
+
api: {
|
|
185
|
+
'greeting': (name) => `Hello, ${name}!`
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
#### Promises → Async Resolution
|
|
190
|
+
```javascript
|
|
191
|
+
api: {
|
|
192
|
+
'async-data': async () => {
|
|
193
|
+
const data = await fetchExternalAPI();
|
|
194
|
+
return data;
|
|
195
|
+
},
|
|
196
|
+
|
|
197
|
+
'delayed': () => new Promise(resolve => {
|
|
198
|
+
setTimeout(() => resolve({ message: 'Done!' }), 1000);
|
|
199
|
+
})
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## Advanced Patterns
|
|
204
|
+
|
|
205
|
+
### Database Integration
|
|
206
|
+
|
|
207
|
+
```javascript
|
|
208
|
+
// Simulated database
|
|
209
|
+
const db = {
|
|
210
|
+
users: [
|
|
211
|
+
{ id: 1, name: 'Alice', email: 'alice@example.com' },
|
|
212
|
+
{ id: 2, name: 'Bob', email: 'bob@example.com' }
|
|
213
|
+
]
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
Server.serve({
|
|
217
|
+
ctrl: require('./client').controls.App,
|
|
218
|
+
|
|
219
|
+
api: {
|
|
220
|
+
// GET /api/users - List all users
|
|
221
|
+
'users': () => db.users,
|
|
222
|
+
|
|
223
|
+
// GET /api/user?id=1 - Get specific user
|
|
224
|
+
'user': ({ id }) => {
|
|
225
|
+
const user = db.users.find(u => u.id === parseInt(id));
|
|
226
|
+
if (!user) throw new Error('User not found');
|
|
227
|
+
return user;
|
|
228
|
+
},
|
|
229
|
+
|
|
230
|
+
// POST /api/user - Create new user
|
|
231
|
+
'create-user': ({ name, email }) => {
|
|
232
|
+
const newUser = {
|
|
233
|
+
id: db.users.length + 1,
|
|
234
|
+
name,
|
|
235
|
+
email
|
|
236
|
+
};
|
|
237
|
+
db.users.push(newUser);
|
|
238
|
+
return { success: true, user: newUser };
|
|
239
|
+
},
|
|
240
|
+
|
|
241
|
+
// POST /api/update-user - Update existing user
|
|
242
|
+
'update-user': ({ id, ...updates }) => {
|
|
243
|
+
const user = db.users.find(u => u.id === parseInt(id));
|
|
244
|
+
if (!user) throw new Error('User not found');
|
|
245
|
+
|
|
246
|
+
Object.assign(user, updates);
|
|
247
|
+
return { success: true, user };
|
|
248
|
+
}
|
|
249
|
+
},
|
|
250
|
+
|
|
251
|
+
port: 3000
|
|
252
|
+
});
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
### File Upload Handling
|
|
256
|
+
|
|
257
|
+
```javascript
|
|
258
|
+
const fs = require('fs');
|
|
259
|
+
const path = require('path');
|
|
260
|
+
|
|
261
|
+
Server.serve({
|
|
262
|
+
ctrl: require('./client').controls.App,
|
|
263
|
+
|
|
264
|
+
api: {
|
|
265
|
+
// POST /api/upload - Handle file uploads
|
|
266
|
+
'upload': async (files) => {
|
|
267
|
+
const uploadedFiles = [];
|
|
268
|
+
|
|
269
|
+
for (const file of files) {
|
|
270
|
+
const filename = `${Date.now()}-${file.filename}`;
|
|
271
|
+
const filepath = path.join(__dirname, 'uploads', filename);
|
|
272
|
+
|
|
273
|
+
// Ensure uploads directory exists
|
|
274
|
+
fs.mkdirSync(path.dirname(filepath), { recursive: true });
|
|
275
|
+
|
|
276
|
+
// Write file
|
|
277
|
+
fs.writeFileSync(filepath, file.buffer);
|
|
278
|
+
|
|
279
|
+
uploadedFiles.push({
|
|
280
|
+
originalName: file.filename,
|
|
281
|
+
savedName: filename,
|
|
282
|
+
size: file.buffer.length,
|
|
283
|
+
url: `/uploads/${filename}`
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return {
|
|
288
|
+
success: true,
|
|
289
|
+
files: uploadedFiles
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
},
|
|
293
|
+
|
|
294
|
+
port: 3000
|
|
295
|
+
});
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
### Authentication and Authorization
|
|
299
|
+
|
|
300
|
+
```javascript
|
|
301
|
+
const jwt = require('jsonwebtoken');
|
|
302
|
+
const bcrypt = require('bcrypt');
|
|
303
|
+
|
|
304
|
+
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key';
|
|
305
|
+
const users = [
|
|
306
|
+
{ id: 1, username: 'admin', passwordHash: '$2b$10$...' }
|
|
307
|
+
];
|
|
308
|
+
|
|
309
|
+
Server.serve({
|
|
310
|
+
ctrl: require('./client').controls.App,
|
|
311
|
+
|
|
312
|
+
api: {
|
|
313
|
+
// POST /api/login
|
|
314
|
+
'login': async ({ username, password }) => {
|
|
315
|
+
const user = users.find(u => u.username === username);
|
|
316
|
+
if (!user) throw new Error('Invalid credentials');
|
|
317
|
+
|
|
318
|
+
const validPassword = await bcrypt.compare(password, user.passwordHash);
|
|
319
|
+
if (!validPassword) throw new Error('Invalid credentials');
|
|
320
|
+
|
|
321
|
+
const token = jwt.sign(
|
|
322
|
+
{ userId: user.id, username: user.username },
|
|
323
|
+
JWT_SECRET,
|
|
324
|
+
{ expiresIn: '24h' }
|
|
325
|
+
);
|
|
326
|
+
|
|
327
|
+
return { token, user: { id: user.id, username: user.username } };
|
|
328
|
+
},
|
|
329
|
+
|
|
330
|
+
// POST /api/protected - Requires authentication
|
|
331
|
+
'protected': (data, req) => {
|
|
332
|
+
// Extract token from request (implementation depends on auth middleware)
|
|
333
|
+
const authHeader = req.headers.authorization;
|
|
334
|
+
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
335
|
+
throw new Error('No token provided');
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const token = authHeader.substring(7);
|
|
339
|
+
try {
|
|
340
|
+
const decoded = jwt.verify(token, JWT_SECRET);
|
|
341
|
+
return { message: 'Protected data', user: decoded };
|
|
342
|
+
} catch (error) {
|
|
343
|
+
throw new Error('Invalid token');
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
},
|
|
347
|
+
|
|
348
|
+
port: 3000
|
|
349
|
+
});
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
## Error Handling
|
|
353
|
+
|
|
354
|
+
### Automatic Error Propagation
|
|
355
|
+
|
|
356
|
+
```javascript
|
|
357
|
+
api: {
|
|
358
|
+
'risky-operation': (params) => {
|
|
359
|
+
if (!params.requiredField) {
|
|
360
|
+
throw new Error('requiredField is required');
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (params.value < 0) {
|
|
364
|
+
throw new Error('Value must be positive');
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
return { result: params.value * 2 };
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
**Error Response:**
|
|
373
|
+
```json
|
|
374
|
+
{
|
|
375
|
+
"error": "requiredField is required"
|
|
376
|
+
}
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
### Custom Error Types
|
|
380
|
+
|
|
381
|
+
```javascript
|
|
382
|
+
class ValidationError extends Error {
|
|
383
|
+
constructor(message, field) {
|
|
384
|
+
super(message);
|
|
385
|
+
this.name = 'ValidationError';
|
|
386
|
+
this.field = field;
|
|
387
|
+
this.statusCode = 400;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
api: {
|
|
392
|
+
'validate': (data) => {
|
|
393
|
+
if (!data.email || !data.email.includes('@')) {
|
|
394
|
+
throw new ValidationError('Invalid email format', 'email');
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
return { valid: true, email: data.email };
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
## Resource Publishers
|
|
403
|
+
|
|
404
|
+
### RESTful Resource Operations
|
|
405
|
+
|
|
406
|
+
Resource publishers provide CRUD operations for data resources:
|
|
407
|
+
|
|
408
|
+
```javascript
|
|
409
|
+
const { Resource } = require('jsgui3-html');
|
|
410
|
+
|
|
411
|
+
class UserResource extends Resource {
|
|
412
|
+
constructor(spec) {
|
|
413
|
+
super(spec);
|
|
414
|
+
this.users = [
|
|
415
|
+
{ id: 1, name: 'Alice', email: 'alice@example.com' },
|
|
416
|
+
{ id: 2, name: 'Bob', email: 'bob@example.com' }
|
|
417
|
+
];
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// GET /api/users
|
|
421
|
+
async get(pathParts) {
|
|
422
|
+
if (pathParts.length === 0) {
|
|
423
|
+
return this.users; // List all users
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const id = parseInt(pathParts[0]);
|
|
427
|
+
const user = this.users.find(u => u.id === id);
|
|
428
|
+
if (!user) throw new Error('User not found');
|
|
429
|
+
return user; // Get specific user
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// POST /api/users
|
|
433
|
+
async post(data) {
|
|
434
|
+
const newUser = {
|
|
435
|
+
id: this.users.length + 1,
|
|
436
|
+
name: data.name,
|
|
437
|
+
email: data.email
|
|
438
|
+
};
|
|
439
|
+
this.users.push(newUser);
|
|
440
|
+
return newUser;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// DELETE /api/users/1
|
|
444
|
+
async delete(pathParts) {
|
|
445
|
+
const id = parseInt(pathParts[0]);
|
|
446
|
+
const index = this.users.findIndex(u => u.id === id);
|
|
447
|
+
if (index === -1) throw new Error('User not found');
|
|
448
|
+
|
|
449
|
+
const deletedUser = this.users.splice(index, 1)[0];
|
|
450
|
+
return { deleted: true, user: deletedUser };
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// In server setup
|
|
455
|
+
const userResource = new UserResource();
|
|
456
|
+
const resourcePublisher = new (require('./publishers/http-resource-publisher'))({
|
|
457
|
+
resource: userResource,
|
|
458
|
+
name: 'users'
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
// Routes: /api/users/* handled by resourcePublisher
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
## Integration with Frontend
|
|
465
|
+
|
|
466
|
+
### Fetch API Integration
|
|
467
|
+
|
|
468
|
+
```javascript
|
|
469
|
+
// Frontend JavaScript
|
|
470
|
+
class ApiClient {
|
|
471
|
+
constructor(baseUrl = '') {
|
|
472
|
+
this.baseUrl = baseUrl;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
async request(endpoint, options = {}) {
|
|
476
|
+
const url = `${this.baseUrl}/api/${endpoint}`;
|
|
477
|
+
const config = {
|
|
478
|
+
headers: {
|
|
479
|
+
'Content-Type': 'application/json',
|
|
480
|
+
...options.headers
|
|
481
|
+
},
|
|
482
|
+
...options
|
|
483
|
+
};
|
|
484
|
+
|
|
485
|
+
if (options.body && typeof options.body === 'object') {
|
|
486
|
+
config.body = JSON.stringify(options.body);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const response = await fetch(url, config);
|
|
490
|
+
|
|
491
|
+
if (!response.ok) {
|
|
492
|
+
const error = await response.json();
|
|
493
|
+
throw new Error(error.error || 'API request failed');
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
return response.json();
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// Convenience methods
|
|
500
|
+
get(endpoint, params) {
|
|
501
|
+
const query = params ? '?' + new URLSearchParams(params) : '';
|
|
502
|
+
return this.request(endpoint + query);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
post(endpoint, data) {
|
|
506
|
+
return this.request(endpoint, {
|
|
507
|
+
method: 'POST',
|
|
508
|
+
body: data
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
put(endpoint, data) {
|
|
513
|
+
return this.request(endpoint, {
|
|
514
|
+
method: 'PUT',
|
|
515
|
+
body: data
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
delete(endpoint) {
|
|
520
|
+
return this.request(endpoint, {
|
|
521
|
+
method: 'DELETE'
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// Usage
|
|
527
|
+
const api = new ApiClient();
|
|
528
|
+
|
|
529
|
+
// GET requests
|
|
530
|
+
const users = await api.get('users');
|
|
531
|
+
const user = await api.get('user', { id: 1 });
|
|
532
|
+
|
|
533
|
+
// POST requests
|
|
534
|
+
const newUser = await api.post('create-user', {
|
|
535
|
+
name: 'Charlie',
|
|
536
|
+
email: 'charlie@example.com'
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
// Error handling
|
|
540
|
+
try {
|
|
541
|
+
const result = await api.post('validate', { email: 'invalid' });
|
|
542
|
+
} catch (error) {
|
|
543
|
+
console.error('Validation failed:', error.message);
|
|
544
|
+
}
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
## Performance Considerations
|
|
548
|
+
|
|
549
|
+
### Caching Strategies
|
|
550
|
+
|
|
551
|
+
```javascript
|
|
552
|
+
api: {
|
|
553
|
+
// Cacheable data
|
|
554
|
+
'static-config': () => ({
|
|
555
|
+
version: '1.0.0',
|
|
556
|
+
features: ['auth', 'api', 'files'],
|
|
557
|
+
limits: { maxUploadSize: '10MB' }
|
|
558
|
+
}),
|
|
559
|
+
|
|
560
|
+
// Dynamic data with cache headers
|
|
561
|
+
'stats': () => {
|
|
562
|
+
// Set cache headers in response
|
|
563
|
+
const stats = {
|
|
564
|
+
users: getUserCount(),
|
|
565
|
+
uptime: process.uptime(),
|
|
566
|
+
timestamp: new Date()
|
|
567
|
+
};
|
|
568
|
+
|
|
569
|
+
// Note: Cache headers would be set by middleware
|
|
570
|
+
return stats;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
### Rate Limiting
|
|
576
|
+
|
|
577
|
+
```javascript
|
|
578
|
+
// Basic rate limiting (implement proper middleware for production)
|
|
579
|
+
let requestCounts = new Map();
|
|
580
|
+
|
|
581
|
+
api: {
|
|
582
|
+
'rate-limited-endpoint': (data, req) => {
|
|
583
|
+
const clientIP = req.connection.remoteAddress;
|
|
584
|
+
const now = Date.now();
|
|
585
|
+
const windowStart = now - 60000; // 1 minute window
|
|
586
|
+
|
|
587
|
+
// Clean old entries
|
|
588
|
+
for (const [ip, timestamps] of requestCounts) {
|
|
589
|
+
requestCounts.set(ip, timestamps.filter(t => t > windowStart));
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// Check rate limit
|
|
593
|
+
const timestamps = requestCounts.get(clientIP) || [];
|
|
594
|
+
if (timestamps.length >= 10) { // 10 requests per minute
|
|
595
|
+
throw new Error('Rate limit exceeded');
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// Record request
|
|
599
|
+
timestamps.push(now);
|
|
600
|
+
requestCounts.set(clientIP, timestamps);
|
|
601
|
+
|
|
602
|
+
return { success: true, data: 'Processed' };
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
```
|
|
606
|
+
|
|
607
|
+
## Testing Function Publishers
|
|
608
|
+
|
|
609
|
+
### Unit Testing
|
|
610
|
+
|
|
611
|
+
```javascript
|
|
612
|
+
const assert = require('assert');
|
|
613
|
+
|
|
614
|
+
// Test function directly
|
|
615
|
+
const helloFunction = (name) => `Hello ${name || 'World'}!`;
|
|
616
|
+
|
|
617
|
+
assert.equal(helloFunction(), 'Hello World!');
|
|
618
|
+
assert.equal(helloFunction('Alice'), 'Hello Alice!');
|
|
619
|
+
|
|
620
|
+
// Test with simulated HTTP
|
|
621
|
+
const Function_Publisher = require('./publishers/http-function-publisher');
|
|
622
|
+
|
|
623
|
+
function testFunctionPublisher() {
|
|
624
|
+
const publisher = new Function_Publisher(helloFunction);
|
|
625
|
+
|
|
626
|
+
// Mock request/response
|
|
627
|
+
const mockReq = {
|
|
628
|
+
headers: { 'content-type': 'application/json' },
|
|
629
|
+
on: function(event, callback) {
|
|
630
|
+
if (event === 'data') callback(Buffer.from('{"name":"Test"}'));
|
|
631
|
+
if (event === 'end') callback();
|
|
632
|
+
}
|
|
633
|
+
};
|
|
634
|
+
|
|
635
|
+
let responseData = '';
|
|
636
|
+
const mockRes = {
|
|
637
|
+
writeHead: () => {},
|
|
638
|
+
end: (data) => { responseData = data; }
|
|
639
|
+
};
|
|
640
|
+
|
|
641
|
+
publisher.handle_http(mockReq, mockRes);
|
|
642
|
+
|
|
643
|
+
// Verify response
|
|
644
|
+
const response = JSON.parse(responseData);
|
|
645
|
+
assert.equal(response.result, 'Hello Test!');
|
|
646
|
+
}
|
|
647
|
+
```
|
|
648
|
+
|
|
649
|
+
### Integration Testing
|
|
650
|
+
|
|
651
|
+
```javascript
|
|
652
|
+
const http = require('http');
|
|
653
|
+
|
|
654
|
+
function testAPIEndpoint() {
|
|
655
|
+
return new Promise((resolve, reject) => {
|
|
656
|
+
const req = http.request({
|
|
657
|
+
hostname: 'localhost',
|
|
658
|
+
port: 3000,
|
|
659
|
+
path: '/api/hello',
|
|
660
|
+
method: 'POST',
|
|
661
|
+
headers: {
|
|
662
|
+
'Content-Type': 'application/json'
|
|
663
|
+
}
|
|
664
|
+
}, (res) => {
|
|
665
|
+
let data = '';
|
|
666
|
+
res.on('data', chunk => data += chunk);
|
|
667
|
+
res.on('end', () => {
|
|
668
|
+
try {
|
|
669
|
+
const response = JSON.parse(data);
|
|
670
|
+
resolve(response);
|
|
671
|
+
} catch (err) {
|
|
672
|
+
reject(err);
|
|
673
|
+
}
|
|
674
|
+
});
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
req.write(JSON.stringify({ name: 'Integration Test' }));
|
|
678
|
+
req.end();
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// Usage
|
|
683
|
+
testAPIEndpoint().then(result => {
|
|
684
|
+
console.log('Integration test passed:', result);
|
|
685
|
+
}).catch(err => {
|
|
686
|
+
console.error('Integration test failed:', err);
|
|
687
|
+
});
|
|
688
|
+
```
|
|
689
|
+
|
|
690
|
+
## Best Practices
|
|
691
|
+
|
|
692
|
+
### API Design
|
|
693
|
+
|
|
694
|
+
1. **Consistent Naming**: Use lowercase, hyphen-separated endpoint names
|
|
695
|
+
2. **RESTful Patterns**: Use appropriate HTTP methods (GET, POST, PUT, DELETE)
|
|
696
|
+
3. **Versioning**: Consider API versioning for breaking changes
|
|
697
|
+
4. **Documentation**: Document all endpoints with expected inputs/outputs
|
|
698
|
+
5. **Error Handling**: Provide meaningful error messages
|
|
699
|
+
|
|
700
|
+
### Security
|
|
701
|
+
|
|
702
|
+
1. **Input Validation**: Always validate and sanitize inputs
|
|
703
|
+
2. **Authentication**: Implement proper auth for sensitive endpoints
|
|
704
|
+
3. **Rate Limiting**: Prevent abuse with rate limiting
|
|
705
|
+
4. **CORS**: Configure appropriate CORS policies
|
|
706
|
+
5. **HTTPS**: Use HTTPS in production
|
|
707
|
+
|
|
708
|
+
### Performance
|
|
709
|
+
|
|
710
|
+
1. **Caching**: Implement appropriate caching strategies
|
|
711
|
+
2. **Compression**: Enable gzip compression for responses
|
|
712
|
+
3. **Async Operations**: Use async/await for I/O operations
|
|
713
|
+
4. **Connection Pooling**: Use connection pools for databases
|
|
714
|
+
5. **Monitoring**: Monitor response times and error rates
|
|
715
|
+
|
|
716
|
+
## Troubleshooting
|
|
717
|
+
|
|
718
|
+
### Common Issues
|
|
719
|
+
|
|
720
|
+
#### Content-Type Mismatch
|
|
721
|
+
```
|
|
722
|
+
Error: Unexpected token in JSON
|
|
723
|
+
```
|
|
724
|
+
**Cause:** Sending wrong content-type header
|
|
725
|
+
**Fix:** Ensure `Content-Type: application/json` for JSON data
|
|
726
|
+
|
|
727
|
+
#### Function Not Found
|
|
728
|
+
```
|
|
729
|
+
Error: Route not found
|
|
730
|
+
```
|
|
731
|
+
**Cause:** API endpoint not properly registered
|
|
732
|
+
**Fix:** Check that function is in the `api` object and server restarted
|
|
733
|
+
|
|
734
|
+
#### Async Function Issues
|
|
735
|
+
```
|
|
736
|
+
Error: Function did not return a value
|
|
737
|
+
```
|
|
738
|
+
**Cause:** Async function not properly awaited
|
|
739
|
+
**Fix:** Ensure async functions use `async/await` pattern
|
|
740
|
+
|
|
741
|
+
#### CORS Issues
|
|
742
|
+
```
|
|
743
|
+
Error: CORS policy blocked
|
|
744
|
+
```
|
|
745
|
+
**Cause:** Missing CORS headers
|
|
746
|
+
**Fix:** Add CORS middleware or headers
|
|
747
|
+
|
|
748
|
+
## Migration from Other Frameworks
|
|
749
|
+
|
|
750
|
+
### From Express.js
|
|
751
|
+
|
|
752
|
+
```javascript
|
|
753
|
+
// Express.js
|
|
754
|
+
app.get('/api/users', (req, res) => {
|
|
755
|
+
res.json(getUsers());
|
|
756
|
+
});
|
|
757
|
+
|
|
758
|
+
// JSGUI3 Server
|
|
759
|
+
Server.serve({
|
|
760
|
+
api: {
|
|
761
|
+
'users': () => getUsers()
|
|
762
|
+
}
|
|
763
|
+
});
|
|
764
|
+
```
|
|
765
|
+
|
|
766
|
+
### From RESTify
|
|
767
|
+
|
|
768
|
+
```javascript
|
|
769
|
+
// RESTify
|
|
770
|
+
server.get('/api/users/:id', (req, res) => {
|
|
771
|
+
res.send(getUser(req.params.id));
|
|
772
|
+
});
|
|
773
|
+
|
|
774
|
+
// JSGUI3 Server
|
|
775
|
+
Server.serve({
|
|
776
|
+
api: {
|
|
777
|
+
'user': ({ id }) => getUser(id)
|
|
778
|
+
}
|
|
779
|
+
});
|
|
780
|
+
```
|
|
781
|
+
|
|
782
|
+
## Advanced Topics
|
|
783
|
+
|
|
784
|
+
### Custom Publishers
|
|
785
|
+
|
|
786
|
+
```javascript
|
|
787
|
+
const HTTP_Publisher = require('./publishers/http-publisher');
|
|
788
|
+
|
|
789
|
+
class CustomPublisher extends HTTP_Publisher {
|
|
790
|
+
constructor(spec) {
|
|
791
|
+
super(spec);
|
|
792
|
+
this.customLogic = spec.customLogic;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
handle_http(req, res) {
|
|
796
|
+
// Custom request handling logic
|
|
797
|
+
if (this.customLogic(req)) {
|
|
798
|
+
// Handle custom case
|
|
799
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
800
|
+
res.end(JSON.stringify({ custom: true }));
|
|
801
|
+
} else {
|
|
802
|
+
// Fall back to default
|
|
803
|
+
super.handle_http(req, res);
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
```
|
|
808
|
+
|
|
809
|
+
### Middleware Integration
|
|
810
|
+
|
|
811
|
+
```javascript
|
|
812
|
+
// Custom middleware
|
|
813
|
+
function loggingMiddleware(req, res, next) {
|
|
814
|
+
console.log(`${req.method} ${req.url}`);
|
|
815
|
+
next();
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
function authMiddleware(req, res, next) {
|
|
819
|
+
const token = req.headers.authorization;
|
|
820
|
+
if (!token) {
|
|
821
|
+
res.writeHead(401);
|
|
822
|
+
res.end('Unauthorized');
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
// Validate token...
|
|
826
|
+
next();
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
Server.serve({
|
|
830
|
+
ctrl: MyControl,
|
|
831
|
+
middleware: [loggingMiddleware, authMiddleware],
|
|
832
|
+
api: { /* ... */ }
|
|
833
|
+
});
|
|
834
|
+
```
|
|
835
|
+
|
|
836
|
+
## Conclusion
|
|
837
|
+
|
|
838
|
+
Function publishers provide a powerful, easy-to-use way to create JSON APIs with JSGUI3 Server. The automatic content-type handling, promise support, and flexible input/output processing make it simple to build robust backend services.
|
|
839
|
+
|
|
840
|
+
Key benefits:
|
|
841
|
+
- **Automatic JSON handling** - No manual serialization
|
|
842
|
+
- **Promise/async support** - Modern JavaScript patterns
|
|
843
|
+
- **Flexible inputs** - JSON, text, or no body
|
|
844
|
+
- **Error propagation** - Automatic error handling
|
|
845
|
+
- **RESTful routing** - Automatic `/api/` prefixing
|
|
846
|
+
|
|
847
|
+
For complex applications, consider combining function publishers with resource publishers for full CRUD operations and more sophisticated API patterns.
|