• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

telefonicaid / fiware-data-access / 23790082499

31 Mar 2026 09:22AM UTC coverage: 95.02% (-0.3%) from 95.333%
23790082499

Pull #129

github

web-flow
Merge f5c6768f0 into 1616b9782
Pull Request #129: REST-ification and segmentation with visibility and fiware-servicePath

863 of 1066 branches covered (80.96%)

Branch coverage included in aggregate %.

308 of 308 new or added lines in 4 files covered. (100.0%)

11 existing lines in 1 file now uncovered.

3697 of 3733 relevant lines covered (99.04%)

56.08 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

93.27
/src/lib/fda.js
1
// Copyright 2025 Telefónica Soluciones de Informática y Comunicaciones de España, S.A.U.
4✔
2
// PROJECT: fiware-data-access
4✔
3
//
4✔
4
// This software and / or computer program has been developed by Telefónica Soluciones
4✔
5
// de Informática y Comunicaciones de España, S.A.U (hereinafter TSOL) and is protected
4✔
6
// as copyright by the applicable legislation on intellectual property.
4✔
7
//
4✔
8
// It belongs to TSOL, and / or its licensors, the exclusive rights of reproduction,
4✔
9
// distribution, public communication and transformation, and any economic right on it,
4✔
10
// all without prejudice of the moral rights of the authors mentioned above. It is expressly
4✔
11
// forbidden to decompile, disassemble, reverse engineer, sublicense or otherwise transmit
4✔
12
// by any means, translate or create derivative works of the software and / or computer
4✔
13
// programs, and perform with respect to all or part of such programs, any type of exploitation.
4✔
14
//
4✔
15
// Any use of all or part of the software and / or computer program will require the
4✔
16
// express written consent of TSOL. In all cases, it will be necessary to make
4✔
17
// an express reference to TSOL ownership in the software and / or computer
4✔
18
// program.
4✔
19
//
4✔
20
// Non-fulfillment of the provisions set forth herein and, in general, any violation of
4✔
21
// the peaceful possession and ownership of these rights will be prosecuted by the means
4✔
22
// provided in both Spanish and international law. TSOL reserves any civil or
4✔
23
// criminal actions it may exercise to protect its rights.
4✔
24

4✔
25
import { getAgenda } from './jobs.js';
4✔
26
import {
4✔
27
  runPreparedStatement,
4✔
28
  runPreparedStatementStream,
4✔
29
  getDBConnection,
4✔
30
  releaseDBConnection,
4✔
31
  toParquet,
4✔
32
  checkParams,
4✔
33
  resolveDAParams,
4✔
34
  validateDAQuery,
4✔
35
  extractDate,
4✔
36
} from './utils/db.js';
4✔
37
import { uploadTable, runPgQuery, createPgCursorReader } from './utils/pg.js';
4✔
38
import {
4✔
39
  getS3Client,
4✔
40
  dropFile,
4✔
41
  moveObject,
4✔
42
  listObjects,
4✔
43
  dropFiles,
4✔
44
} from './utils/aws.js';
4✔
45
import {
4✔
46
  createFDAMongo,
4✔
47
  regenerateFDA,
4✔
48
  retrieveFDAs,
4✔
49
  retrieveFDA,
4✔
50
  storeDA,
4✔
51
  removeFDA,
4✔
52
  retrieveDAs,
4✔
53
  retrieveDA,
4✔
54
  updateDA,
4✔
55
  removeDA,
4✔
56
  updateFDAStatus,
4✔
57
} from './utils/mongo.js';
4✔
58
import {
4✔
59
  convertBigInt,
4✔
60
  getWindowDate,
4✔
61
  assertFreshQueriesEnabled,
4✔
62
  acquireFreshQuerySlot,
4✔
63
} from './utils/utils.js';
4✔
64
import { config } from './fdaConfig.js';
4✔
65
import { FDAError } from './fdaError.js';
4✔
66

4✔
67
const FRESH_CURSOR_BATCH_SIZE = 250;
4✔
68
export const VALID_VISIBILITIES = ['public', 'private'];
4✔
69
const VALID_VISIBILITIES_SET = new Set(VALID_VISIBILITIES);
4✔
70

5✔
71
export async function getFDAs(service, visibility, servicePath) {
5✔
72
  const fdas = await retrieveFDAs(service);
4✔
73

3✔
74
  if (visibility === undefined && servicePath === undefined) {
3!
75
    return fdas;
1✔
76
  }
1✔
77

3✔
78
  const normalizedVisibility = normalizeVisibility(visibility);
3✔
79
  const normalizedServicePath = normalizeServicePath(servicePath);
3✔
80

4✔
81
  return fdas.filter(
4✔
82
    (fda) =>
3✔
83
      normalizeVisibility(fda.visibility) === normalizedVisibility &&
15✔
84
      normalizeServicePath(fda.servicePath) === normalizedServicePath,
2✔
85
  );
2✔
86
}
2✔
87

3✔
88
export async function getFDA(service, fdaId, visibility, servicePath) {
3✔
89
  if (visibility === undefined && servicePath === undefined) {
532!
90
    return await getStoredFDA(service, fdaId);
1✔
91
  }
10✔
92

542✔
93
  return await getAccessibleFDA(service, fdaId, visibility, servicePath);
542✔
94
}
542✔
95

