liferay-headless-sdk 1.0.0
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/README.md +432 -0
- package/package.json +73 -0
- package/src/api-generator.js +192 -0
- package/src/auth.js +84 -0
- package/src/cli.js +253 -0
- package/src/client.js +271 -0
- package/src/errors.js +75 -0
- package/src/http.js +216 -0
- package/src/index.d.ts +212 -0
- package/src/index.js +27 -0
- package/src/pagination.js +84 -0
- package/src/swagger-loader.js +185 -0
- package/src/utils.js +125 -0
package/README.md
ADDED
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
# Liferay Headless SDK
|
|
2
|
+
|
|
3
|
+
A production-ready JavaScript SDK that **dynamically generates** API client methods from Liferay's Swagger/OpenAPI specifications at runtime. Zero manual mapping required — point it at a Liferay instance and call APIs immediately.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Table of Contents
|
|
8
|
+
|
|
9
|
+
- [Installation](#installation)
|
|
10
|
+
- [Quick Start](#quick-start)
|
|
11
|
+
- [Configuration](#configuration)
|
|
12
|
+
- [Authentication](#authentication)
|
|
13
|
+
- [Dynamic Method Usage](#dynamic-method-usage)
|
|
14
|
+
- [Pagination](#pagination)
|
|
15
|
+
- [Interceptors](#interceptors)
|
|
16
|
+
- [Error Handling](#error-handling)
|
|
17
|
+
- [CLI Tool](#cli-tool)
|
|
18
|
+
- [TypeScript](#typescript)
|
|
19
|
+
- [Advanced Usage](#advanced-usage)
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Installation
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm install liferay-headless-sdk
|
|
27
|
+
# or
|
|
28
|
+
yarn add liferay-headless-sdk
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Node.js 18+ is required (uses native `fetch` and ES modules).
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Quick Start
|
|
36
|
+
|
|
37
|
+
```js
|
|
38
|
+
import { LiferayHeadlessClient } from 'liferay-headless-sdk';
|
|
39
|
+
|
|
40
|
+
const client = new LiferayHeadlessClient({
|
|
41
|
+
baseUrl: 'https://your-liferay.com',
|
|
42
|
+
swaggerUrls: [
|
|
43
|
+
'/o/headless-delivery/v1.0/openapi.json',
|
|
44
|
+
'/o/headless-admin-user/v1.0/openapi.json',
|
|
45
|
+
],
|
|
46
|
+
username: 'test@liferay.com',
|
|
47
|
+
password: 'test',
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Initialize (loads and parses OpenAPI schemas)
|
|
51
|
+
await client.init();
|
|
52
|
+
|
|
53
|
+
// Call generated methods
|
|
54
|
+
const { data: sites } = await client.headlessDelivery.site.getSites();
|
|
55
|
+
console.log(sites.items);
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Configuration
|
|
61
|
+
|
|
62
|
+
```js
|
|
63
|
+
const client = new LiferayHeadlessClient({
|
|
64
|
+
// Required
|
|
65
|
+
baseUrl: 'https://your-liferay.com',
|
|
66
|
+
|
|
67
|
+
// OpenAPI endpoints to load (relative or absolute URLs)
|
|
68
|
+
swaggerUrls: [
|
|
69
|
+
'/o/headless-delivery/v1.0/openapi.json',
|
|
70
|
+
'/o/headless-admin-user/v1.0/openapi.json',
|
|
71
|
+
'/o/headless-admin-content/v1.0/openapi.json',
|
|
72
|
+
'/o/object-admin/v1.0/openapi.json',
|
|
73
|
+
],
|
|
74
|
+
|
|
75
|
+
// Auth — use one of the options below
|
|
76
|
+
username: 'test@liferay.com',
|
|
77
|
+
password: 'test',
|
|
78
|
+
// oauthToken: 'your-bearer-token',
|
|
79
|
+
|
|
80
|
+
// HTTP behavior
|
|
81
|
+
timeout: 30000, // ms — default 30s
|
|
82
|
+
retries: 2, // automatic retries on 5xx / network errors
|
|
83
|
+
|
|
84
|
+
// Lazy init via Proxy (default true)
|
|
85
|
+
// Set false if you want to call init() manually and avoid Proxy overhead
|
|
86
|
+
autoGenerate: true,
|
|
87
|
+
});
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Available Swagger Endpoints
|
|
91
|
+
|
|
92
|
+
| API | Path |
|
|
93
|
+
|-----|------|
|
|
94
|
+
| Headless Delivery | `/o/headless-delivery/v1.0/openapi.json` |
|
|
95
|
+
| Headless Admin User | `/o/headless-admin-user/v1.0/openapi.json` |
|
|
96
|
+
| Headless Admin Content | `/o/headless-admin-content/v1.0/openapi.json` |
|
|
97
|
+
| Object Admin | `/o/object-admin/v1.0/openapi.json` |
|
|
98
|
+
| All APIs (discovery) | `/o/api` |
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## Authentication
|
|
103
|
+
|
|
104
|
+
### Basic Auth (username + password)
|
|
105
|
+
|
|
106
|
+
```js
|
|
107
|
+
const client = new LiferayHeadlessClient({
|
|
108
|
+
baseUrl: '...',
|
|
109
|
+
username: 'test@liferay.com',
|
|
110
|
+
password: 'test',
|
|
111
|
+
});
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### OAuth2 Bearer Token
|
|
115
|
+
|
|
116
|
+
```js
|
|
117
|
+
const client = new LiferayHeadlessClient({
|
|
118
|
+
baseUrl: '...',
|
|
119
|
+
oauthToken: 'eyJhbGciOiJSUzI1NiJ9...',
|
|
120
|
+
});
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Switching Auth Dynamically
|
|
124
|
+
|
|
125
|
+
```js
|
|
126
|
+
// Switch to OAuth after initial Basic auth setup
|
|
127
|
+
client.setOAuthToken('new-access-token');
|
|
128
|
+
|
|
129
|
+
// Or switch back to Basic
|
|
130
|
+
client.setBasicAuth('admin@liferay.com', 'admin');
|
|
131
|
+
|
|
132
|
+
// Clear auth
|
|
133
|
+
client.clearAuth();
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## Dynamic Method Usage
|
|
139
|
+
|
|
140
|
+
After `init()`, service namespaces are accessible as properties of the client. The namespace name is derived from the OpenAPI tag (converted to camelCase).
|
|
141
|
+
|
|
142
|
+
```js
|
|
143
|
+
await client.init();
|
|
144
|
+
|
|
145
|
+
// GET /v1.0/sites/{siteId}
|
|
146
|
+
const { data } = await client.headlessDelivery.site.getSite({siteId: 12345});
|
|
147
|
+
|
|
148
|
+
// GET /v1.0/structured-contents/{structuredContentId}
|
|
149
|
+
const { data: contents } = await client.headlessDelivery.structuredContent.getStructuredContent({
|
|
150
|
+
structuredContentId: 999,
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// POST /v1.0/sites/{siteId}/structured-contents
|
|
154
|
+
await client.headlessDelivery.structuredContent.postSiteStructuredContent({
|
|
155
|
+
siteId: 12345,
|
|
156
|
+
body: {
|
|
157
|
+
title: 'My Article',
|
|
158
|
+
contentStructureId: 67890,
|
|
159
|
+
contentFields: [],
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// PUT /v1.0/structured-contents/{structuredContentId}
|
|
164
|
+
await client.headlessDelivery.structuredContent.putStructuredContent({
|
|
165
|
+
structuredContentId: 999,
|
|
166
|
+
body: { title: 'Updated Title' },
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// DELETE /v1.0/structured-contents/{structuredContentId}
|
|
170
|
+
await client.headlessDelivery.structuredContent.deleteStructuredContent({
|
|
171
|
+
structuredContentId: 999,
|
|
172
|
+
});
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Parameter Mapping
|
|
176
|
+
|
|
177
|
+
The SDK auto-maps parameters from the single `params` object:
|
|
178
|
+
|
|
179
|
+
| Param type | How to pass |
|
|
180
|
+
|------------|-------------|
|
|
181
|
+
| Path variable `{siteId}` | `{ siteId: 12345 }` |
|
|
182
|
+
| Query string `?page=1` | `{ page: 1 }` |
|
|
183
|
+
| Request body | `{ body: { ... } }` |
|
|
184
|
+
| Extra headers | `{ headers: { 'X-Custom': 'value' } }` |
|
|
185
|
+
|
|
186
|
+
Any extra keys not defined in the OpenAPI spec are passed as query parameters.
|
|
187
|
+
|
|
188
|
+
### Discovering Available Methods
|
|
189
|
+
|
|
190
|
+
```js
|
|
191
|
+
// List all service namespaces
|
|
192
|
+
console.log(client.getServiceNames());
|
|
193
|
+
// ['headlessDelivery', 'headlessAdminUser', ...]
|
|
194
|
+
|
|
195
|
+
// List methods in a namespace
|
|
196
|
+
console.log(client.getMethodNames('headlessDelivery'));
|
|
197
|
+
// ['getSites', 'getStructuredContents', 'createStructuredContent', ...]
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
## Pagination
|
|
203
|
+
|
|
204
|
+
Liferay returns paginated responses with `{ items, page, pageSize, totalCount, lastPage }`.
|
|
205
|
+
|
|
206
|
+
### Iterate all pages lazily
|
|
207
|
+
|
|
208
|
+
```js
|
|
209
|
+
import { iteratePages } from 'liferay-headless-sdk';
|
|
210
|
+
|
|
211
|
+
await client.init();
|
|
212
|
+
|
|
213
|
+
for await (const site of iteratePages(client.headlessDelivery.getSites, { pageSize: 50 })) {
|
|
214
|
+
console.log(site.name);
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### Collect all items into an array
|
|
219
|
+
|
|
220
|
+
```js
|
|
221
|
+
import { collectAllPages } from 'liferay-headless-sdk';
|
|
222
|
+
|
|
223
|
+
const allUsers = await collectAllPages(client.headlessAdminUser.getUsers, { pageSize: 100 });
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
### Fetch a specific page
|
|
227
|
+
|
|
228
|
+
```js
|
|
229
|
+
import { getPage } from 'liferay-headless-sdk';
|
|
230
|
+
|
|
231
|
+
const page2 = await getPage(client.headlessDelivery.getSites, 2, 20);
|
|
232
|
+
console.log(page2.items, page2.totalCount, page2.lastPage);
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
---
|
|
236
|
+
|
|
237
|
+
## Interceptors
|
|
238
|
+
|
|
239
|
+
Interceptors run on every request/response before it is returned to your code.
|
|
240
|
+
|
|
241
|
+
### Logging
|
|
242
|
+
|
|
243
|
+
```js
|
|
244
|
+
client.addRequestInterceptor((config) => {
|
|
245
|
+
console.log(`→ ${config.method} ${config.path}`);
|
|
246
|
+
return config;
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
client.addResponseInterceptor((response) => {
|
|
250
|
+
console.log(`← ${response.status}`);
|
|
251
|
+
return response;
|
|
252
|
+
});
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
### Adding headers
|
|
256
|
+
|
|
257
|
+
```js
|
|
258
|
+
client.addRequestInterceptor((config) => ({
|
|
259
|
+
...config,
|
|
260
|
+
headers: { ...config.headers, 'X-Correlation-ID': crypto.randomUUID() },
|
|
261
|
+
}));
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
### Token refresh
|
|
265
|
+
|
|
266
|
+
```js
|
|
267
|
+
client.addResponseInterceptor(async (response) => {
|
|
268
|
+
if (response.status === 401) {
|
|
269
|
+
const newToken = await refreshAccessToken();
|
|
270
|
+
client.setOAuthToken(newToken);
|
|
271
|
+
}
|
|
272
|
+
return response;
|
|
273
|
+
});
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
## Error Handling
|
|
279
|
+
|
|
280
|
+
```js
|
|
281
|
+
import { LiferayAPIError, LiferayNetworkError, LiferayTimeoutError } from 'liferay-headless-sdk';
|
|
282
|
+
|
|
283
|
+
try {
|
|
284
|
+
const { data } = await client.headlessDelivery.getSites();
|
|
285
|
+
} catch (err) {
|
|
286
|
+
if (err instanceof LiferayAPIError) {
|
|
287
|
+
// HTTP error response from Liferay
|
|
288
|
+
console.error(`API Error ${err.statusCode}: ${err.message}`);
|
|
289
|
+
console.error('Endpoint:', err.endpoint);
|
|
290
|
+
console.error('Response body:', err.responseBody);
|
|
291
|
+
} else if (err instanceof LiferayTimeoutError) {
|
|
292
|
+
console.error(`Timed out after ${err.timeoutMs}ms on ${err.endpoint}`);
|
|
293
|
+
} else if (err instanceof LiferayNetworkError) {
|
|
294
|
+
console.error(`Network failure on ${err.endpoint}:`, err.message);
|
|
295
|
+
} else {
|
|
296
|
+
throw err;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
### Error properties
|
|
302
|
+
|
|
303
|
+
| Class | Properties |
|
|
304
|
+
|-------|-----------|
|
|
305
|
+
| `LiferayAPIError` | `statusCode`, `message`, `endpoint`, `requestPayload`, `responseBody` |
|
|
306
|
+
| `LiferayNetworkError` | `message`, `endpoint`, `cause` |
|
|
307
|
+
| `LiferayTimeoutError` | `message`, `endpoint`, `timeoutMs` |
|
|
308
|
+
|
|
309
|
+
4xx errors are **not retried**. 5xx and network errors are retried up to `retries` times with exponential backoff.
|
|
310
|
+
|
|
311
|
+
---
|
|
312
|
+
|
|
313
|
+
## CLI Tool
|
|
314
|
+
|
|
315
|
+
Generate a static SDK by pre-fetching Swagger definitions:
|
|
316
|
+
|
|
317
|
+
```bash
|
|
318
|
+
npx liferay-sdk-cli generate \
|
|
319
|
+
--baseUrl https://your-liferay.com \
|
|
320
|
+
--output ./generated-sdk \
|
|
321
|
+
--username test@liferay.com \
|
|
322
|
+
--password test
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
### Options
|
|
326
|
+
|
|
327
|
+
| Flag | Description | Default |
|
|
328
|
+
|------|-------------|---------|
|
|
329
|
+
| `--baseUrl` | Liferay instance URL | *required* |
|
|
330
|
+
| `--output` | Output directory | `./generated-sdk` |
|
|
331
|
+
| `--username` | Basic Auth username | |
|
|
332
|
+
| `--password` | Basic Auth password | |
|
|
333
|
+
| `--token` | OAuth2 bearer token | |
|
|
334
|
+
| `--swagger` | Comma-separated list of OpenAPI paths | all 4 default endpoints |
|
|
335
|
+
|
|
336
|
+
### Custom Swagger URLs
|
|
337
|
+
|
|
338
|
+
```bash
|
|
339
|
+
npx liferay-sdk-cli generate \
|
|
340
|
+
--baseUrl https://your-liferay.com \
|
|
341
|
+
--swagger /o/headless-delivery/v1.0/openapi.json,/o/my-custom-api/v1.0/openapi.json
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
---
|
|
345
|
+
|
|
346
|
+
## TypeScript
|
|
347
|
+
|
|
348
|
+
Full TypeScript declarations are included. The dynamic service namespaces are typed with an index signature:
|
|
349
|
+
|
|
350
|
+
```ts
|
|
351
|
+
import { LiferayHeadlessClient, LiferayAPIError } from 'liferay-headless-sdk';
|
|
352
|
+
|
|
353
|
+
const client = new LiferayHeadlessClient({
|
|
354
|
+
baseUrl: 'https://your-liferay.com',
|
|
355
|
+
username: 'test@liferay.com',
|
|
356
|
+
password: 'test',
|
|
357
|
+
swaggerUrls: ['/o/headless-delivery/v1.0/openapi.json'],
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
await client.init();
|
|
361
|
+
|
|
362
|
+
// Dynamically accessed namespaces return Record<string, ApiMethod>
|
|
363
|
+
const service = client['headlessDelivery'] as Record<string, Function>;
|
|
364
|
+
const result = await service['getSites']();
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
For strongly-typed wrappers, use the CLI to generate static service modules with explicit types.
|
|
368
|
+
|
|
369
|
+
---
|
|
370
|
+
|
|
371
|
+
## Advanced Usage
|
|
372
|
+
|
|
373
|
+
### Load schemas on demand
|
|
374
|
+
|
|
375
|
+
```js
|
|
376
|
+
const client = new LiferayHeadlessClient({
|
|
377
|
+
baseUrl: '...',
|
|
378
|
+
swaggerUrls: [], // Start with no schemas
|
|
379
|
+
autoGenerate: false,
|
|
380
|
+
username: 'test@liferay.com',
|
|
381
|
+
password: 'test',
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
// Load only what you need
|
|
385
|
+
await client.loadSchema('/o/headless-delivery/v1.0/openapi.json');
|
|
386
|
+
const { data } = await client.headlessDelivery.getSites();
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
### Raw HTTP access
|
|
390
|
+
|
|
391
|
+
```js
|
|
392
|
+
const { data } = await client.request({
|
|
393
|
+
method: 'GET',
|
|
394
|
+
path: '/o/headless-delivery/v1.0/sites',
|
|
395
|
+
query: { page: 1, pageSize: 5 },
|
|
396
|
+
});
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
### Cache management
|
|
400
|
+
|
|
401
|
+
```js
|
|
402
|
+
// Clear all cached schemas (forces re-fetch on next init)
|
|
403
|
+
client.clearSchemaCache();
|
|
404
|
+
await client.init();
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
---
|
|
408
|
+
|
|
409
|
+
## File Structure
|
|
410
|
+
|
|
411
|
+
```
|
|
412
|
+
liferay-sdk/
|
|
413
|
+
├── index.js — Public exports
|
|
414
|
+
├── index.d.ts — TypeScript declarations
|
|
415
|
+
├── client.js — LiferayHeadlessClient class
|
|
416
|
+
├── swagger-loader.js — OpenAPI schema fetching & caching
|
|
417
|
+
├── api-generator.js — Dynamic method generation from schemas
|
|
418
|
+
├── http.js — HTTP transport (fetch, retry, timeout)
|
|
419
|
+
├── auth.js — Basic Auth & OAuth2 manager
|
|
420
|
+
├── errors.js — LiferayAPIError, LiferayNetworkError, LiferayTimeoutError
|
|
421
|
+
├── pagination.js — iteratePages, collectAllPages, getPage
|
|
422
|
+
├── utils.js — URL building, camelCase conversion, etc.
|
|
423
|
+
├── cli.js — liferay-sdk-cli tool
|
|
424
|
+
├── package.json
|
|
425
|
+
└── README.md
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
---
|
|
429
|
+
|
|
430
|
+
## License
|
|
431
|
+
|
|
432
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "liferay-headless-sdk",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A production-ready JavaScript SDK dynamically generated from Liferay Headless API Swagger/OpenAPI specifications.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/index.js",
|
|
7
|
+
"module": "./src/index.js",
|
|
8
|
+
"types": "./src/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": "./src/index.js",
|
|
12
|
+
"types": "./src/index.d.ts"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"bin": {
|
|
16
|
+
"liferay-sdk-cli": "./src/cli.js"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"index.js",
|
|
20
|
+
"index.d.ts",
|
|
21
|
+
"client.js",
|
|
22
|
+
"swagger-loader.js",
|
|
23
|
+
"api-generator.js",
|
|
24
|
+
"http.js",
|
|
25
|
+
"auth.js",
|
|
26
|
+
"errors.js",
|
|
27
|
+
"pagination.js",
|
|
28
|
+
"utils.js",
|
|
29
|
+
"cli.js",
|
|
30
|
+
"README.md"
|
|
31
|
+
],
|
|
32
|
+
"scripts": {
|
|
33
|
+
"test": "node --experimental-vm-modules node_modules/.bin/jest",
|
|
34
|
+
"lint": "eslint .",
|
|
35
|
+
"build": "echo 'No build step required for ES modules'"
|
|
36
|
+
},
|
|
37
|
+
"keywords": [
|
|
38
|
+
"liferay",
|
|
39
|
+
"headless",
|
|
40
|
+
"api",
|
|
41
|
+
"sdk",
|
|
42
|
+
"openapi",
|
|
43
|
+
"swagger",
|
|
44
|
+
"rest",
|
|
45
|
+
"client"
|
|
46
|
+
],
|
|
47
|
+
"author": "",
|
|
48
|
+
"license": "MIT",
|
|
49
|
+
"engines": {
|
|
50
|
+
"node": ">=18.0.0"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"eslint": "^8.57.0",
|
|
54
|
+
"jest": "^29.7.0"
|
|
55
|
+
},
|
|
56
|
+
"eslintConfig": {
|
|
57
|
+
"env": {
|
|
58
|
+
"browser": true,
|
|
59
|
+
"es2022": true,
|
|
60
|
+
"node": true
|
|
61
|
+
},
|
|
62
|
+
"parserOptions": {
|
|
63
|
+
"ecmaVersion": 2022,
|
|
64
|
+
"sourceType": "module"
|
|
65
|
+
},
|
|
66
|
+
"rules": {
|
|
67
|
+
"no-console": "warn",
|
|
68
|
+
"no-unused-vars": "warn",
|
|
69
|
+
"prefer-const": "error",
|
|
70
|
+
"no-var": "error"
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Generates dynamic API service modules from parsed OpenAPI schemas.
|
|
3
|
+
* Groups operations by tag, converts operationIds to method names, and wires up
|
|
4
|
+
* path/query/body parameters automatically.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { operationIdToMethodName, tagToPropertyName, interpolatePath, joinUrl } from './utils.js';
|
|
8
|
+
|
|
9
|
+
/** HTTP methods that typically carry a request body */
|
|
10
|
+
const BODY_METHODS = new Set(['post', 'put', 'patch']);
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Parses an OpenAPI schema and groups all operations by their first tag.
|
|
14
|
+
*
|
|
15
|
+
* @param {object} schema - Parsed OpenAPI/Swagger JSON
|
|
16
|
+
* @returns {Map<string, Array<OperationDef>>} Tag → list of operation definitions
|
|
17
|
+
*/
|
|
18
|
+
export function parseOperationsByTag(schema) {
|
|
19
|
+
const tagMap = new Map();
|
|
20
|
+
const paths = schema.paths || {};
|
|
21
|
+
const url = schema.servers[0].url;
|
|
22
|
+
|
|
23
|
+
for (const [pathTemplate, pathItem] of Object.entries(paths)) {
|
|
24
|
+
for (const method of ['get', 'post', 'put', 'patch', 'delete']) {
|
|
25
|
+
const operation = pathItem[method];
|
|
26
|
+
if (!operation) continue;
|
|
27
|
+
|
|
28
|
+
const tags = (operation.tags && operation.tags.length > 0)
|
|
29
|
+
? operation.tags
|
|
30
|
+
: ['default'];
|
|
31
|
+
|
|
32
|
+
const primaryTag = tags[0];
|
|
33
|
+
const tagKey = tagToPropertyName(primaryTag);
|
|
34
|
+
|
|
35
|
+
if (!tagMap.has(tagKey)) {
|
|
36
|
+
tagMap.set(tagKey, []);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
tagMap.get(tagKey).push({
|
|
40
|
+
url,
|
|
41
|
+
method,
|
|
42
|
+
pathTemplate,
|
|
43
|
+
operationId: operation.operationId,
|
|
44
|
+
parameters: operation.parameters || [],
|
|
45
|
+
requestBody: operation.requestBody || null,
|
|
46
|
+
summary: operation.summary || '',
|
|
47
|
+
description: operation.description || '',
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return tagMap;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* @typedef {object} OperationDef
|
|
57
|
+
* @property {string} method
|
|
58
|
+
* @property {string} pathTemplate
|
|
59
|
+
* @property {string} operationId
|
|
60
|
+
* @property {Array} parameters
|
|
61
|
+
* @property {object|null} requestBody
|
|
62
|
+
* @property {string} summary
|
|
63
|
+
* @property {string} description
|
|
64
|
+
*/
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Generates a JavaScript function for a single OpenAPI operation.
|
|
68
|
+
*
|
|
69
|
+
* The generated function accepts a single `params` object and automatically:
|
|
70
|
+
* - Substitutes path variables
|
|
71
|
+
* - Passes remaining params as query string
|
|
72
|
+
* - Accepts a `body` key for the request body
|
|
73
|
+
* - Accepts a `headers` key for extra headers
|
|
74
|
+
*
|
|
75
|
+
* @param {OperationDef} operation
|
|
76
|
+
* @param {import('./http.js').HttpClient} httpClient
|
|
77
|
+
* @returns {Function}
|
|
78
|
+
*/
|
|
79
|
+
export function buildOperationMethod(operation, httpClient) {
|
|
80
|
+
const { url, method, pathTemplate, operationId, parameters } = operation;
|
|
81
|
+
|
|
82
|
+
// Identify path parameter names
|
|
83
|
+
const pathParamNames = new Set(
|
|
84
|
+
parameters.filter((p) => p.in === 'path').map((p) => p.name)
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
// Identify query parameter names
|
|
88
|
+
const queryParamNames = new Set(
|
|
89
|
+
parameters.filter((p) => p.in === 'query').map((p) => p.name)
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Dynamically generated API method.
|
|
94
|
+
* @param {object} [params={}]
|
|
95
|
+
* @param {*} [params.body] - Request body (for POST/PUT/PATCH)
|
|
96
|
+
* @param {Record<string, string>} [params.headers] - Additional headers
|
|
97
|
+
* @returns {Promise<{ status: number, data: * }>}
|
|
98
|
+
*/
|
|
99
|
+
const generatedMethod = async function (params = {}) {
|
|
100
|
+
const { body, headers, ...rest } = params;
|
|
101
|
+
|
|
102
|
+
// Separate path params from query params
|
|
103
|
+
const pathParams = {};
|
|
104
|
+
const queryParams = {};
|
|
105
|
+
const extraParams = {};
|
|
106
|
+
|
|
107
|
+
for (const [key, value] of Object.entries(rest)) {
|
|
108
|
+
if (pathParamNames.has(key)) {
|
|
109
|
+
pathParams[key] = value;
|
|
110
|
+
} else if (queryParamNames.has(key)) {
|
|
111
|
+
queryParams[key] = value;
|
|
112
|
+
} else {
|
|
113
|
+
// Unknown params fall through as query params
|
|
114
|
+
extraParams[key] = value;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const { path } = interpolatePath(joinUrl(url, pathTemplate), pathParams);
|
|
119
|
+
const mergedQuery = { ...queryParams, ...extraParams };
|
|
120
|
+
|
|
121
|
+
// For methods with bodies, use body param; otherwise ignore
|
|
122
|
+
const requestBody = BODY_METHODS.has(method) ? body : undefined;
|
|
123
|
+
|
|
124
|
+
return httpClient.request({
|
|
125
|
+
method: method.toUpperCase(),
|
|
126
|
+
path,
|
|
127
|
+
query: mergedQuery,
|
|
128
|
+
body: requestBody,
|
|
129
|
+
headers,
|
|
130
|
+
});
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
// Attach metadata to the function for introspection
|
|
134
|
+
Object.defineProperty(generatedMethod, 'name', {
|
|
135
|
+
value: operationIdToMethodName(operationId) || `${method}${pathTemplate}`,
|
|
136
|
+
writable: false,
|
|
137
|
+
});
|
|
138
|
+
generatedMethod._operationId = operationId;
|
|
139
|
+
generatedMethod._method = method;
|
|
140
|
+
generatedMethod._path = pathTemplate;
|
|
141
|
+
generatedMethod._summary = operation.summary;
|
|
142
|
+
|
|
143
|
+
return generatedMethod;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Builds a service module object (a plain object of named methods) for a
|
|
148
|
+
* single OpenAPI tag group.
|
|
149
|
+
*
|
|
150
|
+
* @param {Array<OperationDef>} operations
|
|
151
|
+
* @param {import('./http.js').HttpClient} httpClient
|
|
152
|
+
* @returns {Record<string, Function>}
|
|
153
|
+
*/
|
|
154
|
+
export function buildServiceModule(operations, httpClient) {
|
|
155
|
+
const module = {};
|
|
156
|
+
|
|
157
|
+
for (const operation of operations) {
|
|
158
|
+
const methodName = operationIdToMethodName(operation.operationId);
|
|
159
|
+
if (!methodName) {
|
|
160
|
+
console.warn(`[LiferaySDK] Skipping operation with no operationId at ${operation.method.toUpperCase()} ${operation.pathTemplate}`);
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (module[methodName]) {
|
|
164
|
+
// Deduplicate: append HTTP method suffix to avoid collision
|
|
165
|
+
const uniqueName = `${methodName}_${operation.method}`;
|
|
166
|
+
module[uniqueName] = buildOperationMethod(operation, httpClient);
|
|
167
|
+
} else {
|
|
168
|
+
module[methodName] = buildOperationMethod(operation, httpClient);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return module;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Generates all service modules from a parsed OpenAPI schema.
|
|
177
|
+
*
|
|
178
|
+
* @param {object} schema - Parsed OpenAPI JSON
|
|
179
|
+
* @param {import('./http.js').HttpClient} httpClient
|
|
180
|
+
* @returns {Record<string, Record<string, Function>>} Tag → service module
|
|
181
|
+
*/
|
|
182
|
+
export function generateServicesFromSchema(schema, httpClient) {
|
|
183
|
+
const tagMap = parseOperationsByTag(schema);
|
|
184
|
+
const service = tagToPropertyName(schema.info.title);
|
|
185
|
+
const services = {[service]:{}};
|
|
186
|
+
|
|
187
|
+
for (const [tagKey, operations] of tagMap.entries()) {
|
|
188
|
+
services[service][tagKey] = buildServiceModule(operations, httpClient);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return services;
|
|
192
|
+
}
|