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/README.md +497 -0
- package/dist/core/interface-base.d.ts +87 -0
- package/dist/core/settings.d.ts +28 -0
- package/dist/index.d.ts +613 -0
- package/dist/index.js +2210 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +2173 -0
- package/dist/index.mjs.map +1 -0
- package/dist/mapping/process-mapping.d.ts +76 -0
- package/dist/process/process.d.ts +59 -0
- package/dist/release/release.d.ts +94 -0
- package/dist/storage/cached-storage.d.ts +188 -0
- package/dist/storage/settings-storage.d.ts +14 -0
- package/dist/types/index.d.ts +29 -0
- package/dist/utils/logger.d.ts +65 -0
- package/package.json +71 -0
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
|