13✔
96
export async function executeQuery({
13✔
97
  service,
70✔
98
  visibility,
70✔
99
  servicePath,
70✔
100
  params,
70✔
101
  fresh = false,
70✔
102
}) {
70✔
103
  if (fresh) {
70✔
104
    return executeFreshQuery({ service, visibility, servicePath, params });
18!
105
  }
8✔
106

52✔
107
  const { fdaId, daId, ...rest } = params;
52✔
108

52✔
109
  await ensureFDAReadyForQuery(service, fdaId, visibility, servicePath);
52✔
110

44✔
111
  const conn = await getDBConnection();
44✔
112

44✔
113
  try {
44✔
114
    return await runPreparedStatement(conn, service, fdaId, daId, rest);
54✔
115
  } finally {
45✔
116
    await releaseDBConnection(conn);
47✔
117
  }
47✔
118
}
63✔
119

6✔
120
export async function executeQueryStream({
6✔
121
  service,
7✔
122
  visibility,
7✔
123
  servicePath,
7✔
124
  params,
7✔
125
  req,
7✔
126
  res,
7✔
127
  fresh = false,
7✔
128
}) {
7✔
129
  if (fresh) {
7✔
130
    return executeFreshQueryStream({
5✔
131
      service,
5✔
132
      visibility,
5✔
133
      servicePath,
5!
134
      params,
2✔
135
      req,
2✔
136
      res,
2✔
137
    });
2✔
138
  }
2✔
139

2✔
140
  const { fdaId, daId, ...rest } = params;
2✔
141

2✔
142
  await ensureFDAReadyForQuery(service, fdaId, visibility, servicePath);
2✔
143

2✔
144
  const conn = await getDBConnection();
2✔
145

2✔
146
  let stream;
2✔
147
  let close;
2✔
148

2✔
149
  try {
2✔
150
    const result = await runPreparedStatementStream(
2✔
151
      conn,
2✔
152
      service,
2✔
153
      fdaId,
2✔
154
      daId,
2✔
155
      rest,
2✔
156
    );
2✔
157

2✔
158
    stream = result.stream;
2✔
159
    close = result.close;
2✔
160
  } catch (err) {
4!
161
    await releaseDBConnection(conn);
×
162
    throw err;
×
163
  }
×
164

2✔
165
  let cleaned = false;
2✔
166

2✔
167
  const cleanup = async () => {
2✔
168
    if (cleaned) {
4✔
169
      return;
2✔
170
    }
2✔
171
    cleaned = true;
2✔
172

2✔
173
    try {
2✔
174
      await close();
2✔
175
    } finally {
2✔
176
      await releaseDBConnection(conn);
2✔
177
    }
2✔
178
  };
2✔
179

2✔
180
  req.on('close', () => {
2✔
181
    cleanup().catch(() => {});
2✔
182
  });
2✔
183

2✔
184
  res.setHeader('Content-Type', 'application/x-ndjson');
2✔
185

2✔
186
  try {
2✔
187
    const columnNames = stream.columnNames();
2✔
188
    // eslint-disable-next-line no-constant-condition
2✔
189
    while (true) {
4✔
190
      const chunk = await stream.fetchChunk();
4✔
191
      if (chunk.rowCount === 0) {
4✔
192
        break;
2✔
193
      }
2✔
194

2✔
195
      const rows = chunk.getRows();
2✔
196

2✔
197
      const lines = [];
2✔
198

2✔
199
      for (const row of rows) {
4✔
200
        const rowObj = {};
4✔
201

4✔
202
        for (let i = 0; i < columnNames.length; i++) {
4✔
203
          rowObj[columnNames[i]] = row[i];
12✔
204
        }
12✔
205

4✔
206
        const safeObj = convertBigInt(rowObj);
4✔
207
        lines.push(JSON.stringify(safeObj));
4✔
208
      }
4✔
209

2✔
210
      const payload = lines.join('\n') + '\n';
2✔
211

2✔
212
      const ok = res.write(payload);
5✔
213
      if (!ok) {
4!
214
        await new Promise((resolve) => res.once('drain', resolve));
1✔
215
      }
10✔
216
    }
14✔
217
  } finally {
14✔
218
    await cleanup();
12✔
219
  }
12✔
220

12✔
221
  return res.end();
12✔
222
}
14✔
223

13✔
224
async function executeFreshQuery({ service, visibility, servicePath, params }) {
18✔
225
  assertFreshQueriesEnabled(config.roles.syncQueries);
18✔
226

18✔
227
  const releaseFreshSlot = acquireFreshQuerySlot(
18✔
228
    config.freshQueries.maxConcurrent,
18✔
229
  );
18✔
230
  try {
18✔
231
    const { text, values } = await buildFreshQueryStatement(
18✔
232
      service,
18✔
233
      visibility,
9✔
234
      servicePath,
18✔
235
      params,
17✔
236
    );
17✔
237

15✔
238
    const rows = await runPgQuery(service, text, values);
16!
239
    return convertBigInt(rows);
16✔
240
  } catch (e) {
9!
241
    if (e instanceof FDAError) {
3✔
242
      throw e;
3✔
243
    }
3✔
244

3✔
245
    throw e;
3✔
246
  } finally {
11✔
247
    releaseFreshSlot();
9✔
248
  }
9✔
249
}
11✔
250

