@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.
Files changed (60) hide show
  1. package/README.md +1 -0
  2. package/lib/SSIProject.d.ts +343 -0
  3. package/lib/SSIProject.js +429 -0
  4. package/lib/SSIProjectApi.d.ts +384 -0
  5. package/lib/SSIProjectApi.js +534 -0
  6. package/lib/SSISubscribedFeatureApi.d.ts +387 -0
  7. package/lib/SSISubscribedFeatureApi.js +511 -0
  8. package/lib/SSISubscribedLimitApi.d.ts +384 -0
  9. package/lib/SSISubscribedLimitApi.js +534 -0
  10. package/lib/SSISubscribedPlanApi.d.ts +384 -0
  11. package/lib/SSISubscribedPlanApi.js +537 -0
  12. package/lib/SubscribedPlanManager.d.ts +380 -0
  13. package/lib/SubscribedPlanManager.js +288 -0
  14. package/lib/UsageApi.d.ts +128 -0
  15. package/lib/UsageApi.js +224 -0
  16. package/lib/auth.server.d.ts +192 -0
  17. package/lib/auth.server.js +579 -0
  18. package/lib/cache/SSICache.d.ts +40 -0
  19. package/lib/cache/SSICache.js +134 -0
  20. package/lib/cache/backends/MemoryCacheBackend.d.ts +15 -0
  21. package/lib/cache/backends/MemoryCacheBackend.js +46 -0
  22. package/lib/cache/backends/RedisCacheBackend.d.ts +27 -0
  23. package/lib/cache/backends/RedisCacheBackend.js +95 -0
  24. package/lib/cache/constants.d.ts +7 -0
  25. package/lib/cache/constants.js +10 -0
  26. package/lib/cache/types.d.ts +27 -0
  27. package/lib/cache/types.js +2 -0
  28. package/lib/frontend/index.d.ts +1 -0
  29. package/lib/frontend/index.js +6 -0
  30. package/lib/frontend/session/SessionClient.d.ts +24 -0
  31. package/lib/frontend/session/SessionClient.js +145 -0
  32. package/lib/index.d.ts +15 -0
  33. package/lib/index.js +38 -0
  34. package/lib/lib/session/SessionClient.d.ts +11 -0
  35. package/lib/lib/session/SessionClient.js +47 -0
  36. package/lib/lib/session/index.d.ts +3 -0
  37. package/lib/lib/session/index.js +3 -0
  38. package/lib/lib/session/stores/MemoryStore.d.ts +7 -0
  39. package/lib/lib/session/stores/MemoryStore.js +23 -0
  40. package/lib/lib/session/stores/index.d.ts +1 -0
  41. package/lib/lib/session/stores/index.js +1 -0
  42. package/lib/lib/session/types.d.ts +37 -0
  43. package/lib/lib/session/types.js +1 -0
  44. package/lib/session/SessionClient.d.ts +19 -0
  45. package/lib/session/SessionClient.js +132 -0
  46. package/lib/session/SessionManager.d.ts +139 -0
  47. package/lib/session/SessionManager.js +443 -0
  48. package/lib/stateStore.d.ts +5 -0
  49. package/lib/stateStore.js +9 -0
  50. package/lib/storage/SSIStorage.d.ts +24 -0
  51. package/lib/storage/SSIStorage.js +117 -0
  52. package/lib/storage/backends/MemoryBackend.d.ts +10 -0
  53. package/lib/storage/backends/MemoryBackend.js +44 -0
  54. package/lib/storage/backends/PostgresBackend.d.ts +24 -0
  55. package/lib/storage/backends/PostgresBackend.js +106 -0
  56. package/lib/storage/backends/RedisBackend.d.ts +19 -0
  57. package/lib/storage/backends/RedisBackend.js +78 -0
  58. package/lib/storage/types.d.ts +27 -0
  59. package/lib/storage/types.js +2 -0
  60. package/package.json +71 -0
