@js4cytoscape/ndex-client 0.5.0-alpha.9 → 0.6.0-alpha.2

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.
@@ -0,0 +1,338 @@
1
+ import axios, { AxiosInstance } from 'axios';
2
+ import {
3
+ CyNDExConfig,
4
+ CyNDExAuthConfig,
5
+ NDExImportParams,
6
+ NDExExportParams,
7
+ CytoscapeNetworkSummary,
8
+ CyNDExStatusResponse,
9
+ CX2ImportParams
10
+ } from '../types/cytoscape';
11
+
12
+ /**
13
+ * CyNDEx Service - Cytoscape-NDEx Bridge
14
+ *
15
+ * Provides seamless integration between Cytoscape desktop application and NDEx,
16
+ * enabling import/export of networks with support for CX and CX2 formats.
17
+ *
18
+ * This service communicates with Cytoscape via its REST API and handles NDEx
19
+ * authentication parameters that are passed to Cytoscape for NDEx operations.
20
+ *
21
+ * @example
22
+ * ```typescript
23
+ * // Basic usage
24
+ * const cyNDEx = new CyNDExService();
25
+ *
26
+ * // Configure NDEx server
27
+ * cyNDEx.setNDExBaseURL('https://www.ndexbio.org');
28
+ *
29
+ * // Set authentication
30
+ * cyNDEx.setBasicAuth('username', 'password');
31
+ * // or
32
+ * cyNDEx.setAuthToken('oauth-id-token');
33
+ *
34
+ * // Import network from NDEx to Cytoscape
35
+ * await cyNDEx.postNDExNetworkToCytoscape('network-uuid', 'access-key');
36
+ *
37
+ * // Export network from Cytoscape to NDEx
38
+ * await cyNDEx.postCytoscapeNetworkToNDEx('current');
39
+ *
40
+ * // Import CX2 data to Cytoscape
41
+ * await cyNDEx.postCX2NetworkToCytoscape(cx2String, 'My Network', 'My Collection');
42
+ * ```
43
+ */
44
+ export class CyNDExService {
45
+ private _port: number;
46
+ private cyRestBaseURL: string;
47
+ private ndexBaseURL: string;
48
+ private authConfig?: CyNDExAuthConfig;
49
+ private axiosInstance: AxiosInstance;
50
+
51
+ /**
52
+ * Create a new CyNDEx service instance
53
+ *
54
+ * @param port - Port number for Cytoscape REST API (default: 1234)
55
+ * @param config - Optional configuration object
56
+ */
57
+ constructor(port: number = 1234, config?: CyNDExConfig) {
58
+ this._port = port;
59
+ this.cyRestBaseURL = config?.cyRestBaseURL || 'http://127.0.0.1';
60
+ this.ndexBaseURL = config?.ndexBaseURL || 'https://www.ndexbio.org';
61
+
62
+ // Create axios instance for Cytoscape REST API calls
63
+ this.axiosInstance = axios.create({
64
+ baseURL: this.cyRestURL(),
65
+ timeout: 30000,
66
+ headers: {
67
+ 'Content-Type': 'application/json'
68
+ }
69
+ });
70
+ }
71
+
72
+ /**
73
+ * Get the port number for Cytoscape REST API
74
+ */
75
+ get port(): number {
76
+ return this._port;
77
+ }
78
+
79
+ /**
80
+ * Get the base URL for Cytoscape REST API
81
+ */
82
+ static get cyRestBaseURL(): string {
83
+ return 'http://127.0.0.1';
84
+ }
85
+
86
+ /**
87
+ * Set NDEx server base URL
88
+ *
89
+ * @param ndexBaseURL - Complete base URL for NDEx server (e.g., 'https://www.ndexbio.org')
90
+ * Must include protocol (https:// or http://)
91
+ *
92
+ * @example
93
+ * ```typescript
94
+ * cyNDEx.setNDExBaseURL('https://www.ndexbio.org');
95
+ * cyNDEx.setNDExBaseURL('https://test.ndexbio.org');
96
+ * ```
97
+ */
98
+ setNDExBaseURL(ndexBaseURL: string): void {
99
+ if (ndexBaseURL?.trim()) {
100
+ this.ndexBaseURL = ndexBaseURL.trim();
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Get the current NDEx server base URL
106
+ *
107
+ * @returns Complete NDEx server base URL
108
+ */
109
+ getNDExBaseURL(): string {
110
+ return this.ndexBaseURL;
111
+ }
112
+
113
+
114
+ /**
115
+ * Set basic authentication credentials for NDEx operations
116
+ *
117
+ * @param username - NDEx username
118
+ * @param password - NDEx password
119
+ */
120
+ setBasicAuth(username: string, password: string): void {
121
+ if (username?.trim() && password) {
122
+ this.authConfig = {
123
+ type: 'basic',
124
+ username: username.trim(),
125
+ password: password
126
+ };
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Set OAuth ID token for NDEx operations
132
+ *
133
+ * @param idToken - OAuth ID token from authentication provider
134
+ */
135
+ setAuthToken(idToken: string): void {
136
+ if (idToken?.trim()) {
137
+ this.authConfig = {
138
+ type: 'oauth',
139
+ idToken: idToken.trim()
140
+ };
141
+ }
142
+ }
143
+
144
+ /**
145
+ * Clear authentication credentials
146
+ */
147
+ clearAuth(): void {
148
+ this.authConfig = undefined;
149
+ }
150
+
151
+ /**
152
+ * Get the complete Cytoscape REST API URL
153
+ *
154
+ * @returns Complete URL for Cytoscape REST API
155
+ */
156
+ cyRestURL(): string {
157
+ return `${this.cyRestBaseURL}:${this._port}`;
158
+ }
159
+
160
+ /**
161
+ * Get authorization fields for NDEx operations
162
+ * These are passed as parameters to Cytoscape for NDEx authentication
163
+ *
164
+ * @private
165
+ * @returns Authorization parameters object
166
+ */
167
+ private getAuthFields(): Record<string, string> {
168
+ if (!this.authConfig) return {};
169
+
170
+ switch (this.authConfig.type) {
171
+ case 'basic':
172
+ return {
173
+ username: this.authConfig.username!,
174
+ password: this.authConfig.password!
175
+ };
176
+ case 'oauth':
177
+ return {
178
+ idToken: this.authConfig.idToken!
179
+ };
180
+ default:
181
+ return {};
182
+ }
183
+ }
184
+
185
+ /**
186
+ * Perform HTTP GET request to Cytoscape REST API
187
+ *
188
+ * @private
189
+ * @param url - Endpoint URL (relative to base URL)
190
+ * @returns Promise resolving to response data
191
+ */
192
+ private async _httpGet<T = any>(url: string): Promise<T> {
193
+ const response = await this.axiosInstance.get<T>(url);
194
+ return response.data;
195
+ }
196
+
197
+ /**
198
+ * Perform HTTP POST request to Cytoscape REST API
199
+ *
200
+ * @private
201
+ * @param url - Endpoint URL (relative to base URL)
202
+ * @param params - Optional query parameters
203
+ * @param data - Optional request body data
204
+ * @returns Promise resolving to response data
205
+ */
206
+ private async _httpPost<T = any>(url: string, params?: any, data?: any): Promise<T> {
207
+ const response = await this.axiosInstance.post<T>(url, data, { params });
208
+ return response.data;
209
+ }
210
+
211
+ /**
212
+ * Perform HTTP PUT request to Cytoscape REST API
213
+ *
214
+ * @private
215
+ * @param url - Endpoint URL (relative to base URL)
216
+ * @param params - Optional query parameters
217
+ * @param data - Optional request body data
218
+ * @returns Promise resolving to response data
219
+ */
220
+ private async _httpPut<T = any>(url: string, params?: any, data?: any): Promise<T> {
221
+ const response = await this.axiosInstance.put<T>(url, data, { params });
222
+ return response.data;
223
+ }
224
+
225
+ // =============================================================================
226
+ // Public API Methods - Maintain exact same signatures as original CyNDEx.js
227
+ // =============================================================================
228
+
229
+ /**
230
+ * Get CyNDEx plugin status from Cytoscape
231
+ *
232
+ * @returns Promise resolving to CyNDEx status information
233
+ */
234
+ async getCyNDExStatus(): Promise<CyNDExStatusResponse> {
235
+ return this._httpGet<CyNDExStatusResponse>('/cyndex2/v1');
236
+ }
237
+
238
+ /**
239
+ * Get network summary information from Cytoscape
240
+ *
241
+ * @param suid - Network SUID or 'current' for current network (default: 'current')
242
+ * @returns Promise resolving to network summary
243
+ */
244
+ async getCytoscapeNetworkSummary(suid: string = 'current'): Promise<CytoscapeNetworkSummary> {
245
+ return this._httpGet<CytoscapeNetworkSummary>(`/cyndex2/v1/networks/${suid}`);
246
+ }
247
+
248
+ /**
249
+ * Import network from NDEx to Cytoscape
250
+ *
251
+ * @param uuid - NDEx network UUID
252
+ * @param accessKey - Optional access key for private networks
253
+ * @param createView - Whether to create a network view (default: undefined)
254
+ * @returns Promise resolving to import result
255
+ */
256
+ async postNDExNetworkToCytoscape(
257
+ uuid: string,
258
+ accessKey?: string,
259
+ createView?: boolean
260
+ ): Promise<any> {
261
+ const importParams: NDExImportParams = {
262
+ serverUrl: `${this.ndexBaseURL}/v2`,
263
+ uuid: uuid,
264
+ accessKey: accessKey,
265
+ createView: createView,
266
+ ...this.getAuthFields()
267
+ };
268
+
269
+ return this._httpPost('/cyndex2/v1/networks', undefined, importParams);
270
+ }
271
+
272
+ /**
273
+ * Import CX network data to Cytoscape
274
+ *
275
+ * @param cx - CX format network data
276
+ * @returns Promise resolving to import result
277
+ */
278
+ async postCXNetworkToCytoscape(cx: any): Promise<any> {
279
+ return this._httpPost('/cyndex2/v1/networks/cx', undefined, cx);
280
+ }
281
+
282
+ /**
283
+ * Import CX2 network data to Cytoscape
284
+ *
285
+ * @param cx2_string - CX2 format network data as string
286
+ * @param title - Network title
287
+ * @param collection_name - Collection name
288
+ * @returns Promise resolving to import result
289
+ */
290
+ async postCX2NetworkToCytoscape(
291
+ cx2_string: string,
292
+ title: string,
293
+ collection_name: string
294
+ ): Promise<any> {
295
+ const params: CX2ImportParams = {
296
+ format: 'cx2',
297
+ collection: collection_name,
298
+ title: title
299
+ };
300
+
301
+ return this._httpPost('/v1/networks', params, cx2_string);
302
+ }
303
+
304
+ /**
305
+ * Export network from Cytoscape to NDEx (create new network)
306
+ *
307
+ * @param suid - Network SUID or 'current' for current network (default: 'current')
308
+ * @returns Promise resolving to export result
309
+ */
310
+ async postCytoscapeNetworkToNDEx(suid: string = 'current'): Promise<any> {
311
+ const saveParams: NDExExportParams = {
312
+ serverUrl: `${this.ndexBaseURL}/v2`,
313
+ ...this.getAuthFields()
314
+ };
315
+
316
+ return this._httpPost(`/cyndex2/v1/networks/${suid}`, undefined, saveParams);
317
+ }
318
+
319
+ /**
320
+ * Update existing network in NDEx with data from Cytoscape
321
+ *
322
+ * @param suid - Network SUID or 'current' for current network (default: 'current')
323
+ * @param uuid - Target NDEx network UUID to update
324
+ * @returns Promise resolving to update result
325
+ */
326
+ async putCytoscapeNetworkInNDEx(suid: string = 'current', uuid: string): Promise<any> {
327
+ const saveParams: NDExExportParams = {
328
+ serverUrl: `${this.ndexBaseURL}/v2`,
329
+ uuid: uuid,
330
+ ...this.getAuthFields()
331
+ };
332
+
333
+ return this._httpPut(`/cyndex2/v1/networks/${suid}`, undefined, saveParams);
334
+ }
335
+ }
336
+
337
+ // Export both as named export and default for backward compatibility
338
+ export default CyNDExService;
@@ -0,0 +1,234 @@
1
+ import { HTTPService } from './HTTPService';
2
+
3
+ // Type definitions for better type safety
4
+ interface ShareData {
5
+ files: Record<string, 'NETWORK' | 'FOLDER' | 'SHORTCUT'>;
6
+ }
7
+
8
+ interface MemberData {
9
+ members: Record<string, 'READ' | 'WRITE'>;
10
+ }
11
+
12
+ interface UpdateMemberRequest {
13
+ files: ShareData['files'];
14
+ members: MemberData['members'];
15
+ }
16
+
17
+ interface TransferOwnershipRequest {
18
+ files: ShareData['files'];
19
+ new_owner: string;
20
+ }
21
+
22
+ /**
23
+ * FilesService - NDEx file operations and task management
24
+ * Handles file uploads, downloads, exports, and asynchronous task tracking
25
+ */
26
+ export class FilesService {
27
+ constructor(private http: HTTPService) {}
28
+
29
+ /**
30
+ * Copy a file to a different location
31
+ * @param fromUuid - Source file UUID
32
+ * @param toPath - Target path
33
+ * @param type - File type (NETWORK, FOLDER, SHORTCUT)
34
+ * @param accessKey - Optional access key for protected files
35
+ */
36
+ copyFile(fromUuid: string, toPath: string, type: string, accessKey?: string): Promise<any> {
37
+ let parameters: Record<string, any> = {};
38
+
39
+ if (accessKey !== undefined) {
40
+ parameters['accesskey'] = accessKey;
41
+ }
42
+ return this.http.post('files/copy', {from_uuid: fromUuid, type: type, to_path: toPath}, {params: parameters, version: 'v3'});
43
+ }
44
+
45
+ /** Get file count statistics for the current user */
46
+ getCount(): Promise<any> {
47
+ return this.http.get('files/count', {version: 'v3'});
48
+ }
49
+
50
+ /** Get files in the trash for the current user */
51
+ getTrash(): Promise<any> {
52
+ return this.http.get('files/trash', {version: 'v3'});
53
+ }
54
+
55
+ /** Permanently delete all files in trash */
56
+ emptyTrash(): Promise<any> {
57
+ return this.http.delete('files/trash', {version: 'v3'});
58
+ }
59
+
60
+ /**
61
+ * Permanently delete a file from trash
62
+ * @param fileId - File UUID to permanently delete
63
+ */
64
+ permanentlyDeleteFile(fileId: string): Promise<any> {
65
+ return this.http.delete(`files/trash/${fileId}`, { version: 'v3' });
66
+ }
67
+
68
+ /**
69
+ * Restore files from trash
70
+ * @param networkIds - Array of network UUIDs to restore
71
+ * @param folderIds - Array of folder UUIDs to restore
72
+ * @param shortcutIds - Array of shortcut UUIDs to restore
73
+ */
74
+ restoreFile(networkIds: string[], folderIds: string[], shortcutIds: string[]): Promise<any> {
75
+ return this.http.post('files/trash/restore', {networks: networkIds, folders: folderIds, shortcuts: shortcutIds}, {version: 'v3'});
76
+ }
77
+
78
+ private _validateShareData(data: any): void {
79
+ // Check if data is an object and has files property
80
+ if (typeof data !== 'object' || data === null || data.files === undefined) {
81
+ throw new Error('Data must be an object with a "files" property');
82
+ }
83
+
84
+ // Check if files is an object
85
+ if (typeof data.files !== 'object' || data.files === null) {
86
+ throw new Error('The "files" property must be an object');
87
+ }
88
+
89
+ // Check each key-value pair in files
90
+ const validValues = ['NETWORK', 'FOLDER', 'SHORTCUT'];
91
+
92
+ for (const [uuid, fileType] of Object.entries(data.files)) {
93
+ // Validate UUID format (basic validation)
94
+ if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(uuid)) {
95
+ throw new Error(`Invalid UUID format: ${uuid}`);
96
+ }
97
+
98
+ // Validate file type
99
+ if (!validValues.includes(fileType as string)) {
100
+ throw new Error(`Invalid file type for ${uuid}: ${fileType}. Must be one of: ${validValues.join(', ')}`);
101
+ }
102
+ }
103
+ }
104
+
105
+ private _validateMemberData(data: any): void {
106
+ if (typeof data !== 'object' || data === null || data.members === undefined) {
107
+ throw new Error('Data must be an object with a "members" property');
108
+ }
109
+ const validValues = ['READ', 'WRITE'];
110
+ for (const [uuid, permission] of Object.entries(data.members)) {
111
+ if (!validValues.includes(permission as string)) {
112
+ throw new Error(`Invalid permission for ${uuid}: ${permission}. Must be one of: ${validValues.join(', ')}`);
113
+ }
114
+ if (typeof uuid !== 'string' || !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(uuid)) {
115
+ throw new Error(`Invalid UUID format: ${uuid}`);
116
+ }
117
+ }
118
+ }
119
+
120
+ updateMember(files: ShareData['files'], members: MemberData['members']): Promise<any> {
121
+ this._validateShareData({ files });
122
+ this._validateMemberData({ members });
123
+ return this.http.post('files/sharing/members', { files, members }, { version: 'v3' });
124
+ }
125
+
126
+ listMembers(files: any): Promise<any> {
127
+ return this.http.get('files/sharing/members/list', {params: files, version: 'v3'});
128
+ }
129
+
130
+ transferOwnership(files: ShareData['files'], newOwner: string): Promise<any> {
131
+ this._validateShareData({ files });
132
+ return this.http.post('files/sharing/transfer_ownership', { files, new_owner: newOwner }, { version: 'v3' });
133
+ }
134
+
135
+ listShares(limit?: number): Promise<any> {
136
+ const parameters: Record<string, any> = {};
137
+ if (limit !== undefined) {
138
+ parameters['limit'] = limit;
139
+ }
140
+ return this.http.post('files/sharing/list', parameters, { version: 'v3' });
141
+ }
142
+
143
+ share(files: ShareData['files']): Promise<any> {
144
+ this._validateShareData({ files });
145
+ return this.http.post('files/sharing/share', { files }, { version: 'v3' });
146
+ }
147
+
148
+ unshare(files: ShareData['files']): Promise<any> {
149
+ this._validateShareData({ files });
150
+ return this.http.post('files/sharing/unshare', { files }, { version: 'v3' });
151
+ }
152
+
153
+ // Folder operations
154
+ getFolders(limit?: number): Promise<any> {
155
+ let parameters: Record<string, any> = {};
156
+
157
+ if (limit !== undefined) {
158
+ parameters['limit'] = limit;
159
+ }
160
+ return this.http.get('files/folders', {params: parameters, version: 'v3'});
161
+ }
162
+
163
+ createFolder(name: string, parentFolderId?: string): Promise<any> {
164
+ return this.http.post('files/folders', { name: name, parent: parentFolderId }, { version: 'v3' });
165
+ }
166
+
167
+ getFolder(folderId: string, accessKey?: string): Promise<any> {
168
+ const parameters: Record<string, any> = {};
169
+ if (accessKey !== undefined) {
170
+ parameters['accesskey'] = accessKey;
171
+ }
172
+ return this.http.get(`files/folders/${folderId}`, { params: parameters, version: 'v3' });
173
+ }
174
+
175
+ updateFolder(folderId: string, name: string, parentFolderId?: string): Promise<any> {
176
+ return this.http.put(`files/folders/${folderId}`, { name: name, parent: parentFolderId }, { version: 'v3' });
177
+ }
178
+
179
+ deleteFolder(folderId: string): Promise<any> {
180
+ return this.http.delete(`files/folders/${folderId}`, { version: 'v3' });
181
+ }
182
+
183
+ getFolderCount(folderId: string, accessKey?: string): Promise<any> {
184
+ const parameters: Record<string, any> = {};
185
+ if (accessKey !== undefined) {
186
+ parameters['accesskey'] = accessKey;
187
+ }
188
+ return this.http.get(`files/folders/${folderId}/count`, { params: parameters, version: 'v3' });
189
+ }
190
+
191
+ getFolderList(folderId: string, accessKey?: string, format?: string, type?: string): Promise<any> {
192
+ const parameters: Record<string, any> = {};
193
+ if (accessKey !== undefined) {
194
+ parameters['accesskey'] = accessKey;
195
+ }
196
+ if (format !== undefined) {
197
+ parameters['format'] = format;
198
+ }
199
+ if (type !== undefined) {
200
+ parameters['type'] = type;
201
+ }
202
+ return this.http.get(`files/folders/${folderId}/list`, { params: parameters, version: 'v3' });
203
+ }
204
+
205
+ // Shortcut operations
206
+ getShortcuts(limit?: number): Promise<any> {
207
+ let parameters: Record<string, any> = {};
208
+
209
+ if (limit !== undefined) {
210
+ parameters['limit'] = limit;
211
+ }
212
+ return this.http.get('files/shortcuts', {params: parameters, version: 'v3'});
213
+ }
214
+
215
+ createShortcut(name: string, parentFolderId?: string, targetId?: string, targetType?: string): Promise<any> {
216
+ return this.http.post('files/shortcuts', { name: name, parent: parentFolderId, target: targetId, targetType }, { version: 'v3' });
217
+ }
218
+
219
+ getShortcut(shortcutId: string, accessKey?: string): Promise<any> {
220
+ const parameters: Record<string, any> = {};
221
+ if (accessKey !== undefined) {
222
+ parameters['accesskey'] = accessKey;
223
+ }
224
+ return this.http.get(`files/shortcuts/${shortcutId}`, { params: parameters, version: 'v3' });
225
+ }
226
+
227
+ updateShortcut(shortcutId: string, name: string, parentFolderId?: string, targetId?: string, targetType?: string): Promise<any> {
228
+ return this.http.put(`files/shortcuts/${shortcutId}`, { name: name, parent: parentFolderId, target: targetId, targetType }, { version: 'v3' });
229
+ }
230
+
231
+ deleteShortcut(shortcutId: string): Promise<any> {
232
+ return this.http.delete(`files/shortcuts/${shortcutId}`, { version: 'v3' });
233
+ }
234
+ }