6✔
251
async function executeFreshQueryStream({
5✔
252
  service,
5✔
253
  visibility,
5✔
254
  servicePath,
5✔
255
  params,
5✔
256
  req,
5✔
257
  res,
5✔
258
}) {
5✔
259
  assertFreshQueriesEnabled(config.roles.syncQueries);
5✔
260

5✔
261
  const releaseFreshSlot = acquireFreshQuerySlot(
5✔
262
    config.freshQueries.maxConcurrent,
5✔
263
  );
5✔
264
  let cursorReader;
5✔
265

5✔
266
  try {
5✔
267
    const { text, values } = await buildFreshQueryStatement(
5✔
268
      service,
5✔
269
      visibility,
4✔
270
      servicePath,
4✔
271
      params,
3✔
272
    );
3✔
273

3✔
274
    cursorReader = await createPgCursorReader(
4✔
275
      service,
4✔
276
      text,
4✔
277
      values,
4✔
278
      FRESH_CURSOR_BATCH_SIZE,
4✔
279
    );
4✔
280

4✔
281
    req.on('close', () => {
4✔
282
      cursorReader?.close().catch(() => {});
5✔
283
    });
5✔
284

5✔
285
    res.setHeader('Content-Type', 'application/x-ndjson');
5✔
286

3✔
287
    // eslint-disable-next-line no-constant-condition
3✔
288
    while (true) {
3✔
289
      const rows = await cursorReader.readNextChunk();
9✔
290
      if (rows.length === 0) {
9✔
291
        break;
3✔
292
      }
3✔
293

7✔
294
      for (const row of rows) {
9✔
295
        const safeObj = convertBigInt(row);
1,209✔
296
        const ok = res.write(JSON.stringify(safeObj) + '\n');
1,207!
297
        if (!ok) {
1,209!
298
          await new Promise((resolve) => res.once('drain', resolve));
3✔
299
        }
2✔
300
      }
1,207✔
301
    }
19✔
302
  } catch (e) {
15!
303
    if (e instanceof FDAError) {
13✔
304
      throw e;
13✔
305
    }
13✔
306

13✔
307
    throw e;
13✔
308
  } finally {
15✔
309
    await cursorReader?.close();
15✔
310
    releaseFreshSlot();
15✔
311
  }
15✔
312

15✔
313
  return res.end();
15✔
314
}
15✔
315

16✔
316
async function buildFreshQueryStatement(
21✔
317
  service,
9✔
318
  visibility,
9✔
319
  servicePath,
9✔
320
  params,
9✔
321
) {
9✔
322
  const { fdaId, daId, ...rest } = params;
9✔
323

9✔
324
  const da = await retrieveDA(service, fdaId, daId);
20✔
325
  if (!da?.query) {
20!
326
    throw new FDAError(
13✔
327
      404,
10✔
328
      'DaNotFound',
13!
329
      `DA ${daId} does not exist in FDA ${fdaId} with service ${service}.`,
13✔
330
    );
13✔
331
  }
13✔
332

21✔
333
  const fda = await getAccessibleFDA(service, fdaId, visibility, servicePath);
9✔
334

18✔
335
  const validatedParams = resolveDAParams(rest || {}, da.params);
18!
336
  const freshBaseQuery = buildFreshDAQuery(fda.query, da.query);
18!
337

18✔
338
  return replaceNamedParamsWithPositional(freshBaseQuery, validatedParams);
18!
339
}
18✔
340

13✔
341
function buildFreshDAQuery(fdaQuery, daQuery) {
18✔
342
  const cleanFdaQuery = removeTrailingSemicolon(fdaQuery?.trim() || '');
9!
343
  const cleanDaQuery = removeTrailingSemicolon(daQuery?.trim() || '');
18!
344

17✔
345
  if (!cleanDaQuery || /^from\b/i.test(cleanDaQuery)) {
18!
346
    throw new FDAError(
1✔
347
      400,
1✔
348
      'InvalidDAQuery',
1✔
349
      'DA query must not include FROM clause at start. It is managed internally.',
10✔
350
    );
8✔
351
  }
8✔
352

16✔
353
  if (!/^select\b/i.test(cleanDaQuery)) {
16!
354
    throw new FDAError(
8✔
355
      400,
8✔
356
      'InvalidDAQuery',
8✔
357
      'Fresh query mode requires DA query to start with SELECT.',
10!
358
    );
10✔
359
  }
10✔
360

18✔
361
  const selectTail = cleanDaQuery.replace(/^select\s+/i, '');
18✔
362
  const clauseMatch = selectTail.match(
18!
363
    /\b(where|group\s+by|having|order\s+by|limit|offset)\b/i,
9✔
364
  );
9✔
365

9✔
366
  const projection = clauseMatch
18✔
367
    ? selectTail.slice(0, clauseMatch.index).trim()
18✔
368
    : selectTail.trim();
9!
369
  const clauses = clauseMatch ? selectTail.slice(clauseMatch.index).trim() : '';
18!
370

14✔
371
  if (!projection) {
14!
372
    throw new FDAError(
10!
373
      400,
10✔
374
      'InvalidDAQuery',
10✔
375
      'DA query must contain a SELECT projection.',
1✔
376
    );
6✔
377
  }
6✔
378

14✔
379
  if (/\bfrom\b/i.test(projection)) {
14!
380
    throw new FDAError(
6✔
381
      400,
6✔
382
      'InvalidDAQuery',
6✔
383
      'DA query must not include FROM clause. It is managed internally.',
6✔
384
    );
8✔
385
  }
8✔
386

16✔
387
  const trailing = clauses ? ` ${clauses}` : '';
16!
388
  return `SELECT ${projection} FROM (${cleanFdaQuery}) AS fda_source${trailing}`;
9✔
389
}
9✔
390

