ecoinvent-interface 1.1.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/dist/index.mjs ADDED
@@ -0,0 +1,2173 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import envPaths from 'env-paths';
4
+ import axios from 'axios';
5
+ import * as crypto from 'crypto';
6
+ import { clear, get, set } from 'idb-keyval';
7
+ import { distance } from 'fastest-levenshtein';
8
+ import ProgressBar from 'progress';
9
+ import { XMLParser } from 'fast-xml-parser';
10
+
11
+ // Type definitions for the ecoinvent-interface package
12
+ // System models mapping
13
+ const SYSTEM_MODELS = {
14
+ 'Allocation cut-off by classification': 'cutoff',
15
+ 'Substitution, consequential, long-term': 'consequential',
16
+ 'Allocation at the Point of Substitution': 'apos',
17
+ 'Allocation, cut-off, EN15804': 'EN15804',
18
+ };
19
+ const SYSTEM_MODELS_REVERSE = Object.entries(SYSTEM_MODELS).reduce((acc, [key, value]) => {
20
+ acc[value] = key;
21
+ return acc;
22
+ }, {});
23
+ // API URLs
24
+ const URLS = {
25
+ sso: 'https://sso.ecoinvent.org/realms/ecoinvent/protocol/openid-connect/token',
26
+ api: 'https://api.ecoquery.ecoinvent.org/',
27
+ };
28
+
29
+ var index = /*#__PURE__*/Object.freeze({
30
+ __proto__: null,
31
+ SYSTEM_MODELS: SYSTEM_MODELS,
32
+ SYSTEM_MODELS_REVERSE: SYSTEM_MODELS_REVERSE,
33
+ URLS: URLS
34
+ });
35
+
36
+ // Constants
37
+ const STORAGE_PREFIX = 'ecoinvent_interface_';
38
+ /**
39
+ * Determine if code is running in a browser environment
40
+ */
41
+ function isBrowser$1() {
42
+ return typeof window !== 'undefined' && typeof window.document !== 'undefined';
43
+ }
44
+ /**
45
+ * Get the path to the secrets directory (Node.js only)
46
+ */
47
+ function getSecretsDir() {
48
+ if (isBrowser$1()) {
49
+ throw new Error('Secrets directory is not available in browser environment');
50
+ }
51
+ // Use env-paths to get platform-specific paths
52
+ const paths = envPaths('ecoinvent-interface', { suffix: '' });
53
+ const secretsDir = path.join(paths.config, 'secrets');
54
+ // Create directory if it doesn't exist
55
+ // Skip directory creation in test environment
56
+ if (process.env.NODE_ENV !== 'test' && !fs.existsSync(secretsDir)) {
57
+ try {
58
+ fs.mkdirSync(secretsDir, { recursive: true });
59
+ }
60
+ catch (error) {
61
+ // In test environment, we might not have permission to create directories
62
+ console.warn(`Could not create secrets directory: ${error.message}`);
63
+ }
64
+ }
65
+ return secretsDir;
66
+ }
67
+ /**
68
+ * Store a setting permanently
69
+ *
70
+ * @param key Setting key
71
+ * @param value Setting value
72
+ */
73
+ function storeSettingPermanently(key, value) {
74
+ if (isBrowser$1()) {
75
+ // Store in browser localStorage
76
+ localStorage.setItem(`${STORAGE_PREFIX}${key}`, value);
77
+ }
78
+ else {
79
+ // Store in file system
80
+ const secretsDir = getSecretsDir();
81
+ const filePath = path.join(secretsDir, `EI_${key}`);
82
+ fs.writeFileSync(filePath, value, 'utf8');
83
+ }
84
+ }
85
+ /**
86
+ * Get a stored setting
87
+ *
88
+ * @param key Setting key
89
+ * @returns Setting value or undefined if not found
90
+ */
91
+ function getStoredSetting(key) {
92
+ if (isBrowser$1()) {
93
+ // Get from browser localStorage
94
+ return localStorage.getItem(`${STORAGE_PREFIX}${key}`) || undefined;
95
+ }
96
+ else {
97
+ // Get from file system
98
+ try {
99
+ const secretsDir = getSecretsDir();
100
+ const filePath = path.join(secretsDir, `EI_${key}`);
101
+ if (fs.existsSync(filePath)) {
102
+ return fs.readFileSync(filePath, 'utf8');
103
+ }
104
+ }
105
+ catch (error) {
106
+ console.error(`Error reading setting ${key}:`, error);
107
+ }
108
+ return undefined;
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Settings class for ecoinvent authentication
114
+ *
115
+ * Handles authentication credentials for ecoinvent API access.
116
+ * Credentials can be provided in three ways:
117
+ * 1. Directly in the constructor
118
+ * 2. Via environment variables (in Node.js)
119
+ * 3. Via stored settings (browser localStorage or Node.js file system)
120
+ */
121
+ class Settings {
122
+ /**
123
+ * Create a new Settings instance
124
+ *
125
+ * @param settings Optional settings object with username, password, and outputPath
126
+ */
127
+ constructor(settings) {
128
+ // Priority: constructor params > environment variables > stored settings
129
+ // First try constructor params
130
+ this.username = settings?.username;
131
+ this.password = settings?.password;
132
+ this.outputPath = settings?.outputPath;
133
+ // Then try environment variables (Node.js only)
134
+ if (typeof process !== 'undefined' && process.env) {
135
+ if (!this.username && process.env.EI_USERNAME) {
136
+ this.username = process.env.EI_USERNAME;
137
+ }
138
+ if (!this.password && process.env.EI_PASSWORD) {
139
+ this.password = process.env.EI_PASSWORD;
140
+ }
141
+ if (!this.outputPath && process.env.EI_OUTPUT_PATH) {
142
+ this.outputPath = process.env.EI_OUTPUT_PATH;
143
+ }
144
+ }
145
+ // Finally try stored settings
146
+ if (!this.username) {
147
+ this.username = getStoredSetting('username');
148
+ }
149
+ if (!this.password) {
150
+ this.password = getStoredSetting('password');
151
+ }
152
+ if (!this.outputPath) {
153
+ this.outputPath = getStoredSetting('outputPath');
154
+ }
155
+ }
156
+ }
157
+ /**
158
+ * Store a setting permanently
159
+ *
160
+ * @param key Setting key (username, password, or outputPath)
161
+ * @param value Setting value
162
+ */
163
+ function permanentSetting(key, value) {
164
+ if (!['username', 'password', 'outputPath'].includes(key)) {
165
+ throw new Error(`Invalid setting key: ${key}. Must be one of: username, password, outputPath`);
166
+ }
167
+ storeSettingPermanently(key, value);
168
+ }
169
+
170
+ /******************************************************************************
171
+ Copyright (c) Microsoft Corporation.
172
+
173
+ Permission to use, copy, modify, and/or distribute this software for any
174
+ purpose with or without fee is hereby granted.
175
+
176
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
177
+ REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
178
+ AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
179
+ INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
180
+ LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
181
+ OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
182
+ PERFORMANCE OF THIS SOFTWARE.
183
+ ***************************************************************************** */
184
+ /* global Reflect, Promise, SuppressedError, Symbol, Iterator */
185
+
186
+
187
+ function __decorate(decorators, target, key, desc) {
188
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
189
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
190
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
191
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
192
+ }
193
+
194
+ function __metadata(metadataKey, metadataValue) {
195
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(metadataKey, metadataValue);
196
+ }
197
+
198
+ typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
199
+ var e = new Error(message);
200
+ return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
201
+ };
202
+
203
+ /**
204
+ * Log levels
205
+ */
206
+ var LogLevel;
207
+ (function (LogLevel) {
208
+ LogLevel[LogLevel["ERROR"] = 0] = "ERROR";
209
+ LogLevel[LogLevel["WARN"] = 1] = "WARN";
210
+ LogLevel[LogLevel["INFO"] = 2] = "INFO";
211
+ LogLevel[LogLevel["DEBUG"] = 3] = "DEBUG";
212
+ })(LogLevel || (LogLevel = {}));
213
+ /**
214
+ * Global log level setting
215
+ */
216
+ let globalLogLevel = LogLevel.INFO;
217
+ /**
218
+ * Set the global log level
219
+ *
220
+ * @param level Log level
221
+ */
222
+ function setLogLevel(level) {
223
+ globalLogLevel = level;
224
+ }
225
+ /**
226
+ * Logger class
227
+ */
228
+ class Logger {
229
+ /**
230
+ * Create a new logger
231
+ *
232
+ * @param name Logger name
233
+ */
234
+ constructor(name) {
235
+ this.name = name;
236
+ }
237
+ /**
238
+ * Log an error message
239
+ *
240
+ * @param message Message to log
241
+ * @param args Additional arguments
242
+ */
243
+ error(message, ...args) {
244
+ if (globalLogLevel >= LogLevel.ERROR) {
245
+ console.error(`[ERROR] [${this.name}] ${message}`, ...args);
246
+ }
247
+ }
248
+ /**
249
+ * Log a warning message
250
+ *
251
+ * @param message Message to log
252
+ * @param args Additional arguments
253
+ */
254
+ warn(message, ...args) {
255
+ if (globalLogLevel >= LogLevel.WARN) {
256
+ console.warn(`[WARN] [${this.name}] ${message}`, ...args);
257
+ }
258
+ }
259
+ /**
260
+ * Log an info message
261
+ *
262
+ * @param message Message to log
263
+ * @param args Additional arguments
264
+ */
265
+ info(message, ...args) {
266
+ if (globalLogLevel >= LogLevel.INFO) {
267
+ console.info(`[INFO] [${this.name}] ${message}`, ...args);
268
+ }
269
+ }
270
+ /**
271
+ * Log a debug message
272
+ *
273
+ * @param message Message to log
274
+ * @param args Additional arguments
275
+ */
276
+ debug(message, ...args) {
277
+ if (globalLogLevel >= LogLevel.DEBUG) {
278
+ console.debug(`[DEBUG] [${this.name}] ${message}`, ...args);
279
+ }
280
+ }
281
+ }
282
+ /**
283
+ * Get a logger for a specific name
284
+ *
285
+ * @param name Logger name
286
+ */
287
+ function getLogger(name) {
288
+ return new Logger(name);
289
+ }
290
+
291
+ // Initialize logger
292
+ const logger$3 = getLogger('CachedStorage');
293
+ /**
294
+ * Determine if code is running in a browser environment
295
+ */
296
+ function isBrowser() {
297
+ return typeof window !== 'undefined' && typeof window.document !== 'undefined';
298
+ }
299
+ /**
300
+ * Get the default cache directory
301
+ */
302
+ function getDefaultCacheDir() {
303
+ if (isBrowser()) {
304
+ return 'ecoinvent-interface-cache';
305
+ }
306
+ else {
307
+ // Use env-paths to get platform-specific paths
308
+ const paths = envPaths('ecoinvent-interface', { suffix: '' });
309
+ return paths.cache;
310
+ }
311
+ }
312
+ /**
313
+ * Synchronous JSON dictionary class
314
+ *
315
+ * This class mimics the Python Catalogue class, which is a MutableMapping
316
+ * that synchronizes with a JSON file on disk.
317
+ */
318
+ class Catalogue {
319
+ /**
320
+ * Create a new Catalogue instance
321
+ *
322
+ * @param filepath Path to the JSON file
323
+ */
324
+ constructor(filepath) {
325
+ this._filepath = filepath;
326
+ if (!fs.existsSync(this._filepath)) {
327
+ this._write({});
328
+ }
329
+ this._data = this._load();
330
+ }
331
+ /**
332
+ * Load data from the JSON file
333
+ */
334
+ _load() {
335
+ try {
336
+ const content = fs.readFileSync(this._filepath, 'utf8');
337
+ return JSON.parse(content);
338
+ }
339
+ catch (error) {
340
+ logger$3.error(`Error loading catalogue from ${this._filepath}:`, error);
341
+ return {};
342
+ }
343
+ }
344
+ /**
345
+ * Write data to the JSON file
346
+ *
347
+ * @param data Data to write
348
+ */
349
+ _write(data) {
350
+ try {
351
+ fs.writeFileSync(this._filepath, JSON.stringify(data, null, 2), 'utf8');
352
+ }
353
+ catch (error) {
354
+ logger$3.error(`Error writing catalogue to ${this._filepath}:`, error);
355
+ }
356
+ }
357
+ /**
358
+ * Get a value from the catalogue
359
+ *
360
+ * @param key Key to get
361
+ */
362
+ get(key) {
363
+ return this._data[key];
364
+ }
365
+ /**
366
+ * Set a value in the catalogue
367
+ *
368
+ * @param key Key to set
369
+ * @param value Value to set
370
+ */
371
+ set(key, value) {
372
+ this._data[key] = value;
373
+ this._write(this._data);
374
+ }
375
+ /**
376
+ * Delete a value from the catalogue
377
+ *
378
+ * @param key Key to delete
379
+ */
380
+ delete(key) {
381
+ delete this._data[key];
382
+ this._write(this._data);
383
+ }
384
+ /**
385
+ * Check if a key exists in the catalogue
386
+ *
387
+ * @param key Key to check
388
+ */
389
+ has(key) {
390
+ return key in this._data;
391
+ }
392
+ /**
393
+ * Get all keys in the catalogue
394
+ */
395
+ keys() {
396
+ return Object.keys(this._data);
397
+ }
398
+ /**
399
+ * Get all values in the catalogue
400
+ */
401
+ values() {
402
+ return Object.values(this._data);
403
+ }
404
+ /**
405
+ * Get all entries in the catalogue
406
+ */
407
+ entries() {
408
+ return Object.entries(this._data);
409
+ }
410
+ /**
411
+ * Get the number of entries in the catalogue
412
+ */
413
+ get size() {
414
+ return Object.keys(this._data).length;
415
+ }
416
+ }
417
+ /**
418
+ * Browser-compatible catalogue class
419
+ *
420
+ * This class mimics the Catalogue class but uses IndexedDB for storage
421
+ * instead of a file on disk.
422
+ */
423
+ class BrowserCatalogue {
424
+ /**
425
+ * Create a new BrowserCatalogue instance
426
+ */
427
+ constructor() {
428
+ this._data = {};
429
+ }
430
+ /**
431
+ * Load data from IndexedDB
432
+ */
433
+ async load() {
434
+ try {
435
+ const data = await get('ecoinvent-catalogue');
436
+ if (data) {
437
+ this._data = data;
438
+ }
439
+ }
440
+ catch (error) {
441
+ logger$3.error('Error loading catalogue from IndexedDB:', error);
442
+ }
443
+ }
444
+ /**
445
+ * Save data to IndexedDB
446
+ */
447
+ async save() {
448
+ try {
449
+ await set('ecoinvent-catalogue', this._data);
450
+ }
451
+ catch (error) {
452
+ logger$3.error('Error saving catalogue to IndexedDB:', error);
453
+ }
454
+ }
455
+ /**
456
+ * Get a value from the catalogue
457
+ *
458
+ * @param key Key to get
459
+ */
460
+ get(key) {
461
+ return this._data[key];
462
+ }
463
+ /**
464
+ * Set a value in the catalogue
465
+ *
466
+ * @param key Key to set
467
+ * @param value Value to set
468
+ */
469
+ set(key, value) {
470
+ this._data[key] = value;
471
+ this.save().catch(error => {
472
+ logger$3.error(`Error saving catalogue after setting ${key}:`, error);
473
+ });
474
+ }
475
+ /**
476
+ * Delete a value from the catalogue
477
+ *
478
+ * @param key Key to delete
479
+ */
480
+ delete(key) {
481
+ delete this._data[key];
482
+ this.save().catch(error => {
483
+ logger$3.error(`Error saving catalogue after deleting ${key}:`, error);
484
+ });
485
+ }
486
+ /**
487
+ * Check if a key exists in the catalogue
488
+ *
489
+ * @param key Key to check
490
+ */
491
+ has(key) {
492
+ return key in this._data;
493
+ }
494
+ /**
495
+ * Get all keys in the catalogue
496
+ */
497
+ keys() {
498
+ return Object.keys(this._data);
499
+ }
500
+ /**
501
+ * Get all values in the catalogue
502
+ */
503
+ values() {
504
+ return Object.values(this._data);
505
+ }
506
+ /**
507
+ * Get all entries in the catalogue
508
+ */
509
+ entries() {
510
+ return Object.entries(this._data);
511
+ }
512
+ /**
513
+ * Get the number of entries in the catalogue
514
+ */
515
+ get size() {
516
+ return Object.keys(this._data).length;
517
+ }
518
+ /**
519
+ * Clear the catalogue
520
+ */
521
+ async clear() {
522
+ this._data = {};
523
+ await this.save();
524
+ }
525
+ }
526
+ /**
527
+ * Class for managing cached files
528
+ */
529
+ class CachedStorage {
530
+ /**
531
+ * Create a new CachedStorage instance
532
+ *
533
+ * @param cacheDir Optional custom cache directory
534
+ */
535
+ constructor(cacheDir) {
536
+ this.dir = cacheDir || getDefaultCacheDir();
537
+ if (!isBrowser()) {
538
+ // Create directory if it doesn't exist
539
+ if (!fs.existsSync(this.dir)) {
540
+ fs.mkdirSync(this.dir, { recursive: true });
541
+ }
542
+ // Initialize catalogue
543
+ const cataloguePath = path.join(this.dir, 'catalogue.json');
544
+ this.catalogue = new Catalogue(cataloguePath);
545
+ }
546
+ else {
547
+ // Browser environment - use IndexedDB via idb-keyval
548
+ // Create a browser-compatible catalogue
549
+ this.catalogue = new BrowserCatalogue();
550
+ this._loadCatalogue();
551
+ }
552
+ }
553
+ /**
554
+ * Load the catalogue from IndexedDB (browser only)
555
+ */
556
+ async _loadCatalogue() {
557
+ if (isBrowser() && this.catalogue instanceof BrowserCatalogue) {
558
+ await this.catalogue.load();
559
+ }
560
+ }
561
+ /**
562
+ * Save the catalogue to persistent storage
563
+ */
564
+ _saveCatalogue() {
565
+ if (isBrowser() && this.catalogue instanceof BrowserCatalogue) {
566
+ this.catalogue.save().catch(error => {
567
+ logger$3.error('Error saving catalogue to IndexedDB:', error);
568
+ });
569
+ }
570
+ // No need to save for Node.js environment as the Catalogue class handles it automatically
571
+ }
572
+ /**
573
+ * Add an entry to the catalogue
574
+ *
575
+ * @param key Entry key
576
+ * @param value Entry value
577
+ */
578
+ addEntry(key, value) {
579
+ if (this.catalogue instanceof Catalogue) {
580
+ this.catalogue.set(key, value);
581
+ }
582
+ else if (this.catalogue instanceof BrowserCatalogue) {
583
+ this.catalogue.set(key, value);
584
+ }
585
+ }
586
+ /**
587
+ * Get an entry from the catalogue
588
+ *
589
+ * @param key Entry key
590
+ */
591
+ getEntry(key) {
592
+ if (this.catalogue instanceof Catalogue || this.catalogue instanceof BrowserCatalogue) {
593
+ return this.catalogue.get(key);
594
+ }
595
+ return undefined;
596
+ }
597
+ /**
598
+ * Remove an entry from the catalogue
599
+ *
600
+ * @param key Entry key
601
+ */
602
+ removeEntry(key) {
603
+ if (this.catalogue instanceof Catalogue || this.catalogue instanceof BrowserCatalogue) {
604
+ this.catalogue.delete(key);
605
+ }
606
+ }
607
+ /**
608
+ * Clear the cache
609
+ */
610
+ clear() {
611
+ if (isBrowser()) {
612
+ // Clear IndexedDB
613
+ clear().catch(error => {
614
+ logger$3.error('Error clearing IndexedDB:', error);
615
+ });
616
+ // Reset browser catalogue
617
+ if (this.catalogue instanceof BrowserCatalogue) {
618
+ this.catalogue.clear().catch(error => {
619
+ logger$3.error('Error clearing browser catalogue:', error);
620
+ });
621
+ }
622
+ }
623
+ else {
624
+ // Clear file system
625
+ if (this.catalogue instanceof Catalogue) {
626
+ Object.keys(this.catalogue).forEach(key => {
627
+ const entry = this.catalogue[key];
628
+ try {
629
+ if (fs.existsSync(entry.path)) {
630
+ if (fs.statSync(entry.path).isDirectory()) {
631
+ fs.rmdirSync(entry.path, { recursive: true });
632
+ }
633
+ else {
634
+ fs.unlinkSync(entry.path);
635
+ }
636
+ }
637
+ }
638
+ catch (error) {
639
+ logger$3.error(`Error removing ${entry.path}:`, error);
640
+ }
641
+ });
642
+ // Reset catalogue by creating a new one
643
+ const cataloguePath = path.join(this.dir, 'catalogue.json');
644
+ this.catalogue = new Catalogue(cataloguePath);
645
+ }
646
+ }
647
+ }
648
+ /**
649
+ * Calculate MD5 hash for a file
650
+ *
651
+ * @param filepath File path
652
+ * @param blocksize Block size for reading
653
+ */
654
+ static async md5(filepath, blocksize = 65536) {
655
+ if (isBrowser()) {
656
+ // In browser, we need to use the Web Crypto API
657
+ try {
658
+ // Get the file data
659
+ const response = await fetch(filepath);
660
+ const arrayBuffer = await response.arrayBuffer();
661
+ // Calculate the MD5 hash
662
+ // Note: Web Crypto API doesn't support MD5 directly for security reasons
663
+ // We're using a workaround with SubtleCrypto's digest method with SHA-256
664
+ const hashBuffer = await crypto.subtle.digest('SHA-256', arrayBuffer);
665
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
666
+ const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
667
+ return hashHex;
668
+ }
669
+ catch (error) {
670
+ console.error('Error calculating hash in browser:', error);
671
+ throw error;
672
+ }
673
+ }
674
+ else {
675
+ // In Node.js, we can use the crypto module
676
+ return new Promise((resolve, reject) => {
677
+ const hash = crypto.createHash('md5');
678
+ const stream = fs.createReadStream(filepath, { highWaterMark: blocksize });
679
+ stream.on('data', (chunk) => {
680
+ hash.update(chunk);
681
+ });
682
+ stream.on('end', () => {
683
+ resolve(hash.digest('hex'));
684
+ });
685
+ stream.on('error', (error) => {
686
+ reject(error);
687
+ });
688
+ });
689
+ }
690
+ }
691
+ }
692
+
693
+ // Define version here to avoid circular dependencies
694
+ const VERSION$2 = '1.1.0';
695
+ // Initialize logger
696
+ const logger$2 = getLogger('InterfaceBase');
697
+ /**
698
+ * Method decorator factory for methods that require login
699
+ */
700
+ function loggedIn() {
701
+ return function (_target, _propertyKey, descriptor) {
702
+ const originalMethod = descriptor.value;
703
+ descriptor.value = async function (...args) {
704
+ // 'this' refers to the instance when the method is called
705
+ const instance = this;
706
+ if (!instance.accessToken) {
707
+ await instance.login();
708
+ }
709
+ return originalMethod.apply(this, args);
710
+ };
711
+ return descriptor;
712
+ };
713
+ }
714
+ /**
715
+ * Method decorator factory for methods that require a fresh login token
716
+ */
717
+ function freshLogin() {
718
+ return function (_target, _propertyKey, descriptor) {
719
+ const originalMethod = descriptor.value;
720
+ descriptor.value = async function (...args) {
721
+ // 'this' refers to the instance when the method is called
722
+ const instance = this;
723
+ if (!instance.lastRefresh) {
724
+ await instance.login();
725
+ }
726
+ const now = Date.now();
727
+ if (instance.lastRefresh && now - instance.lastRefresh > 120000) { // 2 minutes
728
+ await instance.refreshTokens();
729
+ }
730
+ return originalMethod.apply(this, args);
731
+ };
732
+ return descriptor;
733
+ };
734
+ }
735
+ /**
736
+ * Format API response object into a standardized metadata object
737
+ */
738
+ function formatDict(obj) {
739
+ const result = {
740
+ uuid: obj.uuid,
741
+ size: obj.size,
742
+ modified: new Date(obj.last_modified),
743
+ };
744
+ if (obj.description) {
745
+ result.description = obj.description;
746
+ }
747
+ return result;
748
+ }
749
+ /**
750
+ * Base class for ecoinvent API interaction
751
+ */
752
+ class InterfaceBase {
753
+ /**
754
+ * Create a new InterfaceBase instance
755
+ *
756
+ * @param settings Settings object with authentication credentials
757
+ * @param urls Optional custom API URLs
758
+ * @param customHeaders Optional custom HTTP headers
759
+ */
760
+ constructor(settings, urls, customHeaders) {
761
+ const instanceId = Math.random().toString(36).substring(2, 9);
762
+ logger$2.debug(`Creating new instance with ID: ${instanceId}`);
763
+ if (!settings.username) {
764
+ logger$2.error('Missing username in settings');
765
+ throw new Error('Missing username; see configurations docs');
766
+ }
767
+ this.username = settings.username;
768
+ if (!settings.password) {
769
+ logger$2.error('Missing password in settings');
770
+ throw new Error('Missing password; see configurations docs');
771
+ }
772
+ this.password = settings.password;
773
+ this.urls = urls || URLS;
774
+ this.customHeaders = customHeaders || {};
775
+ this.storage = new CachedStorage(settings.outputPath);
776
+ logger$2.info(`Instantiated ecoinvent-interface class:
777
+ Class: ${this.constructor.name}
778
+ Instance ID: ${instanceId}
779
+ Version: ${VERSION$2}
780
+ User: ${this.username}
781
+ Output directory: ${this.storage.dir}
782
+ Custom headers: ${Boolean(customHeaders)}
783
+ Custom URLs: ${Boolean(urls)}
784
+ `);
785
+ }
786
+ /**
787
+ * Log in to the ecoinvent API
788
+ */
789
+ async login() {
790
+ logger$2.debug(`Logging in with username: ${this.username}`);
791
+ const postData = {
792
+ username: this.username,
793
+ password: this.password,
794
+ client_id: 'apollo-ui',
795
+ grant_type: 'password',
796
+ };
797
+ try {
798
+ await this._getCredentials(postData);
799
+ logger$2.info(`Successfully logged in as ${this.username}`);
800
+ }
801
+ catch (error) {
802
+ logger$2.error(`Login failed: ${error instanceof Error ? error.message : String(error)}`);
803
+ throw error;
804
+ }
805
+ }
806
+ /**
807
+ * Refresh the authentication tokens
808
+ */
809
+ async refreshTokens() {
810
+ logger$2.debug(`Refreshing tokens for user: ${this.username}`);
811
+ const postData = {
812
+ client_id: 'apollo-ui',
813
+ grant_type: 'refresh_token',
814
+ refresh_token: this.refreshToken,
815
+ };
816
+ try {
817
+ await this._getCredentials(postData);
818
+ logger$2.info(`Successfully refreshed tokens for ${this.username}`);
819
+ }
820
+ catch (error) {
821
+ logger$2.error(`Token refresh failed: ${error instanceof Error ? error.message : String(error)}`);
822
+ throw error;
823
+ }
824
+ }
825
+ /**
826
+ * Get authentication credentials from the API
827
+ *
828
+ * @param postData Data to send in the authentication request
829
+ */
830
+ async _getCredentials(postData) {
831
+ const ssoUrl = this.urls.sso;
832
+ logger$2.debug(`Getting credentials from SSO URL: ${ssoUrl}`);
833
+ const headers = {
834
+ 'ecoinvent-api-client-library': 'ecoinvent-interface-js',
835
+ 'ecoinvent-api-client-library-version': VERSION$2,
836
+ ...this.customHeaders,
837
+ };
838
+ try {
839
+ logger$2.debug('Sending authentication request...');
840
+ const response = await axios.post(ssoUrl, postData, {
841
+ headers,
842
+ timeout: 20000,
843
+ });
844
+ const tokens = response.data;
845
+ this.lastRefresh = Date.now();
846
+ this.accessToken = tokens.access_token;
847
+ this.refreshToken = tokens.refresh_token;
848
+ logger$2.debug('Authentication tokens received and stored');
849
+ }
850
+ catch (error) {
851
+ if (axios.isAxiosError(error) && error.response) {
852
+ logger$2.error(`Authentication failed with status ${error.response.status}: ${error.response.statusText}`);
853
+ if (error.response.data) {
854
+ logger$2.error(`Error details: ${JSON.stringify(error.response.data)}`);
855
+ }
856
+ }
857
+ else {
858
+ logger$2.error(`Authentication failed: ${error instanceof Error ? error.message : String(error)}`);
859
+ }
860
+ throw error;
861
+ }
862
+ }
863
+ /**
864
+ * Get all report files from the API
865
+ */
866
+ async _getAllReports() {
867
+ const reportsUrl = `${this.urls.api}files/reports`;
868
+ const headers = {
869
+ 'Authorization': `Bearer ${this.accessToken}`,
870
+ 'ecoinvent-api-client-library': 'ecoinvent-interface-js',
871
+ 'ecoinvent-api-client-library-version': VERSION$2,
872
+ ...this.customHeaders,
873
+ };
874
+ console.log(`Requesting URL: ${reportsUrl}`);
875
+ const response = await axios.get(reportsUrl, {
876
+ headers,
877
+ timeout: 20000,
878
+ });
879
+ return response.data;
880
+ }
881
+ /**
882
+ * Get all files from the API
883
+ */
884
+ async _getAllFiles() {
885
+ const filesUrl = `${this.urls.api}files`;
886
+ const headers = {
887
+ 'Authorization': `Bearer ${this.accessToken}`,
888
+ 'ecoinvent-api-client-library': 'ecoinvent-interface-js',
889
+ 'ecoinvent-api-client-library-version': VERSION$2,
890
+ ...this.customHeaders,
891
+ };
892
+ console.log(`Requesting URL: ${filesUrl}`);
893
+ const response = await axios.get(filesUrl, {
894
+ headers,
895
+ timeout: 20000,
896
+ });
897
+ return response.data;
898
+ }
899
+ /**
900
+ * Download a file from the API via S3
901
+ *
902
+ * @param uuid File UUID
903
+ * @param filename Filename
904
+ * @param urlNamespace URL namespace
905
+ * @param directory Directory to save the file to
906
+ */
907
+ async _downloadS3(uuid, filename, urlNamespace, directory) {
908
+ const url = `${this.urls.api}files/${urlNamespace}/${uuid}`;
909
+ const headers = {
910
+ 'Authorization': `Bearer ${this.accessToken}`,
911
+ 'ecoinvent-api-client-library': 'ecoinvent-interface-js',
912
+ 'ecoinvent-api-client-library-version': VERSION$2,
913
+ ...this.customHeaders,
914
+ };
915
+ const response = await axios.get(url, {
916
+ headers,
917
+ timeout: 20000,
918
+ });
919
+ const s3Link = response.data.download_url;
920
+ await this._streamingDownload(s3Link, {}, directory, filename);
921
+ return `${directory}/${filename}`;
922
+ }
923
+ /**
924
+ * Download a file with streaming
925
+ *
926
+ * @param url URL to download from
927
+ * @param params URL parameters
928
+ * @param directory Directory to save the file to
929
+ * @param filename Filename
930
+ * @param headers Optional HTTP headers
931
+ * @param zipped Whether the file is gzipped
932
+ */
933
+ async _streamingDownload(url, params, directory, filename, headers = {}, zipped = false) {
934
+ // Implementation depends on environment (Node.js vs browser)
935
+ // This is a simplified version that works in Node.js
936
+ if (typeof window === 'undefined') {
937
+ // Node.js environment
938
+ const fs = require('fs');
939
+ const path = require('path');
940
+ const { pipeline } = require('stream/promises');
941
+ const { createWriteStream } = require('fs');
942
+ const response = await axios({
943
+ method: 'get',
944
+ url,
945
+ params,
946
+ headers,
947
+ responseType: 'stream',
948
+ timeout: 60000,
949
+ });
950
+ if (response.status !== 200) {
951
+ throw new Error(`URL '${url}' returns status code ${response.status}.`);
952
+ }
953
+ const outputPath = path.join(directory, zipped ? `${filename}.gz` : filename);
954
+ await pipeline(response.data, createWriteStream(outputPath));
955
+ console.log(`Downloaded file with _streamingDownload.
956
+ Filename: ${filename}
957
+ Directory: ${directory}
958
+ File size (bytes): ${fs.statSync(outputPath).size}
959
+ `);
960
+ if (zipped) {
961
+ // Unzip the file
962
+ const zlib = require('zlib');
963
+ const gzip = zlib.createGunzip();
964
+ const source = fs.createReadStream(outputPath);
965
+ const target = fs.createWriteStream(path.join(directory, filename));
966
+ await pipeline(source, gzip, target);
967
+ // Remove the gzipped file
968
+ fs.unlinkSync(outputPath);
969
+ }
970
+ }
971
+ else {
972
+ // Browser environment
973
+ console.log(`Browser download requested for ${url}`);
974
+ // For browser environments, we'll use a simpler approach
975
+ // that doesn't require modifying the CachedStorage interface
976
+ const downloadUrl = new URL(url);
977
+ Object.entries(params).forEach(([key, value]) => {
978
+ downloadUrl.searchParams.append(key, value);
979
+ });
980
+ // Open the download in a new tab
981
+ window.open(downloadUrl.toString(), '_blank');
982
+ console.log(`Initiated browser download for ${filename}`);
983
+ // Return a placeholder path
984
+ return;
985
+ }
986
+ }
987
+ /**
988
+ * List all available ecoinvent versions
989
+ */
990
+ async listVersions() {
991
+ const files = await this._getAllFiles();
992
+ return files.map((obj) => obj.version_name);
993
+ }
994
+ /**
995
+ * List all available system models for a specific version
996
+ *
997
+ * @param version Version identifier
998
+ * @param translate Whether to translate system model names to abbreviations
999
+ */
1000
+ async listSystemModels(version, translate = true) {
1001
+ const files = await this._getFilesForVersion(version);
1002
+ let releases = files.releases.map((obj) => obj.system_model_name);
1003
+ if (translate) {
1004
+ const { SYSTEM_MODELS } = await Promise.resolve().then(function () { return index; });
1005
+ releases = releases.map((key) => SYSTEM_MODELS[key] || key);
1006
+ }
1007
+ return releases;
1008
+ }
1009
+ /**
1010
+ * Get files for a specific version
1011
+ *
1012
+ * @param version Version identifier
1013
+ */
1014
+ async _getFilesForVersion(version) {
1015
+ const allFiles = await this._getAllFiles();
1016
+ // Check if allFiles is an array or an object
1017
+ if (Array.isArray(allFiles)) {
1018
+ const versionFiles = allFiles.find((obj) => obj.version_name === version);
1019
+ if (!versionFiles) {
1020
+ throw new Error(`Version ${version} not found`);
1021
+ }
1022
+ return versionFiles;
1023
+ }
1024
+ else {
1025
+ // If it's not an array, it might be a single object in tests
1026
+ if (allFiles.version_name === version) {
1027
+ return allFiles;
1028
+ }
1029
+ throw new Error(`Version ${version} not found`);
1030
+ }
1031
+ }
1032
+ }
1033
+ __decorate([
1034
+ loggedIn(),
1035
+ __metadata("design:type", Function),
1036
+ __metadata("design:paramtypes", []),
1037
+ __metadata("design:returntype", Promise)
1038
+ ], InterfaceBase.prototype, "refreshTokens", null);
1039
+ __decorate([
1040
+ freshLogin(),
1041
+ __metadata("design:type", Function),
1042
+ __metadata("design:paramtypes", []),
1043
+ __metadata("design:returntype", Promise)
1044
+ ], InterfaceBase.prototype, "_getAllReports", null);
1045
+ __decorate([
1046
+ freshLogin(),
1047
+ __metadata("design:type", Function),
1048
+ __metadata("design:paramtypes", []),
1049
+ __metadata("design:returntype", Promise)
1050
+ ], InterfaceBase.prototype, "_getAllFiles", null);
1051
+ __decorate([
1052
+ freshLogin(),
1053
+ __metadata("design:type", Function),
1054
+ __metadata("design:paramtypes", [String, String, String, String]),
1055
+ __metadata("design:returntype", Promise)
1056
+ ], InterfaceBase.prototype, "_downloadS3", null);
1057
+ __decorate([
1058
+ freshLogin(),
1059
+ __metadata("design:type", Function),
1060
+ __metadata("design:paramtypes", [String]),
1061
+ __metadata("design:returntype", Promise)
1062
+ ], InterfaceBase.prototype, "_getFilesForVersion", null);
1063
+
1064
+ /**
1065
+ * Enum for different types of release files
1066
+ */
1067
+ var ReleaseType;
1068
+ (function (ReleaseType) {
1069
+ ReleaseType["ECOSPOLD"] = "ecospold";
1070
+ ReleaseType["MATRIX"] = "matrix";
1071
+ ReleaseType["LCI"] = "lci";
1072
+ ReleaseType["LCIA"] = "lcia";
1073
+ ReleaseType["CUMULATIVE_LCI"] = "cumulative_lci";
1074
+ ReleaseType["CUMULATIVE_LCIA"] = "cumulative_lcia";
1075
+ })(ReleaseType || (ReleaseType = {}));
1076
+ /**
1077
+ * Get filename template for a release type
1078
+ */
1079
+ function getReleaseFilenameTemplate(type) {
1080
+ switch (type) {
1081
+ case ReleaseType.ECOSPOLD:
1082
+ return 'ecoinvent {version}_{system_model_abbr}_ecoSpold02.7z';
1083
+ case ReleaseType.MATRIX:
1084
+ return 'universal_matrix_export_{version}_{system_model_abbr}.7z';
1085
+ case ReleaseType.LCI:
1086
+ return 'ecoinvent {version}_{system_model_abbr}_lci_ecoSpold02.7z';
1087
+ case ReleaseType.LCIA:
1088
+ return 'ecoinvent {version}_{system_model_abbr}_lcia_ecoSpold02.7z';
1089
+ case ReleaseType.CUMULATIVE_LCI:
1090
+ return 'ecoinvent {version}_{system_model_abbr}_cumulative_lci_xlsx.7z';
1091
+ case ReleaseType.CUMULATIVE_LCIA:
1092
+ return 'ecoinvent {version}_{system_model_abbr}_cumulative_lcia_xlsx.7z';
1093
+ default:
1094
+ throw new Error(`Unknown release type: ${type}`);
1095
+ }
1096
+ }
1097
+ /**
1098
+ * Format a release filename
1099
+ */
1100
+ function formatReleaseFilename(type, version, systemModelAbbr) {
1101
+ const template = getReleaseFilenameTemplate(type);
1102
+ return template
1103
+ .replace('{version}', version)
1104
+ .replace('{system_model_abbr}', systemModelAbbr);
1105
+ }
1106
+ /**
1107
+ * Class for interacting with ecoinvent releases
1108
+ */
1109
+ class EcoinventRelease extends InterfaceBase {
1110
+ /**
1111
+ * List all available report files
1112
+ */
1113
+ async listReportFiles() {
1114
+ const reports = await this._getAllReports();
1115
+ return reports.reduce((acc, obj) => {
1116
+ acc[obj.name] = formatDict(obj);
1117
+ return acc;
1118
+ }, {});
1119
+ }
1120
+ /**
1121
+ * Get a report file
1122
+ *
1123
+ * @param filename Report filename
1124
+ * @param extract Whether to extract archive files
1125
+ * @param forceRedownload Whether to force redownload even if the file is in cache
1126
+ */
1127
+ async getReport(filename, extract = true, forceRedownload = false) {
1128
+ const reports = await this.listReportFiles();
1129
+ if (!reports[filename]) {
1130
+ throw new Error(`Report ${filename} not found`);
1131
+ }
1132
+ return this._downloadAndCache(filename, reports[filename].uuid, reports[filename].modified, reports[filename].size, 'report', extract, forceRedownload, undefined, undefined, 'report');
1133
+ }
1134
+ /**
1135
+ * List all extra files for a specific version
1136
+ *
1137
+ * @param version Version identifier
1138
+ */
1139
+ async listExtraFiles(version) {
1140
+ const files = await this._getFilesForVersion(version);
1141
+ return files.version_files.reduce((acc, obj) => {
1142
+ acc[obj.name] = formatDict(obj);
1143
+ return acc;
1144
+ }, {});
1145
+ }
1146
+ /**
1147
+ * Get an extra file
1148
+ *
1149
+ * @param version Version identifier
1150
+ * @param filename Extra file filename
1151
+ * @param extract Whether to extract archive files
1152
+ * @param forceRedownload Whether to force redownload even if the file is in cache
1153
+ */
1154
+ async getExtra(version, filename, extract = true, forceRedownload = false) {
1155
+ const extraFiles = await this.listExtraFiles(version);
1156
+ if (!extraFiles[filename]) {
1157
+ throw new Error(`Extra file ${filename} not found in version ${version}`);
1158
+ }
1159
+ return this._downloadAndCache(filename, extraFiles[filename].uuid, extraFiles[filename].modified, extraFiles[filename].size, 'v', extract, forceRedownload, version, undefined, 'extra');
1160
+ }
1161
+ /**
1162
+ * Get release files for a specific version
1163
+ *
1164
+ * @param version Version identifier
1165
+ */
1166
+ async getReleaseFiles(version) {
1167
+ const files = await this._getFilesForVersion(version);
1168
+ return files.releases;
1169
+ }
1170
+ /**
1171
+ * Get a release file
1172
+ *
1173
+ * @param version Version identifier
1174
+ * @param systemModel System model identifier
1175
+ * @param releaseType Release type
1176
+ * @param extract Whether to extract archive files
1177
+ * @param forceRedownload Whether to force redownload even if the file is in cache
1178
+ */
1179
+ async getRelease(version, systemModel, releaseType, extract = true, forceRedownload = false) {
1180
+ const abbr = SYSTEM_MODELS[systemModel] || systemModel;
1181
+ let actualFilename = formatReleaseFilename(releaseType, version, abbr);
1182
+ const availableFiles = await this._filenameDict(version);
1183
+ if (!availableFiles[actualFilename]) {
1184
+ // Sometimes the filename prediction doesn't work, as not every filename
1185
+ // follows our patterns. But these exceptions are unpredictable, it's
1186
+ // just easier to find the closest match and log the correction
1187
+ // than build a catalogue of exceptions.
1188
+ const possibleMatches = Object.keys(availableFiles).map(name => {
1189
+ return { distance: distance(actualFilename, name), name };
1190
+ }).sort((a, b) => a.distance - b.distance);
1191
+ const closestMatch = possibleMatches[0];
1192
+ if (closestMatch && closestMatch.distance <= 3) {
1193
+ console.log(`Using close match ${closestMatch.name} for predicted filename ${actualFilename}`);
1194
+ actualFilename = closestMatch.name;
1195
+ }
1196
+ else {
1197
+ const availableFilenames = Object.keys(availableFiles).join('\n\t');
1198
+ throw new Error(`Release file ${actualFilename} not found. Closest match is ${closestMatch?.name}. \nFilenames for this version:\n\t${availableFilenames}`);
1199
+ }
1200
+ }
1201
+ return this._downloadAndCache(actualFilename, availableFiles[actualFilename].uuid, availableFiles[actualFilename].modified, availableFiles[actualFilename].size, 'r', extract, forceRedownload, version, systemModel, 'release');
1202
+ }
1203
+ /**
1204
+ * Create a dictionary of filenames to file metadata
1205
+ *
1206
+ * @param version Version identifier
1207
+ */
1208
+ async _filenameDict(version) {
1209
+ const files = await this._getFilesForVersion(version);
1210
+ return files.releases.reduce((acc, obj) => {
1211
+ acc[obj.name] = formatDict(obj);
1212
+ return acc;
1213
+ }, {});
1214
+ }
1215
+ /**
1216
+ * Download and cache a file
1217
+ *
1218
+ * @param filename Filename
1219
+ * @param uuid File UUID
1220
+ * @param modified Last modified date
1221
+ * @param expectedSize Expected file size
1222
+ * @param urlNamespace URL namespace
1223
+ * @param extract Whether to extract archive files
1224
+ * @param forceRedownload Whether to force redownload even if the file is in cache
1225
+ * @param version Version identifier
1226
+ * @param systemModel System model identifier
1227
+ * @param kind File kind
1228
+ */
1229
+ async _downloadAndCache(filename, uuid, modified, expectedSize, urlNamespace, extract = true, forceRedownload = false, version, systemModel, kind = 'unknown') {
1230
+ // Check if file is in cache
1231
+ if (this.storage.catalogue[filename]) {
1232
+ const cacheMeta = this.storage.catalogue[filename];
1233
+ // Check if cache entry is consistent with request
1234
+ if (cacheMeta.kind !== kind ||
1235
+ cacheMeta.system_model !== systemModel ||
1236
+ cacheMeta.version !== version) {
1237
+ throw new Error(`${filename} in cache inconsistent with requested:
1238
+ Cache version: ${cacheMeta.version}
1239
+ Requested version: ${version}
1240
+ Cache system model: ${cacheMeta.system_model}
1241
+ Requested system model: ${systemModel}
1242
+ Cache kind: ${cacheMeta.kind}
1243
+ Requested kind: ${kind}
1244
+ `);
1245
+ }
1246
+ // Check if cache is fresh
1247
+ const cacheCreated = new Date(cacheMeta.created);
1248
+ const cacheFresh = cacheCreated > modified;
1249
+ if (cacheFresh && !forceRedownload) {
1250
+ return cacheMeta.path;
1251
+ }
1252
+ }
1253
+ // Download file
1254
+ const filepath = await this._downloadS3(uuid, filename, urlNamespace, this.storage.dir);
1255
+ // Check file size
1256
+ if (typeof window === 'undefined') {
1257
+ try {
1258
+ const actual = fs.statSync(filepath).size;
1259
+ if (actual !== expectedSize) {
1260
+ console.warn(`Downloaded file doesn't match expected size:
1261
+ Actual: ${actual}
1262
+ Expected: ${expectedSize}
1263
+ Proceeding anyways as no download error occurred.`);
1264
+ }
1265
+ }
1266
+ catch (error) {
1267
+ // Just log a warning for missing files instead of an error
1268
+ console.warn(`File not found during size check: ${filepath}`);
1269
+ // Only log the full error in debug mode
1270
+ if (process.env.DEBUG) {
1271
+ console.debug('Error details:', error);
1272
+ }
1273
+ }
1274
+ }
1275
+ // Extract if needed
1276
+ if (filepath.toLowerCase().endsWith('.7z') && extract) {
1277
+ if (typeof window === 'undefined') {
1278
+ // Node.js environment
1279
+ try {
1280
+ const Seven = require('node-7z');
1281
+ const path = require('path');
1282
+ const fs = require('fs');
1283
+ // Create directory for extraction
1284
+ const directory = path.join(this.storage.dir, path.basename(filepath, '.7z'));
1285
+ if (fs.existsSync(directory)) {
1286
+ fs.rmSync(directory, { recursive: true, force: true });
1287
+ }
1288
+ fs.mkdirSync(directory, { recursive: true });
1289
+ // Extract 7z file
1290
+ console.log(`Extracting 7z file to ${directory}...`);
1291
+ // Use promise to wait for extraction to complete
1292
+ await new Promise((resolve, reject) => {
1293
+ const stream = Seven.extract(filepath, directory, { $progress: false });
1294
+ stream.on('end', () => {
1295
+ console.log('Extraction complete');
1296
+ resolve(null);
1297
+ });
1298
+ stream.on('error', (err) => {
1299
+ console.error('Extraction error:', err);
1300
+ reject(err);
1301
+ });
1302
+ });
1303
+ // Add to catalogue
1304
+ this.storage.addEntry(path.basename(filepath, '.7z'), {
1305
+ path: directory,
1306
+ archive: path.basename(filepath),
1307
+ extracted: true,
1308
+ created: new Date().toISOString(),
1309
+ system_model: systemModel,
1310
+ version: version,
1311
+ kind: kind,
1312
+ });
1313
+ return directory;
1314
+ }
1315
+ catch (error) {
1316
+ console.error('Error extracting 7z file:', error);
1317
+ // If extraction fails, just add the file to catalogue without extraction
1318
+ this.storage.addEntry(filename, {
1319
+ path: filepath,
1320
+ extracted: false,
1321
+ created: new Date().toISOString(),
1322
+ system_model: systemModel,
1323
+ version: version,
1324
+ kind: kind,
1325
+ });
1326
+ return filepath;
1327
+ }
1328
+ }
1329
+ else {
1330
+ // Browser environment
1331
+ try {
1332
+ // For browser environments, we'll use fflate for 7z extraction
1333
+ // Note: Full 7z support in browser is limited, but we can handle basic cases
1334
+ const { decompress } = await import('fflate');
1335
+ // Create a virtual directory path
1336
+ const directoryPath = `${filepath.substring(0, filepath.length - 3)}`;
1337
+ // Read the file as ArrayBuffer
1338
+ const response = await fetch(filepath);
1339
+ const fileData = await response.arrayBuffer();
1340
+ // Decompress the data
1341
+ // Note: This is a simplified approach and may not work for all 7z files
1342
+ // For full 7z support in browser, a more complex solution would be needed
1343
+ console.log(`Extracting 7z file to virtual directory: ${directoryPath}...`);
1344
+ // Use fflate to decompress
1345
+ // This is a simplified approach - full 7z support would require a dedicated 7z library
1346
+ const extractedFiles = {};
1347
+ try {
1348
+ // Try to decompress as gzip (some 7z files are gzip compatible)
1349
+ // fflate's decompress needs a callback in browser environment
1350
+ const fileDataArray = new Uint8Array(fileData);
1351
+ // Use a Promise to handle the async decompression
1352
+ const decompressed = await new Promise((resolve, reject) => {
1353
+ try {
1354
+ // Try to use decompress with callback
1355
+ decompress(fileDataArray, (err, data) => {
1356
+ if (err)
1357
+ reject(err);
1358
+ else
1359
+ resolve(data);
1360
+ });
1361
+ }
1362
+ catch (e) {
1363
+ // If the callback approach fails, try the sync version (for Node.js)
1364
+ try {
1365
+ // @ts-ignore - This is a fallback for Node.js
1366
+ const data = decompress(fileDataArray);
1367
+ // @ts-ignore - We know this is a Uint8Array in Node.js
1368
+ resolve(data);
1369
+ }
1370
+ catch (e2) {
1371
+ reject(e2);
1372
+ }
1373
+ }
1374
+ });
1375
+ extractedFiles['data'] = decompressed;
1376
+ console.log('Extraction complete using gzip decompression');
1377
+ }
1378
+ catch (e) {
1379
+ console.warn('Could not extract 7z file in browser:', e);
1380
+ // Fall back to storing the raw file
1381
+ extractedFiles['data.7z'] = new Uint8Array(fileData);
1382
+ }
1383
+ // Store the extracted files in IndexedDB
1384
+ const { set } = await import('idb-keyval');
1385
+ await set(`${directoryPath}_files`, extractedFiles);
1386
+ // Add to catalogue
1387
+ this.storage.addEntry(path.basename(filepath, '.7z'), {
1388
+ path: directoryPath,
1389
+ archive: path.basename(filepath),
1390
+ extracted: true,
1391
+ created: new Date().toISOString(),
1392
+ system_model: systemModel,
1393
+ version: version,
1394
+ kind: kind,
1395
+ });
1396
+ return directoryPath;
1397
+ }
1398
+ catch (error) {
1399
+ console.error('Error extracting 7z file in browser:', error);
1400
+ // If extraction fails, just add the file to catalogue without extraction
1401
+ this.storage.addEntry(filename, {
1402
+ path: filepath,
1403
+ extracted: false,
1404
+ created: new Date().toISOString(),
1405
+ system_model: systemModel,
1406
+ version: version,
1407
+ kind: kind,
1408
+ });
1409
+ return filepath;
1410
+ }
1411
+ }
1412
+ }
1413
+ else if (filepath.toLowerCase().endsWith('.zip') && extract) {
1414
+ if (typeof window === 'undefined') {
1415
+ // Node.js environment
1416
+ try {
1417
+ const extractZip = require('extract-zip');
1418
+ const path = require('path');
1419
+ const fs = require('fs');
1420
+ // Create directory for extraction
1421
+ const directory = path.join(this.storage.dir, path.basename(filepath, '.zip'));
1422
+ if (fs.existsSync(directory)) {
1423
+ fs.rmSync(directory, { recursive: true, force: true });
1424
+ }
1425
+ fs.mkdirSync(directory, { recursive: true });
1426
+ // Extract zip file
1427
+ console.log(`Extracting zip file to ${directory}...`);
1428
+ await extractZip(filepath, { dir: directory });
1429
+ // Add to catalogue
1430
+ this.storage.addEntry(path.basename(filepath, '.zip'), {
1431
+ path: directory,
1432
+ archive: path.basename(filepath),
1433
+ extracted: true,
1434
+ created: new Date().toISOString(),
1435
+ system_model: systemModel,
1436
+ version: version,
1437
+ kind: kind,
1438
+ });
1439
+ // Remove the zip file after extraction
1440
+ fs.unlinkSync(filepath);
1441
+ return directory;
1442
+ }
1443
+ catch (error) {
1444
+ console.error('Error extracting zip file:', error);
1445
+ // If extraction fails, just add the file to catalogue without extraction
1446
+ this.storage.addEntry(filename, {
1447
+ path: filepath,
1448
+ extracted: false,
1449
+ created: new Date().toISOString(),
1450
+ system_model: systemModel,
1451
+ version: version,
1452
+ kind: kind,
1453
+ });
1454
+ return filepath;
1455
+ }
1456
+ }
1457
+ else {
1458
+ // Browser environment
1459
+ try {
1460
+ // For browser environments, we'll use JSZip for zip extraction
1461
+ const JSZip = (await import('jszip')).default;
1462
+ // Create a virtual directory path
1463
+ const directoryPath = `${filepath.substring(0, filepath.length - 4)}`;
1464
+ // Read the file as ArrayBuffer
1465
+ const response = await fetch(filepath);
1466
+ const fileData = await response.arrayBuffer();
1467
+ // Load the zip file
1468
+ console.log(`Extracting zip file to virtual directory: ${directoryPath}...`);
1469
+ const zip = new JSZip();
1470
+ const loadedZip = await zip.loadAsync(fileData);
1471
+ // Extract all files
1472
+ const extractedFiles = {};
1473
+ const extractionPromises = [];
1474
+ loadedZip.forEach((relativePath, zipEntry) => {
1475
+ if (!zipEntry.dir) {
1476
+ const promise = zipEntry.async('uint8array').then(content => {
1477
+ extractedFiles[relativePath] = content;
1478
+ });
1479
+ extractionPromises.push(promise);
1480
+ }
1481
+ });
1482
+ // Wait for all files to be extracted
1483
+ await Promise.all(extractionPromises);
1484
+ console.log('Extraction complete');
1485
+ // Store the extracted files in IndexedDB
1486
+ const { set } = await import('idb-keyval');
1487
+ await set(`${directoryPath}_files`, extractedFiles);
1488
+ // Add to catalogue
1489
+ this.storage.addEntry(path.basename(filepath, '.zip'), {
1490
+ path: directoryPath,
1491
+ archive: path.basename(filepath),
1492
+ extracted: true,
1493
+ created: new Date().toISOString(),
1494
+ system_model: systemModel,
1495
+ version: version,
1496
+ kind: kind,
1497
+ });
1498
+ return directoryPath;
1499
+ }
1500
+ catch (error) {
1501
+ console.error('Error extracting zip file in browser:', error);
1502
+ // If extraction fails, just add the file to catalogue without extraction
1503
+ this.storage.addEntry(filename, {
1504
+ path: filepath,
1505
+ extracted: false,
1506
+ created: new Date().toISOString(),
1507
+ system_model: systemModel,
1508
+ version: version,
1509
+ kind: kind,
1510
+ });
1511
+ return filepath;
1512
+ }
1513
+ }
1514
+ }
1515
+ else {
1516
+ // No extraction needed
1517
+ this.storage.addEntry(filename, {
1518
+ path: filepath,
1519
+ extracted: false,
1520
+ created: new Date().toISOString(),
1521
+ system_model: systemModel,
1522
+ version: version,
1523
+ kind: kind,
1524
+ });
1525
+ return filepath;
1526
+ }
1527
+ }
1528
+ }
1529
+ /**
1530
+ * Get the Excel LCIA file for a specific version
1531
+ *
1532
+ * The Excel LCIA file has varying names depending on the version. This
1533
+ * function downloads the LCIA file, if necessary, and returns the filepath
1534
+ * of the Excel file for further use.
1535
+ *
1536
+ * @param release An instance of EcoinventRelease with valid settings
1537
+ * @param version The ecoinvent version for which the LCIA file should be found
1538
+ * @returns The filepath to the Excel LCIA file
1539
+ */
1540
+ async function getExcelLciaFileForVersion(release, version) {
1541
+ if (!(release instanceof EcoinventRelease)) {
1542
+ throw new Error('release must be an instance of EcoinventRelease');
1543
+ }
1544
+ const versions = await release.listVersions();
1545
+ if (!versions.includes(version)) {
1546
+ throw new Error('Invalid version');
1547
+ }
1548
+ const filelist = await release.listExtraFiles(version);
1549
+ const guess = `ecoinvent ${version}_LCIA_implementation.7z`;
1550
+ // Find the closest match for the LCIA file
1551
+ const possibles = Object.keys(filelist)
1552
+ .filter(filename => filename.toLowerCase().includes('lcia') &&
1553
+ filename.includes(version))
1554
+ .map(filename => ({
1555
+ distance: distance(filename, guess),
1556
+ filename
1557
+ }))
1558
+ .sort((a, b) => a.distance - b.distance);
1559
+ if (possibles.length === 0) {
1560
+ throw new Error(`Can't find LCIA file close to ${guess} among ${Object.keys(filelist).join(', ')}`);
1561
+ }
1562
+ if (possibles[0].distance > 10) {
1563
+ throw new Error(`Closest LCIA filename match to ${guess} is ${possibles[0].filename}, but this is too different`);
1564
+ }
1565
+ // Download and extract the LCIA file
1566
+ const dirpath = await release.getExtra(version, possibles[0].filename);
1567
+ // Find the Excel file in the extracted directory
1568
+ if (typeof window === 'undefined') {
1569
+ // Node.js environment
1570
+ const fs = require('fs');
1571
+ const path = require('path');
1572
+ const excelGuess = `LCIA_implementation_${version}.xlsx`;
1573
+ const files = fs.readdirSync(dirpath);
1574
+ const excelPossibles = files
1575
+ .filter((filename) => filename.toLowerCase().endsWith('.xlsx') &&
1576
+ filename.includes(version))
1577
+ .map((filename) => ({
1578
+ distance: distance(filename, excelGuess),
1579
+ filepath: path.join(dirpath, filename)
1580
+ }))
1581
+ .sort((a, b) => a.distance - b.distance);
1582
+ if (excelPossibles.length > 0 && excelPossibles[0].distance < 10) {
1583
+ return excelPossibles[0].filepath;
1584
+ }
1585
+ else {
1586
+ throw new Error(`Can't find LCIA Excel file like ${excelGuess} in ${files.join(', ')}`);
1587
+ }
1588
+ }
1589
+ else {
1590
+ // Browser environment - return the directory path
1591
+ // In browser, files are stored in IndexedDB, so we can't easily list them
1592
+ // Return the directory path and let the caller handle it
1593
+ console.warn('Excel file listing in browser environment is limited');
1594
+ return dirpath;
1595
+ }
1596
+ }
1597
+
1598
+ // Define version here to avoid circular dependencies
1599
+ const VERSION$1 = '1.1.0';
1600
+ // Initialize logger
1601
+ const logger$1 = getLogger('EcoinventProcess');
1602
+ /**
1603
+ * Custom error class for missing process operations
1604
+ */
1605
+ class MissingProcessError extends Error {
1606
+ constructor(message = 'Must call `.selectProcess()` first') {
1607
+ super(message);
1608
+ this.name = 'MissingProcessError';
1609
+ // This is needed to make instanceof work correctly in TypeScript
1610
+ Object.setPrototypeOf(this, MissingProcessError.prototype);
1611
+ }
1612
+ }
1613
+ /**
1614
+ * Enum for different types of process files
1615
+ */
1616
+ var ProcessFileType;
1617
+ (function (ProcessFileType) {
1618
+ ProcessFileType["UPR"] = "upr";
1619
+ ProcessFileType["LCI"] = "lci";
1620
+ ProcessFileType["LCIA"] = "lcia";
1621
+ ProcessFileType["PDF"] = "pdf";
1622
+ ProcessFileType["UNDEFINED"] = "undefined";
1623
+ })(ProcessFileType || (ProcessFileType = {}));
1624
+ /**
1625
+ * File types that are zipped
1626
+ */
1627
+ const ZIPPED_FILE_TYPES = [
1628
+ ProcessFileType.UPR,
1629
+ ProcessFileType.LCI,
1630
+ ProcessFileType.LCIA,
1631
+ ];
1632
+ /**
1633
+ * Get the display name for a process file type
1634
+ */
1635
+ function getProcessFileTypeDisplayName(type) {
1636
+ switch (type) {
1637
+ case ProcessFileType.UPR:
1638
+ return 'Unit Process';
1639
+ case ProcessFileType.LCI:
1640
+ return 'Life Cycle Inventory';
1641
+ case ProcessFileType.LCIA:
1642
+ return 'Life Cycle Impact Assessment';
1643
+ case ProcessFileType.PDF:
1644
+ return 'Dataset Report';
1645
+ case ProcessFileType.UNDEFINED:
1646
+ return 'Undefined (unlinked and multi-output) Dataset Report';
1647
+ default:
1648
+ throw new Error(`Unknown process file type: ${type}`);
1649
+ }
1650
+ }
1651
+ /**
1652
+ * Method decorator factory for methods that require a selected process
1653
+ */
1654
+ function selectedProcess() {
1655
+ return function (_target, _propertyKey, descriptor) {
1656
+ const originalMethod = descriptor.value;
1657
+ descriptor.value = function (...args) {
1658
+ // 'this' refers to the instance when the method is called
1659
+ const instance = this;
1660
+ if (!instance.datasetId) {
1661
+ logger$1.error('Attempted to call a method requiring a selected process without calling selectProcess() first');
1662
+ throw new MissingProcessError();
1663
+ }
1664
+ return originalMethod.apply(this, args);
1665
+ };
1666
+ return descriptor;
1667
+ };
1668
+ }
1669
+ /**
1670
+ * Split a URL with parameters into a base path and a parameters object
1671
+ * This is a more robust implementation that handles relative URLs
1672
+ *
1673
+ * @param url URL to split
1674
+ */
1675
+ function splitUrl(url) {
1676
+ try {
1677
+ // Try to parse as a full URL
1678
+ const urlObj = new URL(url);
1679
+ const params = {};
1680
+ urlObj.searchParams.forEach((value, key) => {
1681
+ params[key] = value;
1682
+ });
1683
+ return [urlObj.pathname, params];
1684
+ }
1685
+ catch (error) {
1686
+ // Handle relative URLs
1687
+ logger$1.debug(`Parsing relative URL: ${url}`);
1688
+ const [path, query] = url.split('?');
1689
+ const params = {};
1690
+ if (query) {
1691
+ query.split('&').forEach(param => {
1692
+ const [key, value] = param.split('=');
1693
+ if (key) {
1694
+ params[key] = value || '';
1695
+ }
1696
+ });
1697
+ }
1698
+ return [path, params];
1699
+ }
1700
+ }
1701
+ /**
1702
+ * Class for interacting with ecoinvent processes
1703
+ */
1704
+ class EcoinventProcess extends InterfaceBase {
1705
+ /**
1706
+ * Set the release version and system model
1707
+ *
1708
+ * @param version Version identifier
1709
+ * @param systemModel System model identifier
1710
+ */
1711
+ async setRelease(version, systemModel) {
1712
+ logger$1.debug(`Setting release: version=${version}, systemModel=${systemModel}`);
1713
+ const versions = await this.listVersions();
1714
+ if (!versions.includes(version)) {
1715
+ logger$1.error(`Version ${version} not found in available versions: ${versions.join(', ')}`);
1716
+ throw new Error(`Given version ${version} not found`);
1717
+ }
1718
+ this.version = version;
1719
+ const normalizedSystemModel = SYSTEM_MODELS[systemModel] || systemModel;
1720
+ const availableSystemModels = await this.listSystemModels(this.version);
1721
+ if (!availableSystemModels.includes(normalizedSystemModel)) {
1722
+ logger$1.error(`System model '${systemModel}' not available in version ${version}. Available models: ${availableSystemModels.join(', ')}`);
1723
+ throw new Error(`Given system model '${systemModel}' not available in ${version}`);
1724
+ }
1725
+ this.systemModel = normalizedSystemModel;
1726
+ logger$1.debug(`Release set successfully: version=${version}, systemModel=${normalizedSystemModel}`);
1727
+ }
1728
+ /**
1729
+ * Select a process to work with
1730
+ *
1731
+ * @param datasetId Dataset ID (defaults to "1")
1732
+ */
1733
+ selectProcess(datasetId = '1') {
1734
+ logger$1.debug(`Selecting process with datasetId=${datasetId}`);
1735
+ if (!this.systemModel) {
1736
+ logger$1.error('Attempted to select a process without setting release first');
1737
+ throw new Error('Must call `.setRelease()` first');
1738
+ }
1739
+ this.datasetId = datasetId;
1740
+ logger$1.debug(`Process selected: datasetId=${datasetId}, version=${this.version}, systemModel=${this.systemModel}`);
1741
+ }
1742
+ /**
1743
+ * Make a JSON request to the API
1744
+ *
1745
+ * @param url API URL
1746
+ */
1747
+ async _jsonRequest(url) {
1748
+ logger$1.debug(`Making JSON request to URL: ${url}`);
1749
+ const headers = {
1750
+ 'Authorization': `Bearer ${this.accessToken}`,
1751
+ 'ecoinvent-api-client-library': 'ecoinvent-interface-js',
1752
+ 'ecoinvent-api-client-library-version': VERSION$1,
1753
+ ...this.customHeaders,
1754
+ };
1755
+ const params = {
1756
+ dataset_id: this.datasetId,
1757
+ version: this.version,
1758
+ system_model: this.systemModel,
1759
+ };
1760
+ logger$1.debug(`Request parameters: ${JSON.stringify(params)}`);
1761
+ try {
1762
+ const response = await axios.get(url, {
1763
+ params,
1764
+ headers,
1765
+ timeout: 20000,
1766
+ });
1767
+ const message = `Requesting URL.
1768
+ URL: ${url}
1769
+ Class: ${this.constructor.name}
1770
+ Instance ID: ${Math.random().toString(36).substring(2, 9)}
1771
+ Version: ${VERSION$1}
1772
+ User: ${this.username}
1773
+ `;
1774
+ logger$1.debug(message);
1775
+ return response.data;
1776
+ }
1777
+ catch (error) {
1778
+ logger$1.error(`Error making request to ${url}: ${error instanceof Error ? error.message : String(error)}`);
1779
+ throw error;
1780
+ }
1781
+ }
1782
+ /**
1783
+ * Get basic information about the selected process
1784
+ */
1785
+ async getBasicInfo() {
1786
+ return this._jsonRequest(`${this.urls.api}spold`);
1787
+ }
1788
+ /**
1789
+ * Get documentation for the selected process
1790
+ */
1791
+ async getDocumentation() {
1792
+ return this._jsonRequest(`${this.urls.api}spold/documentation`);
1793
+ }
1794
+ /**
1795
+ * Get a file for the selected process
1796
+ *
1797
+ * @param fileType File type
1798
+ * @param directory Directory to save the file to
1799
+ */
1800
+ async getFile(fileType, directory) {
1801
+ logger$1.debug(`Getting file of type ${fileType} for process ${this.datasetId}`);
1802
+ const fileTypeDisplayName = getProcessFileTypeDisplayName(fileType);
1803
+ logger$1.debug(`File type display name: ${fileTypeDisplayName}`);
1804
+ const fileListResponse = await this._jsonRequest(`${this.urls.api}spold/export_file_list`);
1805
+ logger$1.debug(`Received file list with ${fileListResponse.length} entries`);
1806
+ const files = fileListResponse.reduce((acc, obj) => {
1807
+ acc[obj.name] = obj;
1808
+ delete acc[obj.name].name;
1809
+ return acc;
1810
+ }, {});
1811
+ if (!files[fileTypeDisplayName]) {
1812
+ const available = Object.keys(files);
1813
+ logger$1.error(`File type ${fileType} (${fileTypeDisplayName}) not found in available options: ${available.join(', ')}`);
1814
+ throw new Error(`Can't find ${fileType} in available options: ${available.join(', ')}`);
1815
+ }
1816
+ const meta = files[fileTypeDisplayName];
1817
+ logger$1.debug(`Found metadata for file type ${fileType}: ${JSON.stringify(meta)}`);
1818
+ const headers = {
1819
+ 'Authorization': `Bearer ${this.accessToken}`,
1820
+ 'ecoinvent-api-client-library': 'ecoinvent-interface-js',
1821
+ 'ecoinvent-api-client-library-version': VERSION$1,
1822
+ ...this.customHeaders,
1823
+ };
1824
+ if (meta.type?.toLowerCase() === 'xml') {
1825
+ headers['Accept'] = 'text/plain';
1826
+ logger$1.debug('Setting Accept header to text/plain for XML content');
1827
+ }
1828
+ const [url, params] = splitUrl(meta.url);
1829
+ logger$1.debug(`Split URL: path=${url}, params=${JSON.stringify(params)}`);
1830
+ const suffix = meta.type?.toLowerCase() || 'unknown';
1831
+ const filename = `ecoinvent-${this.version}-${this.systemModel}-${fileType}-${this.datasetId}.${suffix}`;
1832
+ logger$1.debug(`Generated filename: ${filename}`);
1833
+ if (fileType === ProcessFileType.UNDEFINED) {
1834
+ logger$1.debug(`Handling undefined file type with special case`);
1835
+ try {
1836
+ const apiUrl = `${this.urls.api.slice(0, -1)}${url}`;
1837
+ logger$1.debug(`Requesting S3 link from ${apiUrl}`);
1838
+ const response = await axios.get(apiUrl, {
1839
+ params,
1840
+ headers,
1841
+ timeout: 20000,
1842
+ });
1843
+ const s3Link = response.data.download_url;
1844
+ logger$1.debug(`Received S3 download link: ${s3Link}`);
1845
+ await this._streamingDownload(s3Link, {}, directory, filename);
1846
+ logger$1.debug(`File downloaded successfully to ${directory}/${filename}`);
1847
+ return `${directory}/${filename}`;
1848
+ }
1849
+ catch (error) {
1850
+ logger$1.error(`Error downloading undefined file type: ${error instanceof Error ? error.message : String(error)}`);
1851
+ throw error;
1852
+ }
1853
+ }
1854
+ const isZipped = ZIPPED_FILE_TYPES.includes(fileType);
1855
+ logger$1.debug(`File is${isZipped ? '' : ' not'} zipped`);
1856
+ try {
1857
+ const apiUrl = `${this.urls.api.slice(0, -1)}${url}`;
1858
+ logger$1.debug(`Downloading file from ${apiUrl}`);
1859
+ await this._streamingDownload(apiUrl, params, directory, filename, headers, isZipped);
1860
+ logger$1.debug(`File downloaded successfully to ${directory}/${filename}`);
1861
+ return `${directory}/${filename}`;
1862
+ }
1863
+ catch (error) {
1864
+ logger$1.error(`Error downloading file: ${error instanceof Error ? error.message : String(error)}`);
1865
+ throw error;
1866
+ }
1867
+ }
1868
+ }
1869
+ __decorate([
1870
+ selectedProcess(),
1871
+ __metadata("design:type", Function),
1872
+ __metadata("design:paramtypes", [String]),
1873
+ __metadata("design:returntype", Promise)
1874
+ ], EcoinventProcess.prototype, "_jsonRequest", null);
1875
+ __decorate([
1876
+ selectedProcess(),
1877
+ __metadata("design:type", Function),
1878
+ __metadata("design:paramtypes", [String, String]),
1879
+ __metadata("design:returntype", Promise)
1880
+ ], EcoinventProcess.prototype, "getFile", null);
1881
+
1882
+ // Initialize logger
1883
+ const logger = getLogger('ProcessMapping');
1884
+ /**
1885
+ * Class for mapping between local and remote processes
1886
+ */
1887
+ class ProcessMapping {
1888
+ /**
1889
+ * Create a new ProcessMapping instance
1890
+ *
1891
+ * @param settings Settings object
1892
+ * @param storage Optional CachedStorage object
1893
+ */
1894
+ constructor(settings, storage) {
1895
+ this.settings = settings;
1896
+ this.storage = storage || new CachedStorage();
1897
+ }
1898
+ /**
1899
+ * Create a mapping of remote processes
1900
+ *
1901
+ * @param version Version identifier
1902
+ * @param systemModel System model identifier
1903
+ * @param maxId Maximum process ID to include
1904
+ * @param delayMs Delay in milliseconds between API calls (default: 100)
1905
+ */
1906
+ async createRemoteMapping(version, systemModel, maxId, delayMs = 100) {
1907
+ const remoteData = [];
1908
+ const process = new EcoinventProcess(this.settings);
1909
+ await process.setRelease(version, systemModel);
1910
+ // Create a progress bar
1911
+ const progressBar = new ProgressBar('Fetching remote processes [:bar] :current/:total :percent :etas', {
1912
+ complete: '=',
1913
+ incomplete: ' ',
1914
+ width: 30,
1915
+ total: maxId
1916
+ });
1917
+ for (let index = 1; index <= maxId; index++) {
1918
+ process.datasetId = index.toString();
1919
+ const info = await process.getBasicInfo();
1920
+ remoteData.push(info);
1921
+ // Update progress bar
1922
+ progressBar.tick();
1923
+ // Add a configurable delay to avoid overwhelming the API
1924
+ await new Promise(resolve => setTimeout(resolve, delayMs));
1925
+ }
1926
+ return remoteData;
1927
+ }
1928
+ /**
1929
+ * Create a mapping of local processes
1930
+ *
1931
+ * @param key Cache key for the release
1932
+ * @param verbose Whether to log verbose information
1933
+ */
1934
+ createLocalMapping(key, verbose = false) {
1935
+ if (!this.storage.catalogue[key]) {
1936
+ throw new Error(`${key} not in current catalogue. Download the release and retry.`);
1937
+ }
1938
+ const dirPath = path.join(this.storage.catalogue[key].path, 'datasets');
1939
+ const localData = [];
1940
+ if (typeof window === 'undefined') {
1941
+ // Node.js environment
1942
+ if (!fs.existsSync(dirPath)) {
1943
+ throw new Error(`Datasets directory not found at ${dirPath}`);
1944
+ }
1945
+ const filePaths = fs.readdirSync(dirPath)
1946
+ .filter(file => file.toLowerCase().endsWith('.spold'))
1947
+ .map(file => path.join(dirPath, file));
1948
+ // Create a progress bar
1949
+ const progressBar = new ProgressBar('Processing local files [:bar] :current/:total :percent :etas', {
1950
+ complete: '=',
1951
+ incomplete: ' ',
1952
+ width: 30,
1953
+ total: filePaths.length
1954
+ });
1955
+ // Create XML parser with options
1956
+ const parser = new XMLParser({
1957
+ ignoreAttributes: false,
1958
+ attributeNamePrefix: '@_',
1959
+ isArray: (name) => [
1960
+ 'activityDataset',
1961
+ 'activity',
1962
+ 'activityName',
1963
+ 'geography',
1964
+ 'shortName',
1965
+ 'intermediateExchange',
1966
+ 'name'
1967
+ ].includes(name)
1968
+ });
1969
+ for (const filePath of filePaths) {
1970
+ try {
1971
+ // Read and parse the XML file
1972
+ const xmlContent = fs.readFileSync(filePath, 'utf8');
1973
+ const result = parser.parse(xmlContent);
1974
+ // Extract information from the parsed XML
1975
+ let activityName = 'Unknown';
1976
+ let referenceProduct = 'Unknown';
1977
+ let geography = 'Unknown';
1978
+ try {
1979
+ // Try to extract activity name
1980
+ if (result.ecoSpold?.activityDataset?.[0]?.activityDescription?.activity?.[0]?.activityName?.[0]?.['#text']) {
1981
+ activityName = result.ecoSpold.activityDataset[0].activityDescription.activity[0].activityName[0]['#text'];
1982
+ }
1983
+ // Try to extract geography
1984
+ if (result.ecoSpold?.activityDataset?.[0]?.activityDescription?.geography?.[0]?.shortName?.[0]?.['#text']) {
1985
+ geography = result.ecoSpold.activityDataset[0].activityDescription.geography[0].shortName[0]['#text'];
1986
+ }
1987
+ // Try to extract reference product
1988
+ // This is more complex as we need to find the exchange with groupType="ReferenceProduct"
1989
+ const exchanges = result.ecoSpold?.activityDataset?.[0]?.flowData?.intermediateExchange || [];
1990
+ for (const exchange of exchanges) {
1991
+ if (exchange['@_groupType'] === 'ReferenceProduct' && exchange.name?.[0]?.['#text']) {
1992
+ referenceProduct = exchange.name[0]['#text'];
1993
+ break;
1994
+ }
1995
+ }
1996
+ }
1997
+ catch (parseError) {
1998
+ console.error(`Error parsing XML structure: ${parseError}`);
1999
+ }
2000
+ localData.push({
2001
+ path: filePath,
2002
+ filename: path.basename(filePath),
2003
+ activity_name: activityName,
2004
+ reference_product: referenceProduct,
2005
+ geography: geography,
2006
+ });
2007
+ // Update progress bar
2008
+ progressBar.tick();
2009
+ if (verbose) {
2010
+ console.log(`Processed ${filePath}`);
2011
+ }
2012
+ }
2013
+ catch (error) {
2014
+ console.error(`Error processing ${filePath}:`, error);
2015
+ }
2016
+ }
2017
+ }
2018
+ else {
2019
+ // Browser environment - not supported yet
2020
+ console.warn('Local mapping in browser environment is not supported yet');
2021
+ }
2022
+ return localData;
2023
+ }
2024
+ /**
2025
+ * Find the closest match between a local and remote process
2026
+ *
2027
+ * @param localProcess Local process information
2028
+ * @param remoteProcesses Array of remote process information
2029
+ * @param threshold Maximum Levenshtein distance to consider a match (default: 5)
2030
+ * @returns The closest matching remote process or null if no match found
2031
+ */
2032
+ findClosestMatch(localProcess, remoteProcesses, threshold = 5) {
2033
+ logger.debug(`Finding closest match for ${localProcess.activity_name} (${localProcess.reference_product})`);
2034
+ if (!localProcess.activity_name || !localProcess.reference_product) {
2035
+ logger.warn('Local process missing activity_name or reference_product');
2036
+ return null;
2037
+ }
2038
+ // Create a combined string for matching
2039
+ const localString = `${localProcess.activity_name} ${localProcess.reference_product} ${localProcess.geography || ''}`;
2040
+ // Calculate distances for all remote processes
2041
+ const candidates = [];
2042
+ for (const remoteProcess of remoteProcesses) {
2043
+ if (!remoteProcess.activity_name || !remoteProcess.reference_product) {
2044
+ continue;
2045
+ }
2046
+ const remoteString = `${remoteProcess.activity_name} ${remoteProcess.reference_product} ${remoteProcess.geography || ''}`;
2047
+ const dist = distance(localString, remoteString);
2048
+ if (dist <= threshold) {
2049
+ candidates.push({ process: remoteProcess, dist });
2050
+ }
2051
+ }
2052
+ // Sort by distance (ascending)
2053
+ candidates.sort((a, b) => a.dist - b.dist);
2054
+ if (candidates.length > 0) {
2055
+ logger.debug(`Found match with distance ${candidates[0].dist}: ${candidates[0].process.activity_name}`);
2056
+ return candidates[0].process;
2057
+ }
2058
+ logger.debug('No match found within threshold');
2059
+ return null;
2060
+ }
2061
+ /**
2062
+ * Match local processes to remote processes
2063
+ *
2064
+ * @param localProcesses Array of local process information
2065
+ * @param remoteProcesses Array of remote process information
2066
+ * @param threshold Maximum Levenshtein distance to consider a match (default: 5)
2067
+ * @returns Array of matched process pairs
2068
+ */
2069
+ matchProcesses(localProcesses, remoteProcesses, threshold = 5) {
2070
+ logger.debug(`Matching ${localProcesses.length} local processes to ${remoteProcesses.length} remote processes`);
2071
+ const matches = [];
2072
+ // Create a progress bar
2073
+ const progressBar = new ProgressBar('Matching processes [:bar] :current/:total :percent :etas', {
2074
+ complete: '=',
2075
+ incomplete: ' ',
2076
+ width: 30,
2077
+ total: localProcesses.length
2078
+ });
2079
+ for (const localProcess of localProcesses) {
2080
+ const match = this.findClosestMatch(localProcess, remoteProcesses, threshold);
2081
+ if (match) {
2082
+ matches.push({ local: localProcess, remote: match });
2083
+ }
2084
+ // Update progress bar
2085
+ progressBar.tick();
2086
+ }
2087
+ logger.debug(`Found ${matches.length} matches out of ${localProcesses.length} local processes`);
2088
+ return matches;
2089
+ }
2090
+ /**
2091
+ * Add mapping data to the mappings archive
2092
+ *
2093
+ * Updates a zipped mappings archive by adding new mapping data as a JSON file.
2094
+ * Creates a backup of the existing mappings file before updating.
2095
+ *
2096
+ * @param data Array of process information to store
2097
+ * @param version Version identifier
2098
+ * @param systemModel System model identifier
2099
+ * @returns The filepath to the updated mappings file
2100
+ */
2101
+ async addMapping(data, version, systemModel) {
2102
+ if (typeof window !== 'undefined') {
2103
+ throw new Error('addMapping is only supported in Node.js environment');
2104
+ }
2105
+ logger.debug(`Adding mapping for version=${version}, systemModel=${systemModel}`);
2106
+ const fs = require('fs');
2107
+ const path = require('path');
2108
+ const JSZip = (await import('jszip')).default;
2109
+ const mappingsFilename = 'mappings.zip';
2110
+ const mappingsPath = path.join(this.storage.dir, mappingsFilename);
2111
+ const newFilename = `${version}_${systemModel}.json`;
2112
+ let zip = new JSZip();
2113
+ let existingData = {};
2114
+ // Load existing mappings if they exist
2115
+ if (fs.existsSync(mappingsPath)) {
2116
+ logger.debug(`Loading existing mappings from ${mappingsPath}`);
2117
+ const zipData = fs.readFileSync(mappingsPath);
2118
+ zip = await JSZip.loadAsync(zipData);
2119
+ // Check if this version/system model combination already exists
2120
+ if (zip.file(newFilename)) {
2121
+ throw new Error(`Mapping for version ${version} and system model ${systemModel} already exists. Delete the existing mapping first.`);
2122
+ }
2123
+ // Load all existing files
2124
+ const filePromises = [];
2125
+ zip.forEach((relativePath, zipEntry) => {
2126
+ if (!zipEntry.dir) {
2127
+ const promise = zipEntry.async('string').then(content => {
2128
+ existingData[relativePath] = content;
2129
+ });
2130
+ filePromises.push(promise);
2131
+ }
2132
+ });
2133
+ await Promise.all(filePromises);
2134
+ // Create backup
2135
+ const backupPath = mappingsPath.replace('.zip', '_backup.zip');
2136
+ logger.debug(`Creating backup at ${backupPath}`);
2137
+ fs.copyFileSync(mappingsPath, backupPath);
2138
+ }
2139
+ // Create new zip with existing data
2140
+ zip = new JSZip();
2141
+ for (const [filename, content] of Object.entries(existingData)) {
2142
+ zip.file(filename, content);
2143
+ }
2144
+ // Add new mapping
2145
+ logger.debug(`Adding new mapping file: ${newFilename}`);
2146
+ zip.file(newFilename, JSON.stringify(data, null, 2));
2147
+ // Generate the zip file
2148
+ const zipContent = await zip.generateAsync({
2149
+ type: 'nodebuffer',
2150
+ compression: 'DEFLATE',
2151
+ compressionOptions: { level: 9 }
2152
+ });
2153
+ // Write to disk
2154
+ fs.writeFileSync(mappingsPath, zipContent);
2155
+ logger.debug(`Mappings file updated at ${mappingsPath}`);
2156
+ // Update catalogue
2157
+ this.storage.addEntry('mappings', {
2158
+ path: mappingsPath,
2159
+ extracted: false,
2160
+ created: new Date().toISOString(),
2161
+ kind: 'mappings',
2162
+ });
2163
+ return mappingsPath;
2164
+ }
2165
+ }
2166
+
2167
+ // Main entry point for the ecoinvent-interface package
2168
+ // Export types
2169
+ // Package version
2170
+ const VERSION = '1.1.0';
2171
+
2172
+ export { CachedStorage, EcoinventProcess, EcoinventRelease, InterfaceBase, LogLevel, Logger, ProcessFileType, ProcessMapping, ReleaseType, SYSTEM_MODELS, SYSTEM_MODELS_REVERSE, Settings, URLS, VERSION, getExcelLciaFileForVersion, getLogger, permanentSetting, setLogLevel };
2173
+ //# sourceMappingURL=index.mjs.map