@@ -0,0 +1,429 @@
1
+ /**
2
+ * @file
3
+ * SSIProjectApi - JSON:API Client for Project Entities
4
+ *
5
+ * A TypeScript client for interacting with Drupal JSON:API endpoints
6
+ * for project entities. This class is independent of Drupal and can be used
7
+ * on any platform that supports fetch.
8
+ *
9
+ * Requirements:
10
+ * - Node.js 18+ (for native fetch) or a fetch polyfill
11
+ * - TypeScript 5.0+
12
+ *
13
+ * Usage:
14
+ * @example
15
+ * ```typescript
16
+ * // Initialize the client with a domain
17
+ * const client = new SSIProjectApi('https://example.com');
18
+ *
19
+ * // Set Bearer token authentication
20
+ * client.setBearerToken('your-oauth-or-jwt-token-here');
21
+ *
22
+ * // List projects with filters and pagination
23
+ * const projects = await client.list(
24
+ * { status: 'active' },
25
+ * ['-created'],
26
+ * { offset: 0, limit: 10 }
27
+ * );
28
+ *
29
+ * // Get a single project
30
+ * const project = await client.get('uuid-here', ['owner', 'tags']);
31
+ *
32
+ * // Create a project
33
+ * const newProject = await client.create(
34
+ * { title: 'New Project', status: 'active' },
35
+ * { owner: { type: 'user--user', id: 'user-uuid' } }
36
+ * );
37
+ *
38
+ * // Update a project
39
+ * await client.update('uuid-here', { status: 'completed' });
40
+ *
41
+ * // Delete a project
42
+ * await client.delete('uuid-here');
43
+ * ```
44
+ */
45
+ /**
46
+ * SSIProjectApi - JSON:API Client for Project Entities
47
+ */
48
+ export class SSIProjectApi {
49
+ /**
50
+ * Constructs an SSIProjectApi client.
51
+ *
52
+ * @param domain - The full URL prefix of the Drupal site (e.g., 'https://example.com').
53
+ * @param options - Optional configuration:
54
+ * - apiBasePath: Override the default API path (default: '/jsonapi/project/project')
55
+ * - timeout: Request timeout in milliseconds (default: 30000)
56
+ */
57
+ constructor(domain, options) {
58
+ this.apiBasePath = '/jsonapi/project/project';
59
+ this.bearerToken = null;
60
+ this.defaultHeaders = {
61
+ 'Accept': 'application/vnd.api+json',
62
+ 'Content-Type': 'application/vnd.api+json',
63
+ };
64
+ this.baseUrl = domain.trim().replace(/\/+$/, ''); // Remove trailing slashes
65
+ if (options?.apiBasePath) {
66
+ this.apiBasePath = options.apiBasePath;
67
+ }
68
+ // TODO: Add CSRF token handling
69
+ // CSRF tokens may be required for write operations (POST, PATCH, DELETE)
70
+ // when using certain authentication methods. This should be implemented
71
+ // by fetching the token from /session/token endpoint before write operations.
72
+ }
73
+ /**
74
+ * Sets the Bearer token for authentication.
75
+ *
76
+ * @param token - The Bearer token (OAuth/JWT).
77
+ */
78
+ setBearerToken(token) {
79
+ this.bearerToken = token;
80
+ }
81
+ /**
82
+ * Gets the current Bearer token.
83
+ *
84
+ * @returns The Bearer token, or null if not set.
85
+ */
86
+ getBearerToken() {
87
+ return this.bearerToken;
88
+ }
89
+ /**
90
+ * Lists project entities with optional filtering, sorting, and pagination.
91
+ *
92
+ * @param options - List options:
93
+ * - filters: Filter conditions (e.g., { status: 'active' } or { created: { operator: '>', value: '2024-01-01' } })
94
+ * - sort: Sort fields (e.g., ['-created', 'title'] for descending created, ascending title)
95
+ * - pagination: Pagination options (offset, limit; max limit: 50)
96
+ * - include: Related resources to include (e.g., ['owner', 'tags'])
97
+ * - fields: Sparse fieldsets to limit returned fields
98
+ *
99
+ * @returns Promise resolving to the JSON:API document with projects.
100
+ *
101
+ * @throws Error if the request fails.
102
+ */
103
+ async list(options = {}) {
104
+ const queryParams = new URLSearchParams();
105
+ // Build filter query parameters
106
+ if (options.filters) {
107
+ const filterParams = this.buildFilterParams(options.filters);
108
+ for (const [key, value] of Object.entries(filterParams)) {
109
+ queryParams.append(key, String(value));
110
+ }
111
+ }
112
+ // Build sort query parameters
113
+ if (options.sort && options.sort.length > 0) {
114
+ queryParams.set('sort', options.sort.join(','));
115
+ }
116
+ // Build pagination query parameters
117
+ if (options.pagination) {
118
+ if (options.pagination.offset !== undefined) {
119
+ queryParams.set('page[offset]', String(options.pagination.offset));
120
+ }
121
+ if (options.pagination.limit !== undefined) {
122
+ queryParams.set('page[limit]', String(Math.min(options.pagination.limit, 50)));
123
+ }
124
+ }
125
+ // Build include query parameters
126
+ if (options.include && options.include.length > 0) {
127
+ queryParams.set('include', options.include.join(','));
128
+ }
129
+ // Build fields query parameters
130
+ if (options.fields) {
131
+ for (const [resourceType, fieldList] of Object.entries(options.fields)) {
132
+ queryParams.set(`fields[${resourceType}]`, fieldList.join(','));
133
+ }
134
+ }
135
+ const url = `${this.baseUrl}${this.apiBasePath}${queryParams.toString() ? `?${queryParams.toString()}` : ''}`;
136
+ return this.request('GET', url);
137
+ }
138
+ /**
139
+ * Retrieves a single project entity by UUID.
140
+ *
141
+ * @param uuid - The UUID of the project entity.
142
+ * @param options - Get options:
143
+ * - include: Related resources to include
144
+ * - fields: Sparse fieldsets to limit returned fields
145
+ *
146
+ * @returns Promise resolving to the JSON:API document with the project.
147
+ *
148
+ * @throws Error if the request fails or entity is not found.
149
+ */
150
+ async get(uuid, options = {}) {
151
+ const queryParams = new URLSearchParams();
152
+ if (options.include && options.include.length > 0) {
153
+ queryParams.set('include', options.include.join(','));
154
+ }
155
+ if (options.fields) {
156
+ for (const [resourceType, fieldList] of Object.entries(options.fields)) {
157
+ queryParams.set(`fields[${resourceType}]`, fieldList.join(','));
158
+ }
159
+ }
160
+ const url = `${this.baseUrl}${this.apiBasePath}/${uuid}${queryParams.toString() ? `?${queryParams.toString()}` : ''}`;
161
+ return this.request('GET', url);
162
+ }
163
+ /**
164
+ * Creates a new project entity.
165
+ *
166
+ * @param attributes - Entity attributes (e.g., { title: 'My Project', status: 'active' }).
167
+ * @param options - Create options:
168
+ * - relationships: Entity relationships
169
+ * - resourceType: The resource type (default: 'project--project')
170
+ *
171
+ * @returns Promise resolving to the JSON:API document with the created entity.
172
+ *
173
+ * @throws Error if the request fails or validation errors occur.
174
+ */
175
+ async create(attributes, options = {}) {
176
+ const resourceType = options.resourceType || 'project--project';
177
+ const data = {
178
+ type: resourceType,
179
+ attributes,
180
+ };
181
+ if (options.relationships) {
182
+ data.relationships = this.formatRelationships(options.relationships);
183
+ }
184
+ const body = { data };
185
+ const url = `${this.baseUrl}${this.apiBasePath}`;
186
+ return this.request('POST', url, body);
187
+ }
188
+ /**
189
+ * Updates an existing project entity.
190
+ *
191
+ * @param uuid - The UUID of the project entity to update.
192
+ * @param attributes - Attributes to update (partial updates supported).
193
+ * @param options - Update options:
194
+ * - relationships: Relationships to update (optional)
195
+ * - resourceType: The resource type (default: 'project--project')
196
+ *
197
+ * @returns Promise resolving to the JSON:API document with the updated entity.
198
+ *
199
+ * @throws Error if the request fails or validation errors occur.
200
+ */
201
+ async update(uuid, attributes, options = {}) {
202
+ const resourceType = options.resourceType || 'project--project';
203
+ const data = {
204
+ type: resourceType,
205
+ id: uuid,
206
+ attributes,
207
+ };
208
+ if (options.relationships) {
209
+ data.relationships = this.formatRelationships(options.relationships);
210
+ }
211
+ const body = { data };
212
+ const url = `${this.baseUrl}${this.apiBasePath}/${uuid}`;
213
+ return this.request('PATCH', url, body);
214
+ }
215
+ /**
216
+ * Deletes a project entity.
217
+ *
218
+ * @param uuid - The UUID of the project entity to delete.
219
+ *
220
+ * @returns Promise resolving to true if deletion was successful.
221
+ *
222
+ * @throws Error if the request fails.
223
+ */
224
+ async delete(uuid) {
225
+ const url = `${this.baseUrl}${this.apiBasePath}/${uuid}`;
226
+ const response = await this.requestRaw('DELETE', url);
227
+ // DELETE returns 204 No Content on success
228
+ if (response.status === 204) {
229
+ return true;
230
+ }
231
+ // If not 204, try to parse error response
232
+ if (!response.ok) {
233
+ const errorData = await this.parseErrorResponse(response);
234
+ throw this.createError(errorData, response.status, response.statusText);
235
+ }
236
+ return true;
237
+ }
238
+ /**
239
+ * Makes an HTTP request to the API and returns the parsed JSON:API document.
240
+ *
241
+ * @param method - HTTP method (GET, POST, PATCH).
242
+ * @param url - The full URL.
243
+ * @param body - Request body (for POST/PATCH).
244
+ *
245
+ * @returns Promise resolving to the parsed JSON:API document.
246
+ *
247
+ * @throws Error if the request fails.
248
+ *
249
+ * @private
250
+ */
251
+ async request(method, url, body) {
252
+ const response = await this.requestRaw(method, url, body);
253
+ if (!response.ok) {
254
+ const errorData = await this.parseErrorResponse(response);
255
+ throw this.createError(errorData, response.status, response.statusText);
256
+ }
257
+ // Handle 204 No Content (shouldn't happen for GET/POST/PATCH, but handle gracefully)
258
+ if (response.status === 204) {
259
+ return { data: [] };
260
+ }
261
+ const json = await response.json();
262
+ return json;
263
+ }
264
+ /**
265
+ * Makes a raw HTTP request to the API and returns the Response object.
266
+ *
267
+ * @param method - HTTP method (GET, POST, PATCH, DELETE).
268
+ * @param url - The full URL.
269
+ * @param body - Request body (for POST/PATCH).
270
+ *
271
+ * @returns Promise resolving to the Response object.
272
+ *
273
+ * @throws Error if the request fails (network errors, etc.).
274
+ *
275
+ * @private
276
+ */
277
+ async requestRaw(method, url, body) {
278
+ const headers = { ...this.defaultHeaders };
279
+ // Add authentication if configured
280
+ if (this.bearerToken) {
281
+ headers['Authorization'] = `Bearer ${this.bearerToken}`;
282
+ }
283
+ // TODO: Add CSRF token for write operations
284
+ // if (['POST', 'PATCH', 'DELETE'].includes(method)) {
285
+ // const csrfToken = await this.getCsrfToken();
286
+ // if (csrfToken) {
287
+ // headers['X-CSRF-Token'] = csrfToken;
288
+ // }
289
+ // }
290
+ const fetchOptions = {
291
+ method,
292
+ headers,
293
+ };
294
+ if (body && (method === 'POST' || method === 'PATCH')) {
295
+ fetchOptions.body = JSON.stringify(body);
296
+ }
297
+ try {
298
+ const response = await fetch(url, fetchOptions);
299
+ return response;
300
+ }
301
+ catch (error) {
302
+ if (error instanceof Error) {
303
+ throw new Error(`Request failed: ${error.message}`);
304
+ }
305
+ throw new Error(`Request failed: ${String(error)}`);
306
+ }
307
+ }
308
+ /**
309
+ * Parses error response from JSON:API.
310
+ *
311
+ * @param response - The HTTP response.
312
+ *
313
+ * @returns Promise resolving to the error data.
314
+ *
315
+ * @private
316
+ */
317
+ async parseErrorResponse(response) {
318
+ try {
319
+ const text = await response.text();
320
+ if (!text) {
321
+ return null;
322
+ }
323
+ return JSON.parse(text);
324
+ }
325
+ catch {
326
+ return null;
327
+ }
328
+ }
329
+ /**
330
+ * Creates an error from JSON:API error response.
331
+ *
332
+ * @param errorData - The error data.
333
+ * @param status - HTTP status code.
334
+ * @param statusText - HTTP status text.
335
+ *
336
+ * @returns Error instance.
337
+ *
338
+ * @private
339
+ */
340
+ createError(errorData, status, statusText) {
341
+ if (errorData && errorData.errors && errorData.errors.length > 0) {
342
+ const messages = errorData.errors.map((error) => {
343
+ const statusCode = error.status || String(status);
344
+ const message = error.detail || error.title || 'Unknown error';
345
+ return `[${statusCode}] ${message}`;
346
+ });
347
+ const error = new Error(messages.join('; '));
348
+ error.status = status;
349
+ error.errors = errorData.errors;
350
+ return error;
351
+ }
352
+ const error = new Error(`HTTP ${status} ${statusText}`);
353
+ error.status = status;
354
+ return error;
355
+ }
356
+ /**
357
+ * Builds filter query parameters from a filter object.
358
+ *
359
+ * @param filters - Filter conditions.
360
+ *
361
+ * @returns Query parameters for filters.
362
+ *
363
+ * @private
364
+ */
365
+ buildFilterParams(filters) {
366
+ const params = {};
367
+ let index = 0;
368
+ for (const [field, condition] of Object.entries(filters)) {
369
+ // Simple equality filter: { status: 'active' }
370
+ if (typeof condition === 'string' || typeof condition === 'number') {
371
+ params[`filter[${field}]`] = String(condition);
372
+ continue;
373
+ }
374
+ // Complex filter with operator: { created: { operator: '>', value: '2024-01-01' } }
375
+ if (condition && typeof condition === 'object' && 'operator' in condition) {
376
+ const filterKey = `filter[${index}]`;
377
+ params[`${filterKey}[condition][path]`] = field;
378
+ params[`${filterKey}[condition][operator]`] = condition.operator;
379
+ if ('value' in condition) {
380
+ const value = condition.value;
381
+ // Handle array values for IN, NOT IN operators
382
+ if (Array.isArray(value)) {
383
+ value.forEach((v, i) => {
384
+ params[`${filterKey}[condition][value][${i}]`] = String(v);
385
+ });
386
+ }
387
+ else {
388
+ params[`${filterKey}[condition][value]`] = String(value);
389
+ }
390
+ }
391
+ index++;
392
+ }
393
+ }
394
+ return params;
395
+ }
396
+ /**
397
+ * Formats relationships for JSON:API format.
398
+ *
399
+ * @param relationships - Relationships input.
400
+ *
401
+ * @returns Formatted relationships.
402
+ *
403
+ * @private
404
+ */
405
+ formatRelationships(relationships) {
406
+ const formatted = {};
407
+ for (const [fieldName, relationship] of Object.entries(relationships)) {
408
+ // Single relationship: { owner: { type: 'user--user', id: 'user-uuid' } }
409
+ if (relationship && typeof relationship === 'object' && 'type' in relationship && 'id' in relationship) {
410
+ formatted[fieldName] = {
411
+ data: {
412
+ type: relationship.type,
413
+ id: relationship.id,
414
+ },
415
+ };
416
+ }
417
+ // Multiple relationships: { tags: [{ type: 'taxonomy_term--tags', id: 'tag-uuid-1' }, ...] }
418
+ else if (Array.isArray(relationship)) {
419
+ formatted[fieldName] = {
420
+ data: relationship.map((item) => ({
421
+ type: item.type,
422
+ id: item.id,
423
+ })),
424
+ };
425
+ }
426
+ }
427
+ return formatted;
428
+ }
429
+ }