10✔
391
function replaceNamedParamsWithPositional(query, params) {
16✔
392
  const indexes = new Map();
14✔
393
  const values = [];
14✔
394

14✔
395
  const text = query.replace(/\$([A-Za-z_][A-Za-z0-9_]*)/g, (_m, name) => {
14✔
396
    if (!Object.prototype.hasOwnProperty.call(params, name)) {
9!
397
      throw new FDAError(
20✔
398
        400,
1✔
UNCOV
399
        'InvalidQueryParam',
×
UNCOV
400
        `Missing required param "${name}".`,
×
401
      );
×
402
    }
×
403

8✔
404
    if (!indexes.has(name)) {
8✔
405
      indexes.set(name, values.length + 1);
8✔
406
      values.push(params[name]);
8✔
407
    }
8✔
408

8✔
409
    return `$${indexes.get(name)}`;
8✔
410
  });
8✔
411

8✔
412
  return { text, values };
8✔
413
}
8✔
414

3✔
415
function removeTrailingSemicolon(query) {
16✔
416
  return query.replace(/;+\s*$/, '');
16✔
417
}
16✔
418

3✔
419
export async function createDA(
3✔
420
  service,
38✔
421
  fdaId,
38✔
422
  daId,
38✔
423
  description,
38✔
424
  userQuery,
38✔
425
  params,
38✔
426
  visibility,
38✔
427
  servicePath,
38✔
428
) {
38✔
429
  const conn = await getDBConnection();
38✔
430

38✔
431
  try {
38✔
432
    if (visibility !== undefined || servicePath !== undefined) {
38!
433
      await getAccessibleFDA(service, fdaId, visibility, servicePath);
38✔
434
    }
38✔
435

38✔
436
    const existing = await retrieveDA(service, fdaId, daId);
39✔
437

51✔
438
    if (existing) {
51!
439
      throw new FDAError(
13✔
440
        409,
13✔
441
        'DuplicatedKey',
13✔
442
        `DA ${daId} already exists in FDA ${fdaId}`,
13✔
443
      );
13✔
444
    }
13✔
445

51✔
446
    const normalizedParams = checkParams(params);
51✔
447
    await validateDAQuery(conn, service, fdaId, userQuery);
51✔
448
    await storeDA(
33✔
449
      service,
33✔
450
      fdaId,
33✔
451
      daId,
33✔
452
      description,
33✔
453
      userQuery,
33✔
454
      normalizedParams,
33✔
455
    );
30✔
456
  } finally {
48✔
457
    await releaseDBConnection(conn);
48✔
458
  }
48✔
459
}
48✔
460

