@serialsubscriptions/platform-integration 0.0.79
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 +1 -0
- package/lib/SSIProject.d.ts +343 -0
- package/lib/SSIProject.js +429 -0
- package/lib/SSIProjectApi.d.ts +384 -0
- package/lib/SSIProjectApi.js +534 -0
- package/lib/SSISubscribedFeatureApi.d.ts +387 -0
- package/lib/SSISubscribedFeatureApi.js +511 -0
- package/lib/SSISubscribedLimitApi.d.ts +384 -0
- package/lib/SSISubscribedLimitApi.js +534 -0
- package/lib/SSISubscribedPlanApi.d.ts +384 -0
- package/lib/SSISubscribedPlanApi.js +537 -0
- package/lib/SubscribedPlanManager.d.ts +380 -0
- package/lib/SubscribedPlanManager.js +288 -0
- package/lib/UsageApi.d.ts +128 -0
- package/lib/UsageApi.js +224 -0
- package/lib/auth.server.d.ts +192 -0
- package/lib/auth.server.js +579 -0
- package/lib/cache/SSICache.d.ts +40 -0
- package/lib/cache/SSICache.js +134 -0
- package/lib/cache/backends/MemoryCacheBackend.d.ts +15 -0
- package/lib/cache/backends/MemoryCacheBackend.js +46 -0
- package/lib/cache/backends/RedisCacheBackend.d.ts +27 -0
- package/lib/cache/backends/RedisCacheBackend.js +95 -0
- package/lib/cache/constants.d.ts +7 -0
- package/lib/cache/constants.js +10 -0
- package/lib/cache/types.d.ts +27 -0
- package/lib/cache/types.js +2 -0
- package/lib/frontend/index.d.ts +1 -0
- package/lib/frontend/index.js +6 -0
- package/lib/frontend/session/SessionClient.d.ts +24 -0
- package/lib/frontend/session/SessionClient.js +145 -0
- package/lib/index.d.ts +15 -0
- package/lib/index.js +38 -0
- package/lib/lib/session/SessionClient.d.ts +11 -0
- package/lib/lib/session/SessionClient.js +47 -0
- package/lib/lib/session/index.d.ts +3 -0
- package/lib/lib/session/index.js +3 -0
- package/lib/lib/session/stores/MemoryStore.d.ts +7 -0
- package/lib/lib/session/stores/MemoryStore.js +23 -0
- package/lib/lib/session/stores/index.d.ts +1 -0
- package/lib/lib/session/stores/index.js +1 -0
- package/lib/lib/session/types.d.ts +37 -0
- package/lib/lib/session/types.js +1 -0
- package/lib/session/SessionClient.d.ts +19 -0
- package/lib/session/SessionClient.js +132 -0
- package/lib/session/SessionManager.d.ts +139 -0
- package/lib/session/SessionManager.js +443 -0
- package/lib/stateStore.d.ts +5 -0
- package/lib/stateStore.js +9 -0
- package/lib/storage/SSIStorage.d.ts +24 -0
- package/lib/storage/SSIStorage.js +117 -0
- package/lib/storage/backends/MemoryBackend.d.ts +10 -0
- package/lib/storage/backends/MemoryBackend.js +44 -0
- package/lib/storage/backends/PostgresBackend.d.ts +24 -0
- package/lib/storage/backends/PostgresBackend.js +106 -0
- package/lib/storage/backends/RedisBackend.d.ts +19 -0
- package/lib/storage/backends/RedisBackend.js +78 -0
- package/lib/storage/types.d.ts +27 -0
- package/lib/storage/types.js +2 -0
- package/package.json +71 -0
|
@@ -0,0 +1,537 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @file
|
|
4
|
+
* SSISubscribedPlanApi - JSON:API Client for Subscribed Plan Entities
|
|
5
|
+
*
|
|
6
|
+
* A TypeScript client for interacting with Drupal JSON:API endpoints
|
|
7
|
+
* for subscribed_plan entities. This class is independent of Drupal and can be used
|
|
8
|
+
* on any platform that supports fetch.
|
|
9
|
+
*
|
|
10
|
+
* Requirements:
|
|
11
|
+
* - Node.js 18+ (for native fetch) or a fetch polyfill
|
|
12
|
+
* - TypeScript 5.0+
|
|
13
|
+
*
|
|
14
|
+
* Usage:
|
|
15
|
+
* @example
|
|
16
|
+
* ```typescript
|
|
17
|
+
* // Initialize the client with a domain
|
|
18
|
+
* const client = new SSISubscribedPlanApi('https://example.com');
|
|
19
|
+
*
|
|
20
|
+
* // Set Bearer token authentication
|
|
21
|
+
* client.setBearerToken('your-oauth-or-jwt-token-here');
|
|
22
|
+
*
|
|
23
|
+
* // List subscribed plans with filters and pagination
|
|
24
|
+
* const plans = await client.list(
|
|
25
|
+
* { status: 'active' },
|
|
26
|
+
* ['-created'],
|
|
27
|
+
* { offset: 0, limit: 10 }
|
|
28
|
+
* );
|
|
29
|
+
*
|
|
30
|
+
* // Get a single subscribed plan by UUID
|
|
31
|
+
* const plan = await client.get('550e8400-e29b-41d4-a716-446655440000', {
|
|
32
|
+
* include: ['owner', 'subscription_plan']
|
|
33
|
+
* });
|
|
34
|
+
*
|
|
35
|
+
* // Create a subscribed plan
|
|
36
|
+
* const newPlan = await client.create(
|
|
37
|
+
* { status: 'active' },
|
|
38
|
+
* { user: { type: 'user--user', id: 'user-uuid' } }
|
|
39
|
+
* );
|
|
40
|
+
*
|
|
41
|
+
* // Update a subscribed plan by UUID
|
|
42
|
+
* await client.update('550e8400-e29b-41d4-a716-446655440000', { status: 'cancelled' });
|
|
43
|
+
*
|
|
44
|
+
* // Delete a subscribed plan by UUID
|
|
45
|
+
* await client.delete('550e8400-e29b-41d4-a716-446655440000');
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
49
|
+
exports.SSISubscribedPlanApi = void 0;
|
|
50
|
+
/**
|
|
51
|
+
* SSISubscribedPlanApi - JSON:API Client for Subscribed Plan Entities
|
|
52
|
+
*/
|
|
53
|
+
class SSISubscribedPlanApi {
|
|
54
|
+
/**
|
|
55
|
+
* Constructs an SSISubscribedPlanApi client.
|
|
56
|
+
*
|
|
57
|
+
* @param domain - The full URL prefix of the Drupal site (e.g., 'https://example.com').
|
|
58
|
+
* @param options - Optional configuration:
|
|
59
|
+
* - apiBasePath: Override the default API path (default: '/jsonapi/subscribed_plan/subscribed_plan')
|
|
60
|
+
* - timeout: Request timeout in milliseconds (currently unused; reserved for future use)
|
|
61
|
+
*/
|
|
62
|
+
constructor(domain, options) {
|
|
63
|
+
this.apiBasePath = '/jsonapi/subscribed_plan/subscribed_plan';
|
|
64
|
+
this.bearerToken = null;
|
|
65
|
+
this.csrfToken = null;
|
|
66
|
+
this.defaultHeaders = {
|
|
67
|
+
Accept: 'application/vnd.api+json',
|
|
68
|
+
'Content-Type': 'application/vnd.api+json',
|
|
69
|
+
};
|
|
70
|
+
this.baseUrl = domain.trim().replace(/\/+$/, ''); // Remove trailing slashes
|
|
71
|
+
if (options?.apiBasePath) {
|
|
72
|
+
this.apiBasePath = options.apiBasePath;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Sets the Bearer token for authentication.
|
|
77
|
+
*
|
|
78
|
+
* @param token - The Bearer token (OAuth/JWT).
|
|
79
|
+
*/
|
|
80
|
+
setBearerToken(token) {
|
|
81
|
+
this.bearerToken = token;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Gets the current Bearer token.
|
|
85
|
+
*
|
|
86
|
+
* @returns The Bearer token, or null if not set.
|
|
87
|
+
*/
|
|
88
|
+
getBearerToken() {
|
|
89
|
+
return this.bearerToken;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Fetches a CSRF token from the Drupal session/token endpoint.
|
|
93
|
+
* The token is cached after the first fetch to avoid unnecessary requests.
|
|
94
|
+
*
|
|
95
|
+
* @returns Promise resolving to the CSRF token string.
|
|
96
|
+
*
|
|
97
|
+
* @throws Error if the request fails.
|
|
98
|
+
*
|
|
99
|
+
* @private
|
|
100
|
+
*/
|
|
101
|
+
async getCsrfToken() {
|
|
102
|
+
// Return cached token if available
|
|
103
|
+
if (this.csrfToken) {
|
|
104
|
+
return this.csrfToken;
|
|
105
|
+
}
|
|
106
|
+
const url = `${this.baseUrl}/session/token`;
|
|
107
|
+
const headers = {
|
|
108
|
+
Accept: 'text/plain',
|
|
109
|
+
};
|
|
110
|
+
// Include Bearer token if available (for authenticated requests)
|
|
111
|
+
if (this.bearerToken) {
|
|
112
|
+
headers['Authorization'] = `Bearer ${this.bearerToken}`;
|
|
113
|
+
}
|
|
114
|
+
try {
|
|
115
|
+
const response = await fetch(url, {
|
|
116
|
+
method: 'GET',
|
|
117
|
+
headers,
|
|
118
|
+
});
|
|
119
|
+
if (!response.ok) {
|
|
120
|
+
throw new Error(`Failed to fetch CSRF token: HTTP ${response.status} ${response.statusText}`);
|
|
121
|
+
}
|
|
122
|
+
const token = await response.text();
|
|
123
|
+
if (!token || !token.trim()) {
|
|
124
|
+
throw new Error('CSRF token response was empty');
|
|
125
|
+
}
|
|
126
|
+
// Cache the token
|
|
127
|
+
this.csrfToken = token.trim();
|
|
128
|
+
return this.csrfToken;
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
console.error('[SSISubscribedPlanApi] Failed to fetch CSRF token:', error);
|
|
132
|
+
if (error instanceof Error) {
|
|
133
|
+
throw new Error(`CSRF token fetch failed: ${error.message}`);
|
|
134
|
+
}
|
|
135
|
+
throw new Error(`CSRF token fetch failed: ${String(error)}`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Clears the cached CSRF token, forcing a fresh fetch on the next write operation.
|
|
140
|
+
* This may be useful if the token expires or becomes invalid.
|
|
141
|
+
*/
|
|
142
|
+
clearCsrfToken() {
|
|
143
|
+
this.csrfToken = null;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Lists subscribed_plan entities with optional filtering, sorting, and pagination.
|
|
147
|
+
*
|
|
148
|
+
* @param options - List options:
|
|
149
|
+
* - filters: Filter conditions (e.g., { status: 'active' } or { created: { operator: '>', value: '2024-01-01' } })
|
|
150
|
+
* - sort: Sort fields (e.g., ['-created', 'status'] for descending created, ascending status)
|
|
151
|
+
* - pagination: Pagination options (offset, limit; max limit: 50)
|
|
152
|
+
* - include: Related resources to include (e.g., ['user', 'subscription_plan'])
|
|
153
|
+
* - fields: Sparse fieldsets to limit returned fields
|
|
154
|
+
*
|
|
155
|
+
* @returns Promise resolving to the JSON:API document with subscribed plans.
|
|
156
|
+
*
|
|
157
|
+
* @throws Error if the request fails.
|
|
158
|
+
*/
|
|
159
|
+
async list(options = {}) {
|
|
160
|
+
const queryParams = new URLSearchParams();
|
|
161
|
+
// Build filter query parameters
|
|
162
|
+
if (options.filters) {
|
|
163
|
+
const filterParams = this.buildFilterParams(options.filters);
|
|
164
|
+
for (const [key, value] of Object.entries(filterParams)) {
|
|
165
|
+
queryParams.append(key, String(value));
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
// Build sort query parameters
|
|
169
|
+
if (options.sort && options.sort.length > 0) {
|
|
170
|
+
queryParams.set('sort', options.sort.join(','));
|
|
171
|
+
}
|
|
172
|
+
// Build pagination query parameters
|
|
173
|
+
if (options.pagination) {
|
|
174
|
+
if (options.pagination.offset !== undefined) {
|
|
175
|
+
queryParams.set('page[offset]', String(options.pagination.offset));
|
|
176
|
+
}
|
|
177
|
+
if (options.pagination.limit !== undefined) {
|
|
178
|
+
queryParams.set('page[limit]', String(Math.min(options.pagination.limit, 50)));
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
// Build include query parameters
|
|
182
|
+
if (options.include && options.include.length > 0) {
|
|
183
|
+
queryParams.set('include', options.include.join(','));
|
|
184
|
+
}
|
|
185
|
+
// Build fields query parameters
|
|
186
|
+
if (options.fields) {
|
|
187
|
+
for (const [resourceType, fieldList] of Object.entries(options.fields)) {
|
|
188
|
+
queryParams.set(`fields[${resourceType}]`, fieldList.join(','));
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
const url = `${this.baseUrl}${this.apiBasePath}${queryParams.toString() ? `?${queryParams.toString()}` : ''}`;
|
|
192
|
+
return this.request('GET', url);
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Retrieves a single subscribed_plan entity by UUID.
|
|
196
|
+
*
|
|
197
|
+
* The API endpoint is constructed as: baseUrl + apiBasePath + "/" + uuid
|
|
198
|
+
* Example: https://example.com/jsonapi/subscribed_plan/subscribed_plan/550e8400-e29b-41d4-a716-446655440000
|
|
199
|
+
*
|
|
200
|
+
* @param uuid - The UUID of the subscribed_plan entity (NOT the internal ID).
|
|
201
|
+
* Must be a valid UUID string (e.g., '550e8400-e29b-41d4-a716-446655440000').
|
|
202
|
+
* @param options - Get options:
|
|
203
|
+
* - include: Related resources to include (e.g., ['user', 'subscription_plan'])
|
|
204
|
+
* - fields: Sparse fieldsets to limit returned fields
|
|
205
|
+
*
|
|
206
|
+
* @returns Promise resolving to the JSON:API document with the subscribed plan.
|
|
207
|
+
*
|
|
208
|
+
* @throws Error if the request fails or entity is not found.
|
|
209
|
+
*/
|
|
210
|
+
async get(uuid, options = {}) {
|
|
211
|
+
const queryParams = new URLSearchParams();
|
|
212
|
+
if (options.include && options.include.length > 0) {
|
|
213
|
+
queryParams.set('include', options.include.join(','));
|
|
214
|
+
}
|
|
215
|
+
if (options.fields) {
|
|
216
|
+
for (const [resourceType, fieldList] of Object.entries(options.fields)) {
|
|
217
|
+
queryParams.set(`fields[${resourceType}]`, fieldList.join(','));
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
// Construct URL: baseUrl + apiBasePath + "/" + uuid
|
|
221
|
+
const url = `${this.baseUrl}${this.apiBasePath}/${uuid}${queryParams.toString() ? `?${queryParams.toString()}` : ''}`;
|
|
222
|
+
return this.request('GET', url);
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Creates a new subscribed_plan entity.
|
|
226
|
+
*
|
|
227
|
+
* @param attributes - Entity attributes (e.g., { status: 'active' }).
|
|
228
|
+
* @param options - Create options:
|
|
229
|
+
* - relationships: Entity relationships
|
|
230
|
+
* - resourceType: The resource type (default: 'subscribed_plan--subscribed_plan')
|
|
231
|
+
*
|
|
232
|
+
* @returns Promise resolving to the JSON:API document with the created entity.
|
|
233
|
+
*
|
|
234
|
+
* @throws Error if the request fails or validation errors occur.
|
|
235
|
+
*/
|
|
236
|
+
async create(attributes, options = {}) {
|
|
237
|
+
const resourceType = options.resourceType || 'subscribed_plan--subscribed_plan';
|
|
238
|
+
const data = {
|
|
239
|
+
type: resourceType,
|
|
240
|
+
attributes,
|
|
241
|
+
};
|
|
242
|
+
if (options.relationships) {
|
|
243
|
+
data.relationships = this.formatRelationships(options.relationships);
|
|
244
|
+
}
|
|
245
|
+
const body = { data };
|
|
246
|
+
const url = `${this.baseUrl}${this.apiBasePath}`;
|
|
247
|
+
return this.request('POST', url, body);
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Updates an existing subscribed_plan entity.
|
|
251
|
+
*
|
|
252
|
+
* The API endpoint is constructed as: baseUrl + apiBasePath + "/" + uuid
|
|
253
|
+
* Example: https://example.com/jsonapi/subscribed_plan/subscribed_plan/550e8400-e29b-41d4-a716-446655440000
|
|
254
|
+
*
|
|
255
|
+
* @param uuid - The UUID of the subscribed_plan entity to update (NOT the internal ID).
|
|
256
|
+
* Must be a valid UUID string (e.g., '550e8400-e29b-41d4-a716-446655440000').
|
|
257
|
+
* @param attributes - Attributes to update (partial updates supported).
|
|
258
|
+
* @param options - Update options:
|
|
259
|
+
* - relationships: Relationships to update (optional)
|
|
260
|
+
* - resourceType: The resource type (default: 'subscribed_plan--subscribed_plan')
|
|
261
|
+
*
|
|
262
|
+
* @returns Promise resolving to the JSON:API document with the updated entity.
|
|
263
|
+
*
|
|
264
|
+
* @throws Error if the request fails or validation errors occur.
|
|
265
|
+
*/
|
|
266
|
+
async update(uuid, attributes, options = {}) {
|
|
267
|
+
const resourceType = options.resourceType || 'subscribed_plan--subscribed_plan';
|
|
268
|
+
const data = {
|
|
269
|
+
type: resourceType,
|
|
270
|
+
id: uuid,
|
|
271
|
+
attributes,
|
|
272
|
+
};
|
|
273
|
+
if (options.relationships) {
|
|
274
|
+
data.relationships = this.formatRelationships(options.relationships);
|
|
275
|
+
}
|
|
276
|
+
const body = { data };
|
|
277
|
+
// Construct URL: baseUrl + apiBasePath + "/" + uuid
|
|
278
|
+
const url = `${this.baseUrl}${this.apiBasePath}/${uuid}`;
|
|
279
|
+
return this.request('PATCH', url, body);
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Deletes a subscribed_plan entity.
|
|
283
|
+
*
|
|
284
|
+
* The API endpoint is constructed as: baseUrl + apiBasePath + "/" + uuid
|
|
285
|
+
* Example: https://example.com/jsonapi/subscribed_plan/subscribed_plan/550e8400-e29b-41d4-a716-446655440000
|
|
286
|
+
*
|
|
287
|
+
* @param uuid - The UUID of the subscribed_plan entity to delete (NOT the internal ID).
|
|
288
|
+
* Must be a valid UUID string (e.g., '550e8400-e29b-41d4-a716-446655440000').
|
|
289
|
+
*
|
|
290
|
+
* @returns Promise resolving to true if deletion was successful.
|
|
291
|
+
*
|
|
292
|
+
* @throws Error if the request fails.
|
|
293
|
+
*/
|
|
294
|
+
async delete(uuid) {
|
|
295
|
+
// Construct URL: baseUrl + apiBasePath + "/" + uuid
|
|
296
|
+
const url = `${this.baseUrl}${this.apiBasePath}/${uuid}`;
|
|
297
|
+
const response = await this.requestRaw('DELETE', url);
|
|
298
|
+
// Get response text for debugging
|
|
299
|
+
const responseClone = response.clone();
|
|
300
|
+
const responseText = await responseClone.text();
|
|
301
|
+
// DELETE returns 204 No Content on success
|
|
302
|
+
if (response.status === 204) {
|
|
303
|
+
return true;
|
|
304
|
+
}
|
|
305
|
+
// If not 204, try to parse error response
|
|
306
|
+
if (!response.ok) {
|
|
307
|
+
const errorData = await this.parseErrorResponse(response);
|
|
308
|
+
console.error('[SSISubscribedPlanApi] DELETE error response:', errorData);
|
|
309
|
+
throw this.createError(errorData, response.status, response.statusText);
|
|
310
|
+
}
|
|
311
|
+
return true;
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Makes an HTTP request to the API and returns the parsed JSON:API document.
|
|
315
|
+
*
|
|
316
|
+
* @param method - HTTP method (GET, POST, PATCH).
|
|
317
|
+
* @param url - The full URL.
|
|
318
|
+
* @param body - Request body (for POST/PATCH).
|
|
319
|
+
*
|
|
320
|
+
* @returns Promise resolving to the parsed JSON:API document.
|
|
321
|
+
*
|
|
322
|
+
* @throws Error if the request fails.
|
|
323
|
+
*
|
|
324
|
+
* @private
|
|
325
|
+
*/
|
|
326
|
+
async request(method, url, body) {
|
|
327
|
+
const response = await this.requestRaw(method, url, body);
|
|
328
|
+
// Get response text for debugging and parsing
|
|
329
|
+
const responseText = await response.text();
|
|
330
|
+
if (!response.ok) {
|
|
331
|
+
// Parse error from the response text we already read
|
|
332
|
+
const errorData = this.parseErrorResponseFromText(responseText);
|
|
333
|
+
console.error('[SSISubscribedPlanApi] Error response:', errorData);
|
|
334
|
+
throw this.createError(errorData, response.status, response.statusText);
|
|
335
|
+
}
|
|
336
|
+
// Handle 204 No Content (shouldn't happen for GET/POST/PATCH, but handle gracefully)
|
|
337
|
+
if (response.status === 204) {
|
|
338
|
+
return { data: [] };
|
|
339
|
+
}
|
|
340
|
+
// Parse JSON from the response text
|
|
341
|
+
const json = JSON.parse(responseText);
|
|
342
|
+
return json;
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Makes a raw HTTP request to the API and returns the Response object.
|
|
346
|
+
*
|
|
347
|
+
* @param method - HTTP method (GET, POST, PATCH, DELETE).
|
|
348
|
+
* @param url - The full URL.
|
|
349
|
+
* @param body - Request body (for POST/PATCH).
|
|
350
|
+
*
|
|
351
|
+
* @returns Promise resolving to the Response object.
|
|
352
|
+
*
|
|
353
|
+
* @throws Error if the request fails (network errors, etc.).
|
|
354
|
+
*
|
|
355
|
+
* @private
|
|
356
|
+
*/
|
|
357
|
+
async requestRaw(method, url, body) {
|
|
358
|
+
const headers = { ...this.defaultHeaders };
|
|
359
|
+
// Add authentication if configured
|
|
360
|
+
if (this.bearerToken) {
|
|
361
|
+
headers['Authorization'] = `Bearer ${this.bearerToken}`;
|
|
362
|
+
}
|
|
363
|
+
// Add CSRF token for write operations
|
|
364
|
+
if (['POST', 'PATCH', 'DELETE'].includes(method)) {
|
|
365
|
+
try {
|
|
366
|
+
const csrfToken = await this.getCsrfToken();
|
|
367
|
+
if (csrfToken) {
|
|
368
|
+
headers['X-CSRF-Token'] = csrfToken;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
catch (error) {
|
|
372
|
+
console.warn('[SSISubscribedPlanApi] Failed to fetch CSRF token, proceeding without it:', error);
|
|
373
|
+
// Continue without CSRF token - some configurations may not require it
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
const fetchOptions = {
|
|
377
|
+
method,
|
|
378
|
+
headers,
|
|
379
|
+
};
|
|
380
|
+
if (body && (method === 'POST' || method === 'PATCH')) {
|
|
381
|
+
fetchOptions.body = JSON.stringify(body);
|
|
382
|
+
}
|
|
383
|
+
try {
|
|
384
|
+
const response = await fetch(url, fetchOptions);
|
|
385
|
+
return response;
|
|
386
|
+
}
|
|
387
|
+
catch (error) {
|
|
388
|
+
console.error('[SSISubscribedPlanApi] Request error:', error);
|
|
389
|
+
if (error instanceof Error) {
|
|
390
|
+
throw new Error(`Request failed: ${error.message}`);
|
|
391
|
+
}
|
|
392
|
+
throw new Error(`Request failed: ${String(error)}`);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Parses error response from JSON:API.
|
|
397
|
+
*
|
|
398
|
+
* @param response - The HTTP response.
|
|
399
|
+
*
|
|
400
|
+
* @returns Promise resolving to the error data.
|
|
401
|
+
*
|
|
402
|
+
* @private
|
|
403
|
+
*/
|
|
404
|
+
async parseErrorResponse(response) {
|
|
405
|
+
try {
|
|
406
|
+
const text = await response.text();
|
|
407
|
+
return this.parseErrorResponseFromText(text);
|
|
408
|
+
}
|
|
409
|
+
catch {
|
|
410
|
+
return null;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Parses error response from text string.
|
|
415
|
+
*
|
|
416
|
+
* @param text - The response text.
|
|
417
|
+
*
|
|
418
|
+
* @returns The error data, or null if parsing fails.
|
|
419
|
+
*
|
|
420
|
+
* @private
|
|
421
|
+
*/
|
|
422
|
+
parseErrorResponseFromText(text) {
|
|
423
|
+
try {
|
|
424
|
+
if (!text) {
|
|
425
|
+
return null;
|
|
426
|
+
}
|
|
427
|
+
return JSON.parse(text);
|
|
428
|
+
}
|
|
429
|
+
catch {
|
|
430
|
+
return null;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* Creates an error from JSON:API error response.
|
|
435
|
+
*
|
|
436
|
+
* @param errorData - The error data.
|
|
437
|
+
* @param status - HTTP status code.
|
|
438
|
+
* @param statusText - HTTP status text.
|
|
439
|
+
*
|
|
440
|
+
* @returns Error instance.
|
|
441
|
+
*
|
|
442
|
+
* @private
|
|
443
|
+
*/
|
|
444
|
+
createError(errorData, status, statusText) {
|
|
445
|
+
if (errorData && errorData.errors && errorData.errors.length > 0) {
|
|
446
|
+
const messages = errorData.errors.map((error) => {
|
|
447
|
+
const statusCode = error.status || String(status);
|
|
448
|
+
const message = error.detail || error.title || 'Unknown error';
|
|
449
|
+
return `[${statusCode}] ${message}`;
|
|
450
|
+
});
|
|
451
|
+
const error = new Error(messages.join('; '));
|
|
452
|
+
error.status = status;
|
|
453
|
+
error.errors = errorData.errors;
|
|
454
|
+
return error;
|
|
455
|
+
}
|
|
456
|
+
const error = new Error(`HTTP ${status} ${statusText}`);
|
|
457
|
+
error.status = status;
|
|
458
|
+
return error;
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* Builds filter query parameters from a filter object.
|
|
462
|
+
*
|
|
463
|
+
* @param filters - Filter conditions.
|
|
464
|
+
*
|
|
465
|
+
* @returns Query parameters for filters.
|
|
466
|
+
*
|
|
467
|
+
* @private
|
|
468
|
+
*/
|
|
469
|
+
buildFilterParams(filters) {
|
|
470
|
+
const params = {};
|
|
471
|
+
let index = 0;
|
|
472
|
+
for (const [field, condition] of Object.entries(filters)) {
|
|
473
|
+
// Simple equality filter: { status: 'active' }
|
|
474
|
+
if (typeof condition === 'string' || typeof condition === 'number') {
|
|
475
|
+
params[`filter[${field}]`] = String(condition);
|
|
476
|
+
continue;
|
|
477
|
+
}
|
|
478
|
+
// Complex filter with operator: { created: { operator: '>', value: '2024-01-01' } }
|
|
479
|
+
if (condition && typeof condition === 'object' && 'operator' in condition) {
|
|
480
|
+
const filterKey = `filter[${index}]`;
|
|
481
|
+
params[`${filterKey}[condition][path]`] = field;
|
|
482
|
+
params[`${filterKey}[condition][operator]`] = condition.operator;
|
|
483
|
+
if ('value' in condition) {
|
|
484
|
+
const value = condition.value;
|
|
485
|
+
// Handle array values for IN, NOT IN operators
|
|
486
|
+
if (Array.isArray(value)) {
|
|
487
|
+
value.forEach((v, i) => {
|
|
488
|
+
params[`${filterKey}[condition][value][${i}]`] = String(v);
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
else {
|
|
492
|
+
params[`${filterKey}[condition][value]`] = String(value);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
index++;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
return params;
|
|
499
|
+
}
|
|
500
|
+
/**
|
|
501
|
+
* Formats relationships for JSON:API format.
|
|
502
|
+
*
|
|
503
|
+
* @param relationships - Relationships input.
|
|
504
|
+
*
|
|
505
|
+
* @returns Formatted relationships.
|
|
506
|
+
*
|
|
507
|
+
* @private
|
|
508
|
+
*/
|
|
509
|
+
formatRelationships(relationships) {
|
|
510
|
+
const formatted = {};
|
|
511
|
+
for (const [fieldName, relationship] of Object.entries(relationships)) {
|
|
512
|
+
// Single relationship: { user: { type: 'user--user', id: 'user-uuid' } }
|
|
513
|
+
if (relationship &&
|
|
514
|
+
typeof relationship === 'object' &&
|
|
515
|
+
'type' in relationship &&
|
|
516
|
+
'id' in relationship) {
|
|
517
|
+
formatted[fieldName] = {
|
|
518
|
+
data: {
|
|
519
|
+
type: relationship.type,
|
|
520
|
+
id: relationship.id,
|
|
521
|
+
},
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
// Multiple relationships: { tags: [{ type: 'taxonomy_term--tags', id: 'tag-uuid-1' }, ...] }
|
|
525
|
+
else if (Array.isArray(relationship)) {
|
|
526
|
+
formatted[fieldName] = {
|
|
527
|
+
data: relationship.map((item) => ({
|
|
528
|
+
type: item.type,
|
|
529
|
+
id: item.id,
|
|
530
|
+
})),
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
return formatted;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
exports.SSISubscribedPlanApi = SSISubscribedPlanApi;
|