13✔
461
export async function fetchFDA(
16✔
462
  fdaId,
31✔
463
  query,
31✔
464
  service,
31✔
465
  visibility,
31✔
466
  servicePath,
43✔
467
  description,
39✔
468
  refreshPolicy,
39✔
469
  timeColumn,
39✔
470
  objStgConf,
39✔
471
) {
39✔
472
  const normalizedVisibility = normalizeVisibility(visibility);
39✔
473
  const normalizedServicePath = normalizeServicePath(servicePath);
39✔
474

39✔
475
  await createFDAMongo(
39✔
476
    fdaId,
39✔
477
    query,
39✔
478
    service,
39✔
479
    normalizedVisibility,
43✔
480
    normalizedServicePath,
43✔
481
    description,
43✔
482
    refreshPolicy,
35✔
483
    timeColumn,
35✔
484
    objStgConf,
35✔
485
  );
35✔
486

33✔
487
  try {
33✔
488
    await createOneRowParquetSync(service, fdaId, query);
33✔
489
  } catch (err) {
35!
490
    await rollbackFDAProvisioning(service, fdaId);
5✔
491
    throw err;
5✔
492
  }
5✔
493

33✔
494
  const agenda = getAgenda();
33✔
495

33✔
496
  // Execute first fetch immediately (when a fetcher is free)
33✔
497
  await agenda.now('refresh-fda', {
33✔
498
    fdaId,
33✔
499
    query,
33✔
500
    service,
33✔
501
    timeColumn,
33✔
502
    objStgConf,
33✔
503
  });
33✔
504

33✔
505
  // Schedule refreshes according to policy
33✔
506
  if (refreshPolicy?.type === 'interval' || refreshPolicy?.type === 'cron') {
35!
507
    // unique is not really needed since we check existence before, but it adds an extra layer of safety in case of duplicate calls
2✔
508
    await agenda.every(
2✔
509
      refreshPolicy.value,
2✔
510
      'refresh-fda',
2✔
511
      { fdaId, query, service, timeColumn, objStgConf },
7✔
512
      {
7✔
513
        skipImmediate: true,
7✔
514
        unique: {
4✔
515
          name: 'refresh-fda',
4✔
516
          'data.fdaId': fdaId,
4✔
517
        },
4✔
518
      },
15✔
519
    );
9✔
520

9✔
521
    const { deleteInterval, windowSize } = refreshPolicy;
15✔
522
    if (deleteInterval && windowSize) {
5!
523
      await agenda.every(
3✔
524
        deleteInterval,
3✔
525
        'clean-partition',
3✔
526
        {
3✔
527
          fdaId,
3✔
528
          service,
3✔
529
          windowSize,
3✔
530
          objStgConf,
3✔
531
        },
3✔
532
        {
3✔
533
          skipImmediate: true,
3✔
534
          unique: {
3✔
535
            name: 'refresh-fda',
3✔
536
            'data.fdaId': fdaId,
3✔
537
          },
3✔
538
        },
3✔
539
      );
3✔
540
    }
3✔
541
    if (deleteInterval && !windowSize) {
5!
542
      throw new FDAError(
3✔
543
        400,
3✔
544
        'InvalidParam',
3✔
545
        `Window size is required with a delete interval.`,
3✔
546
      );
3✔
547
    }
3✔
548
  }
5✔
549

31✔
550
  if (refreshPolicy?.type === 'window') {
33✔
551
    const { interval, windowQuery } = getUpdateWindow(
10✔
552
      refreshPolicy.value,
10✔
553
      query,
10✔
554
      timeColumn,
10✔
555
    );
10✔
556

10✔
557
    // partitionFlag lets us know we are refreshing already existing partitioned files for performance purposes
10✔
558
    await agenda.every(
10✔
559
      interval,
11✔
560
      'refresh-fda',
11✔
561
      {
11✔
562
        fdaId,
11✔
563
        query: windowQuery,
9✔
564
        service,
9✔
565
        timeColumn,
9✔
566
        objStgConf,
9✔
567
        partitionFlag: true,
9✔
568
      },
11✔
569
      {
11✔
570
        skipImmediate: true,
11✔
571
        unique: {
11✔
572
          name: 'refresh-fda',
11✔
573
          'data.fdaId': fdaId,
11✔
574
        },
11✔
575
      },
11✔
576
    );
11✔
577

9✔
578
    const { deleteInterval, windowSize } = refreshPolicy;
9✔
579
    if (deleteInterval && windowSize) {
11✔
580
      await agenda.every(
9✔
581
        deleteInterval,
9✔
582
        'clean-partition',
9✔
583
        {
9✔
584
          fdaId,
9✔
585
          service,
9✔
586
          windowSize,
9✔
587
          objStgConf,
9✔
588
        },
9✔
589
        {
9✔
590
          skipImmediate: true,
9✔
591
          unique: {
9✔
592
            name: 'refresh-fda',
9✔
593
            'data.fdaId': fdaId,
9✔
594
          },
9✔
595
        },
9✔
596
      );
9✔
597
    }
9✔
598
    if (deleteInterval && !windowSize) {
11!
599
      throw new FDAError(
3✔
600
        400,
3✔
601
        'InvalidParam',
3✔
602
        `Window size is required with a delete interval.`,
3!
UNCOV
603
      );
×
UNCOV
604
    }
×
605
  }
8✔
606
}
30✔
607

3✔
608
function getUpdateWindow(windowType, query, timeColumn) {
9✔
609
  const slidingWindow = {
9✔
610
    hourly: {
9✔
611
      interval: '0 * * * *',
9✔
612
      windowQuery: `SELECT * FROM (${query}) q WHERE ${timeColumn} >= NOW() - INTERVAL '1 hour'`,
9!
613
    },
8✔
614
    daily: {
9✔
615
      interval: '0 0 * * *',
9✔
616
      windowQuery: `SELECT * FROM (${query}) q WHERE ${timeColumn} >= NOW() - INTERVAL '1 day'`,
9✔
617
    },
9✔
618
    weekly: {
9✔
619
      interval: '0 0 * * 0',
9✔
620
      windowQuery: `SELECT * FROM (${query}) q WHERE ${timeColumn} >= NOW() - INTERVAL '1 week'`,
9✔
621
    },
9✔
622
    monthly: {
9✔
623
      interval: '0 0 1 * *',
9✔
624
      windowQuery: `SELECT * FROM (${query}) q WHERE ${timeColumn} >= NOW() - INTERVAL '1 month'`,
9✔
625
    },
10✔
626
  };
10✔
627

10✔
628
  const window = slidingWindow[windowType];
10✔
629
  if (!window) {
10✔
630
    throw new FDAError(
4✔
631
      400,
4✔
632
      'InvalidParam',
4✔
633
      `Invalid window type: ${windowType}.`,
4✔
634
    );
4✔
635
  }
3✔
636
  return window;
7✔
637
}
9✔
638

4✔
639
export async function updateFDA(service, fdaId, visibility, servicePath) {
4✔
640
  if (arguments.length >= 4) {
9✔
641
    await getAccessibleFDA(service, fdaId, visibility, servicePath);
9✔
642
  }
11✔
643

11✔
644
  const previous = await regenerateFDA(service, fdaId);
11✔
645

7✔
646
  const agenda = getAgenda();
7✔
647

7✔
648
  // Execute refresh immediately (when a fetcher is free)
7!
649
  await agenda.now('refresh-fda', {
7✔
650
    fdaId,
7✔
651
    query: previous.query,
7✔
652
    service,
7✔
653
    timeColumn: previous.timeColumn,
7✔
654
    objStgConf: previous.objStgConf,
5✔
655
    partitionFlag: true,
5✔
656
  });
5✔
657
}
9✔
658

4✔
659
export async function processFDAAsync(
6✔
660
  fdaId,
34✔
661
  query,
34✔
662
  service,
34✔
663
  timeColumn,
34✔
664
  objStgConf,
34✔
665
  partitionFlag,
34✔
666
) {
34✔
667
  try {
34✔
668
    await updateFDAStatus(service, fdaId, 'fetching', 10);
34✔
669

34✔
670
    await uploadTableToObjStg(
34✔
671
      service,
34✔
672
      service,
34✔
673
      query,
34✔
674
      service,
34✔
675
      fdaId,
34✔
676
      timeColumn,
34✔
677
      objStgConf,
34✔
678
      partitionFlag,
34✔
679
    );
34✔
680

30✔
681
    await updateFDAStatus(service, fdaId, 'completed', 100);
30✔
682
  } catch (err) {
34✔
683
    await updateFDAStatus(service, fdaId, 'failed', 0, err.message);
6✔
684
    throw err;
6✔
685
  }
6✔
686
}
34✔
687

5✔
688
export async function deleteFDA(service, fdaId, visibility, servicePath) {
4✔
689
  if (visibility !== undefined || servicePath !== undefined) {
2!
690
    await getAccessibleFDA(service, fdaId, visibility, servicePath);
2✔
691
  }
2✔
692

2✔
693
  const { _id } = (await retrieveFDA(service, fdaId)) ?? {};
2!
694

2✔
695
  if (!service || !_id) {
2!
696
    throw new FDAError(
1✔
697
      404,
2✔
698
      'FDANotFound',
2✔
699
      `FDA ${fdaId} of the service ${service} not found.`,
2✔
700
    );
2✔
701
  }
2✔
702
  const s3Client = await getS3Client(
4!
703
    `${config.objstg.protocol}://${config.objstg.endpoint}`,
4✔
704
    config.objstg.usr,
4✔
705
    config.objstg.pass,
4✔
706
  );
4✔
707
  // This way we remove FDAs independently of if theyre partitioned or not
4✔
708
  const objPaths = await listObjects(s3Client, service, fdaId);
3✔
709
  await dropFiles(s3Client, service, objPaths);
3✔
710

5✔
711
  await removeFDA(service, fdaId);
5✔
712

5✔
713
  const agenda = getAgenda();
5✔
714
  await agenda.cancel({
5✔
715
    name: 'refresh-fda',
5✔
716
    'data.fdaId': fdaId,
5✔
717
  });
5✔
718
  await agenda.cancel({
5✔
719
    name: 'clean-partition',
5!
720
    'data.fdaId': fdaId,
2✔
721
  });
2✔
722
}
2✔
723

3✔
724
export async function getDAs(service, fdaId, visibility, servicePath) {
6✔
725
  if (visibility !== undefined || servicePath !== undefined) {
5!
726
    await getAccessibleFDA(service, fdaId, visibility, servicePath);
5✔
727
  }
4✔
728

4✔
729
  return retrieveDAs(service, fdaId);
5✔
730
}
5✔
731

6✔
732
export async function getDA(service, fdaId, daId, visibility, servicePath) {
4✔
733
  if (visibility !== undefined || servicePath !== undefined) {
4!
734
    await getAccessibleFDA(service, fdaId, visibility, servicePath);
4✔
735
  }
4✔
736

4✔
737
  const da = await retrieveDA(service, fdaId, daId);
4✔
738
  if (da) {
4✔
739
    da.id = daId;
2✔
740
  } else {
3✔
741
    throw new FDAError(
6✔
742
      404,
6✔
743
      'DaNotFound',
6✔
744
      `DA ${daId} not found in FDA ${fdaId} and service ${service}.`,
6✔
745
    );
3✔
746
  }
3✔
747

3✔
748
  return da;
3✔
749
}
5✔
750

4✔
751
export async function putDA(
4✔
752
  service,
3✔
753
  fdaId,
3✔
754
  daId,
3✔
755
  description,
3✔
756
  userQuery,
3✔
757
  params,
3✔
758
  visibility,
3✔
759
  servicePath,
3✔
760
) {
3✔
761
  const conn = await getDBConnection();
6✔
762

5✔
763
  try {
5✔
764
    if (visibility !== undefined || servicePath !== undefined) {
6✔
765
      await getAccessibleFDA(service, fdaId, visibility, servicePath);
6✔
766
    }
4✔
767

4✔
768
    const normalizedParams = checkParams(params);
4✔
769
    await validateDAQuery(conn, service, fdaId, userQuery);
4✔
770
    await updateDA(
4✔
771
      service,
4✔
772
      fdaId,
4✔
773
      daId,
4✔
774
      description,
4✔
775
      userQuery,
4✔
776
      normalizedParams,
4✔
777
    );
4✔
778
  } finally {
6✔
779
    await releaseDBConnection(conn);
6✔
780
  }
6✔
781
}
6✔
782

7✔
783
export async function deleteDA(service, fdaId, daId, visibility, servicePath) {
7✔
784
  if (visibility !== undefined || servicePath !== undefined) {
1✔
785
    await getAccessibleFDA(service, fdaId, visibility, servicePath);
2✔
786
  }
2✔
787

2✔
788
  await removeDA(service, fdaId, daId);
2✔
789
}
2✔
790

5✔
791
export async function cleanPartition(service, fdaId, windowSize, objStgConf) {
5✔
792
  if (!objStgConf?.partition) {
2✔
793
    // DEBATE: With no partitioned folders doesn't make much sense to clean cause we'd had a FDA with no file
2✔
794
    throw new FDAError(
2✔
795
      400,
1✔
796
      'CleaningError',
1✔
797
      `Removing a non partitioned FDA ${fdaId}.`,
1✔
798
    );
1✔
799
  }
1✔
800

1✔
801
  const cutoff = getWindowDate(windowSize);
1✔
802
  if (!cutoff) {
1✔
803
    throw new FDAError(
1✔
804
      400,
1✔
805
      'CleaningError',
1✔
806
      `Incorrect window size in refresh policy.`,
1✔
807
    );
1✔
808
  }
1✔
809

1✔
810
  const s3Client = getS3Client(
1✔
811
    `${config.objstg.protocol}://${config.objstg.endpoint}`,
1✔
812
    config.objstg.usr,
2!
UNCOV
813
    config.objstg.pass,
×
UNCOV
814
  );
×
UNCOV
815

×
816
  const objPaths = await listObjects(s3Client, service, fdaId);
2✔
817

2✔
818
  const partitionsToRemove = [];
2✔
819
  for (const path of objPaths) {
2✔
820
    const partitionDate = extractDate(path);
2!
821

2✔
822
    if (partitionDate < cutoff) {
2!
UNCOV
823
      partitionsToRemove.push(path);
×
UNCOV
824
    }
×
UNCOV
825
  }
×
UNCOV
826
  await dropFiles(s3Client, service, partitionsToRemove);
×
827
}
×
828

3✔
829
async function uploadTableToObjStg(
32✔
830
  service,
32✔
831
  database,
32✔
832
  query,
32✔
833
  bucket,
32✔
834
  path,
32✔
835
  timeColumn,
32✔
836
  objStgConf,
32✔
837
  partitionFlag,
32✔
838
) {
32✔
839
  const s3Client = getS3Client(
32✔
840
    `${config.objstg.protocol}://${config.objstg.endpoint}`,
34✔
841
    config.objstg.usr,
33✔
842
    config.objstg.pass,
33✔
843
  );
33✔
844
  await updateFDAStatus(service, path, 'fetching', 20);
33✔
845
  await uploadTable(s3Client, bucket, database, query, path);
34!
846

32✔
847
  const conn = await getDBConnection();
32✔
848
  try {
32✔
849
    await updateFDAStatus(service, path, 'transforming', 60);
34✔
850

33✔
851
    // DuckDB cant overwrite files in Minio, so for partitioned files we upload them in a tmp file and the move them
34!
852
    // We only do this for files that already exist (partitionFlag=true) so upload performance on partitions doesnt get affected
33✔
853
    const parquetPath = partitionFlag
32✔
854
      ? getPath(bucket, 'tmp/' + path, '.parquet')
32✔
855
      : getPath(bucket, path, '.parquet');
32✔
856

32✔
857
    await toParquet(
32✔
858
      conn,
32✔
859
      getPath(bucket, path, '.csv'),
32✔
860
      parquetPath,
32✔
861
      timeColumn,
32✔
862
      objStgConf?.partition,
33✔
863
      objStgConf?.compression,
44✔
864
    );
44✔
865

40✔
866
    if (partitionFlag) {
44✔
867
      const objectsList = await listObjects(
16✔
868
        s3Client,
5✔
869
        bucket,
5✔
870
        `tmp/${path}.parquet`,
5✔
871
      );
5✔
872
      for (const tempPartition of objectsList) {
5✔
873
        await moveObject(
16✔
874
          s3Client,
16✔
875
          bucket,
16✔
876
          `${bucket}/${tempPartition}`,
16✔
877
          tempPartition.replace('tmp/', ''),
16✔
878
        );
16✔
879
        await dropFile(s3Client, bucket, tempPartition);
16✔
880
      }
16✔
881
    }
16✔
882

40✔
883
    await updateFDAStatus(service, path, 'uploading', 80);
40✔
884
    // DuckDb doesn't replace one row parquet snippet with partitioned file, so we remove it by hand
40✔
885
    if (objStgConf?.partition) {
44✔
886
      await dropFile(s3Client, bucket, `${path}.parquet`);
14✔
887
    }
14✔
888
    await dropFile(s3Client, bucket, `${path}.csv`);
40!
889
  } catch (e) {
32✔
890
    throw new FDAError(500, 'UploadError', e.message);
4✔
891
  } finally {
44✔
892
    await releaseDBConnection(conn);
33✔
893
  }
71✔
894
}
71✔
895

42✔
896
async function ensureFDAReadyForQuery(service, fdaId, visibility, servicePath) {
93!
897
  const fda = await getAccessibleFDA(service, fdaId, visibility, servicePath);
93✔
898

87✔
899
  // Queries are blocked only before the first successful fetch.
49✔
900
  if (!fda.lastFetch) {
91✔
901
    throw new FDAError(
36✔
902
      409,
39✔
903
      'FDAUnavailable',
5✔
904
      `FDA ${fdaId} is not queryable yet because the first fetch has not completed`,
39✔
905
    );
36✔
906
  }
36✔
907
}
88✔
908

37✔
909
async function getStoredFDA(service, fdaId) {
682✔
910
  const fda = await retrieveFDA(service, fdaId);
685!
911

648✔
912
  if (!fda) {
648✔
913
    throw new FDAError(
2✔
914
      404,
2✔
915
      'FDANotFound',
2✔
916
      `FDA ${fdaId} not found in service ${service}`,
2✔
917
    );
39✔
918
  }
36✔
919

680✔
920
  return fda;
680✔
921
}
682✔
922

37✔
923
async function getAccessibleFDA(service, fdaId, visibility, servicePath) {
651✔
924
  const normalizedVisibility = normalizeVisibility(visibility);
660✔
925
  const normalizedServicePath = normalizeServicePath(servicePath);
660✔
926
  const fda = await getStoredFDA(service, fdaId);
660✔
927

656✔
928
  if (normalizeVisibility(fda.visibility) !== normalizedVisibility) {
660✔
929
    throw new FDAError(
13✔
930
      403,
13✔
931
      'VisibilityMismatch',
13✔
932
      `FDA ${fdaId} does not belong to ${normalizedVisibility}`,
13✔
933
    );
13✔
934
  }
13✔
935

651✔
936
  if (normalizeServicePath(fda.servicePath) !== normalizedServicePath) {
659!
937
    throw new FDAError(
9✔
938
      403,
9✔
939
      'ServicePathMismatch',
9✔
940
      `FDA ${fdaId} does not belong to servicePath ${normalizedServicePath}`,
1✔
941
    );
10✔
942
  }
10✔
943

652✔
944
  return fda;
652✔
945
}
660✔
946

13✔
947
function normalizeVisibility(visibility) {
1,352✔
948
  if (!VALID_VISIBILITIES_SET.has(visibility)) {
1,343✔
949
    throw new FDAError(
3✔
950
      400,
3✔
951
      'InvalidVisibility',
3✔
952
      'Visibility must be public or private',
3✔
953
    );
3✔
954
  }
3✔
955

1,341✔
956
  return visibility;
1,341✔
957
}
1,343✔
958

4✔
959
function normalizeServicePath(servicePath) {
1,337✔
960
  if (!servicePath || typeof servicePath !== 'string') {
1,337!
961
    throw new FDAError(
1✔
962
      400,
1✔
963
      'InvalidServicePath',
1✔
964
      'Fiware-ServicePath header is required',
1✔
965
    );
20✔
966
  }
20✔
967

1,356✔
968
  const normalizedServicePath = servicePath.trim();
1,356!
969

1,336✔
970
  if (!/^\/(?:[^/\s]+(?:\/[^/\s]+)*)?$/.test(normalizedServicePath)) {
1,356!
971
    throw new FDAError(
20✔
972
      400,
20✔
973
      'InvalidServicePath',
20✔
974
      'Fiware-ServicePath must be a valid absolute path (e.g. / or /servicepath)',
1✔
975
    );
1✔
976
  }
1✔
977

1,337✔
978
  return normalizedServicePath;
1,337✔
979
}
1,337✔
980

4✔
981
async function createOneRowParquetSync(service, fdaId, query) {
29✔
982
  const s3Client = getS3Client(
29✔
983
    `${config.objstg.protocol}://${config.objstg.endpoint}`,
29✔
984
    config.objstg.usr,
29✔
985
    config.objstg.pass,
29✔
986
  );
29✔
987

29✔
988
  const oneRowQuery = buildOneRowQuery(query);
29✔
989
  await uploadTable(s3Client, service, service, oneRowQuery, fdaId);
29✔
990

29✔
991
  const conn = await getDBConnection();
29✔
992
  try {
29✔
993
    const parquetPath = getPath(service, fdaId, '.parquet');
29✔
994
    await toParquet(conn, getPath(service, fdaId, '.csv'), parquetPath);
29✔
995
    await dropFile(s3Client, service, `${fdaId}.csv`);
29✔
996
  } finally {
29✔
997
    await releaseDBConnection(conn);
29✔
998
  }
29✔
999
}
29✔
1000

4✔
1001
function buildOneRowQuery(query) {
29✔
1002
  const normalizedQuery = query.trim().replace(/;+\s*$/, '');
29✔
1003
  return `SELECT * FROM (${normalizedQuery}) AS fda_one_row LIMIT 1`;
29✔
1004
}
29✔
1005

4✔
1006
async function rollbackFDAProvisioning(service, fdaId) {
1✔
1007
  const s3Client = getS3Client(
1✔
1008
    `${config.objstg.protocol}://${config.objstg.endpoint}`,
1✔
1009
    config.objstg.usr,
1✔
1010
    config.objstg.pass,
1✔
1011
  );
1✔
1012

1✔
1013
  await Promise.allSettled([
1✔
1014
    dropFile(s3Client, service, `${fdaId}.csv`),
1✔
1015
    dropFile(s3Client, service, `${fdaId}.parquet`),
1✔
1016
    removeFDA(service, fdaId),
1✔
1017
  ]);
1✔
1018
}
1✔
1019

4✔
1020
const getPath = (bucket, path, extension) => {
4✔
1021
  const cleanBucket = bucket?.endsWith('/') ? bucket.slice(0, -1) : bucket;
121!
1022
  const cleanPath = path?.startsWith('/') ? path.slice(1) : path;
121!
1023
  return `${cleanBucket}/${cleanPath}${extension}`;
121✔
1024
};
4✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc