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

statuscompliance / status-backend / 19760628403

28 Nov 2025 10:09AM UTC coverage: 91.493% (+2.9%) from 88.591%
19760628403

Pull #246

github

web-flow
Merge fc1380aee into b4c84c6d9
Pull Request #246: feat(databinder): add datasources persistence

1428 of 1642 branches covered (86.97%)

Branch coverage included in aggregate %.

679 of 808 new or added lines in 21 files covered. (84.03%)

2691 of 2860 relevant lines covered (94.09%)

27.81 hits per line

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

48.43
/src/controllers/linker.controller.js
1
import { models } from '../models/models.js';
2
import { getDatabinderCatalog } from '../config/databinder.js';
3
import logger from '../config/logger.js';
4
import { 
5
  generateInstanceId, 
6
  extractResultData,
7
  applyPropertyMapping,
8
  generateCorrelationIds,
9
  createTelemetryContext
10
} from '../utils/databinder/index.js';
11
import { 
12
  normalizeName,
13
  sanitizeLinker,
14
  checkLinkerOwnership,
15
  validateLinkerInput,
16
  validateLinkerUpdateInput,
17
  validateDatasourcesExist,
18
  validateDatasourceConfigs,
19
  generateLinkerExecutionId,
20
  mergeDatasourceResults,
21
  createLinkerExecutionMetadata,
22
  normalizeDatasourceConfigs,
23
  createLinkerExecutionSummary,
24
  cacheLinkerExecution,
25
  getCachedLinkerExecution,
26
  invalidateLinkerCache
27
} from '../utils/databinder/index.js';
28

29
// Get the initialized DatasourceCatalog
30
const datasourceCatalog = getDatabinderCatalog();
78✔
31

32
/**
33
 * Internal function to execute linker and fetch data from all datasources
34
 * This is used both when creating a linker and when executing it via /execute endpoint
35
 * @param {Object} linker - The linker instance
36
 * @param {number} userId - The user ID
37
 * @param {Object} options - Options to pass to datasource methods
38
 * @param {string} executionId - Execution ID for logging
39
 * @param {string} traceId - Trace ID for telemetry
40
 * @param {string} spanId - Span ID for telemetry
41
 * @returns {Promise<Object>} Execution result with status, data, and metadata
42
 */
43
const executeLinkerInternal = async (linker, userId, options = {}, executionId, traceId, spanId) => {
78!
NEW
44
  logger.debug(`[${executionId}] Starting internal linker execution`, {
×
45
    linkerId: linker.id,
46
    linkerName: linker.name,
47
    datasourceCount: linker.datasourceIds.length,
48
    userId,
49
    traceId,
50
    spanId
51
  });
52

NEW
53
  const executionStartTime = Date.now();
×
54

55
  // Fetch all datasources
NEW
56
  const datasources = await models.Datasource.findAll({
×
57
    where: {
58
      id: linker.datasourceIds,
59
      ownerId: userId
60
    }
61
  });
62

NEW
63
  if (datasources.length !== linker.datasourceIds.length) {
×
NEW
64
    const foundIds = datasources.map(ds => ds.id);
×
NEW
65
    const missingIds = linker.datasourceIds.filter(id => !foundIds.includes(id));
×
66
    
NEW
67
    throw new Error(`Some datasources are no longer available. Missing: ${missingIds.join(', ')}`);
×
68
  }
69

70
  // Execute each datasource
NEW
71
  const results = [];
×
72
  
NEW
73
  for (const datasource of datasources) {
×
NEW
74
    try {
×
NEW
75
      const dsConfig = linker.datasourceConfigs?.[datasource.id];
×
NEW
76
      const methodName = dsConfig?.methodConfig?.methodName || linker.defaultMethodName;
×
NEW
77
      const methodOptions = { ...options, ...(dsConfig?.methodConfig?.options || {}) };
×
NEW
78
      const propertyMapping = dsConfig?.propertyMapping || null;
×
79

80
      // Create datasource instance
NEW
81
      const instanceId = generateInstanceId(datasource.id, Date.now(), 'linker');
×
NEW
82
      const instance = datasourceCatalog.createDatasourceInstance(
×
83
        datasource.definitionId,
84
        datasource.config,
85
        instanceId
86
      );
87

88
      // Check if method exists
NEW
89
      if (!instance.methods[methodName]) {
×
NEW
90
        results.push({
×
91
          datasourceId: datasource.id,
92
          datasourceName: datasource.name,
93
          success: false,
94
          error: `Method '${methodName}' not available`,
95
          data: null
96
        });
NEW
97
        continue;
×
98
      }
99

100
      // Execute method
NEW
101
      const callStartTime = Date.now();
×
NEW
102
      const result = await instance.methods[methodName](methodOptions);
×
NEW
103
      const callEndTime = Date.now();
×
104

105
      // Extract and transform data
NEW
106
      const extractedResult = extractResultData(result);
×
NEW
107
      const finalResult = propertyMapping ? applyPropertyMapping(extractedResult, propertyMapping) : extractedResult;
×
108

NEW
109
      results.push({
×
110
        datasourceId: datasource.id,
111
        datasourceName: datasource.name,
112
        definitionId: datasource.definitionId,
113
        methodUsed: methodName,
114
        success: true,
115
        error: null,
116
        data: finalResult,
117
        executionDuration: `${callEndTime - callStartTime}ms`,
118
        propertyMappingApplied: !!propertyMapping
119
      });
120

121
    } catch (error) {
NEW
122
      logger.error(`Error executing datasource ${datasource.id} in linker ${linker.id}:`, error);
×
NEW
123
      results.push({
×
124
        datasourceId: datasource.id,
125
        datasourceName: datasource.name,
126
        success: false,
127
        error: error.message,
128
        data: null
129
      });
130
    }
131
  }
132

133
  // Determine overall execution status
NEW
134
  const allSuccessful = results.every(r => r.success);
×
NEW
135
  const anySuccessful = results.some(r => r.success);
×
NEW
136
  const overallStatus = allSuccessful ? 'success' : (anySuccessful ? 'success' : 'failure');
×
137

NEW
138
  const executionEndTime = Date.now();
×
139

140
  // Always use 'indexed' merge strategy for caching
NEW
141
  const mergedData = mergeDatasourceResults(results, 'indexed');
×
142

143
  // Create execution metadata
NEW
144
  const executionMetadata = createLinkerExecutionMetadata({
×
145
    linkerId: linker.id,
146
    datasourceIds: linker.datasourceIds,
147
    executionId,
148
    startTime: executionStartTime,
149
    endTime: executionEndTime,
150
    results
151
  });
152

153
  // Create execution summary
NEW
154
  const executionSummary = createLinkerExecutionSummary(results);
×
155

NEW
156
  logger.debug(`[${executionId}] Internal linker execution completed`, {
×
157
    executionStatus: overallStatus,
158
    executionDuration: `${executionEndTime - executionStartTime}ms`,
159
    successfulDatasources: executionSummary.successful,
160
    failedDatasources: executionSummary.failed,
161
    traceId,
162
    spanId
163
  });
164

NEW
165
  return {
×
166
    overallStatus,
167
    mergedData,
168
    executionMetadata,
169
    executionSummary,
170
    detailedResults: results,
171
    executionStartTime,
172
    executionEndTime
173
  };
174
};
175

176
export const listLinkers = async (req, res) => {
78✔
177
  try {
4✔
178
    const userId = req.user?.user_id;
4✔
179
    if (!userId) return res.status(401).json({ message: 'Unauthorized' });
4✔
180

181
    const linkers = await models.Linker.findAll({
3✔
182
      where: { ownerId: userId },
183
    });
184

185
    const sanitized = linkers.map((linker) => sanitizeLinker(linker));
2✔
186
    res.json(sanitized);
2✔
187
  } catch (error) {
188
    logger.error('Error listing linkers:', error);
1✔
189
    res.status(500).json({ message: 'Error listing linkers', error: error.message });
1✔
190
  }
191
};
192

193
export const getLinker = async (req, res) => {
78✔
194
  try {
4✔
195
    const userId = req.user?.user_id;
4✔
196
    const { id } = req.params;
4✔
197

198
    const linker = await models.Linker.findByPk(id);
4✔
199

200
    if (!checkLinkerOwnership(linker, userId)) {
3✔
201
      return res.status(404).json({ message: 'Linker not found or access denied' });
2✔
202
    }
203

204
    res.json(sanitizeLinker(linker, true));
1✔
205
  } catch (error) {
206
    logger.error('Error fetching linker:', error);
1✔
207
    res.status(500).json({ message: 'Error fetching linker', error: error.message });
1✔
208
  }
209
};
210

211
export const createLinker = async (req, res) => {
78✔
212
  try {
5✔
213
    const userId = req.user?.user_id;
5✔
214
    if (!userId) return res.status(401).json({ message: 'Unauthorized' });
5✔
215

216
    const { name, defaultMethodName, datasourceIds, datasourceConfigs, description, environment } = req.body;
4✔
217

218
    // Validate input
219
    const validation = validateLinkerInput({ datasourceIds, defaultMethodName, datasourceConfigs });
4✔
220
    if (!validation.isValid) {
4✔
221
      return res.status(400).json({ error: validation.errors.join(', ') });
1✔
222
    }
223

224
    // Validate that all datasource IDs exist and belong to the user
225
    const dsValidation = await validateDatasourcesExist(datasourceIds, models.Datasource, userId);
3✔
226
    if (!dsValidation.isValid) {
3✔
227
      return res.status(400).json({ error: dsValidation.errors.join(', ') });
1✔
228
    }
229

230
    // Validate datasource configs structure if provided
231
    if (datasourceConfigs) {
2✔
232
      const configValidation = validateDatasourceConfigs(datasourceConfigs, datasourceIds);
1✔
233
      if (!configValidation.isValid) {
1!
234
        return res.status(400).json({ error: configValidation.errors.join(', ') });
1✔
235
      }
236
    }
237

238
    // Normalize and check for existing linker with same name
239
    const normalizedName = name ? normalizeName(name) : null;
1!
240
    if (normalizedName) {
1!
241
      const existing = await models.Linker.findOne({
1✔
242
        where: { name: normalizedName, ownerId: userId }
243
      });
244
      if (existing) {
1!
245
        return res.status(409).json({ message: 'A linker with this name already exists.' });
1✔
246
      }
247
    }
248

249
    // Normalize datasource configs
NEW
250
    const normalizedConfigs = normalizeDatasourceConfigs(datasourceConfigs, datasourceIds);
×
251

252
    // Create linker
NEW
253
    const linkerData = {
×
254
      name: normalizedName,
255
      defaultMethodName: defaultMethodName || 'default',
×
256
      datasourceIds,
257
      datasourceConfigs: Object.keys(normalizedConfigs).length > 0 ? normalizedConfigs : null,
×
258
      description: description || null,
×
259
      environment: environment || 'production',
×
260
      isActive: true,
261
      createdBy: req.user.username,
262
      version: 1,
263
      ownerId: userId,
264
    };
265

NEW
266
    const newLinker = await models.Linker.create(linkerData);
×
267

268
    // Execute linker immediately after creation and cache the results
NEW
269
    try {
×
NEW
270
      const { traceId, spanId } = generateCorrelationIds();
×
NEW
271
      const executionId = generateLinkerExecutionId(newLinker.id, Date.now());
×
272
      
NEW
273
      logger.info(`Executing linker immediately after creation: ${newLinker.id}`);
×
274
      
NEW
275
      const executionResult = await executeLinkerInternal(
×
276
        newLinker,
277
        userId,
278
        {},
279
        executionId,
280
        traceId,
281
        spanId
282
      );
283

284
      // Update linker execution status
NEW
285
      await newLinker.update({
×
286
        executionStatus: executionResult.overallStatus,
287
        lastExecutedAt: new Date()
288
      });
289

290
      // Cache the execution result with 2 weeks expiration
NEW
291
      await cacheLinkerExecution(newLinker.id, executionResult.mergedData, {
×
292
        executionId,
293
        executionStatus: executionResult.overallStatus,
294
        executionSummary: executionResult.executionSummary,
295
        traceId,
296
        spanId
297
      });
298

NEW
299
      logger.info(`Linker ${newLinker.id} executed and cached successfully`, {
×
300
        linkerId: newLinker.id,
301
        executionStatus: executionResult.overallStatus
302
      });
303

304
    } catch (execError) {
305
      // Log error but don't fail the creation
NEW
306
      logger.error(`Error executing linker after creation: ${newLinker.id}`, execError);
×
NEW
307
      await newLinker.update({
×
308
        executionStatus: 'failure',
309
        lastExecutedAt: new Date()
310
      });
311
    }
312

NEW
313
    const response = sanitizeLinker(newLinker, true);
×
NEW
314
    res.status(201).json({ 
×
315
      message: 'Linker created successfully',
316
      datasourceCount: datasourceIds.length,
317
      ...response 
318
    });
319
  } catch (error) {
NEW
320
    logger.error('Error creating linker:', error);
×
NEW
321
    res.status(500).json({ message: 'Error creating linker', error: error.message });
×
322
  }
323
};
324

325
export const updateLinker = async (req, res) => {
78✔
326
  try {
8✔
327
    const userId = req.user?.user_id;
8✔
328
    const { id } = req.params;
8✔
329
    const { name, defaultMethodName, datasourceIds, datasourceConfigs, description, environment, isActive } = req.body;
8✔
330

331
    const linker = await models.Linker.findByPk(id);
8✔
332
    if (!checkLinkerOwnership(linker, userId)) {
8✔
333
      return res.status(404).json({ message: 'Linker not found or access denied' });
2✔
334
    }
335

336
    // Validate input
337
    const validation = validateLinkerUpdateInput({ datasourceIds, defaultMethodName, datasourceConfigs });
6✔
338
    if (!validation.isValid) {
6!
NEW
339
      return res.status(400).json({ error: validation.errors.join(', ') });
×
340
    }
341

342
    const updateData = {};
6✔
343

344
    if (name !== undefined) {
6✔
345
      const normalizedName = normalizeName(name);
2✔
346
      // Check for name conflicts
347
      if (normalizedName !== linker.name) {
2!
348
        const existing = await models.Linker.findOne({
2✔
349
          where: { name: normalizedName, ownerId: userId }
350
        });
351
        if (existing && existing.id !== id) {
2✔
352
          return res.status(409).json({ message: 'A linker with this name already exists.' });
1✔
353
        }
354
      }
355
      updateData.name = normalizedName;
1✔
356
    }
357

358
    if (defaultMethodName !== undefined) {
5!
NEW
359
      updateData.defaultMethodName = defaultMethodName;
×
360
    }
361

362
    let shouldInvalidateCache = false;
5✔
363

364
    if (datasourceIds !== undefined) {
5✔
365
      // Validate that all datasource IDs exist and belong to the user
366
      const dsValidation = await validateDatasourcesExist(datasourceIds, models.Datasource, userId);
3✔
367
      if (!dsValidation.isValid) {
3✔
368
        return res.status(400).json({ error: dsValidation.errors.join(', ') });
1✔
369
      }
370

371
      updateData.datasourceIds = datasourceIds;
2✔
372
      updateData.version = linker.version + 1;
2✔
373
      updateData.executionStatus = 'not_executed';
2✔
374
      shouldInvalidateCache = true;
2✔
375
    }
376

377
    if (datasourceConfigs !== undefined) {
4!
NEW
378
      const targetDatasourceIds = datasourceIds || linker.datasourceIds;
×
379
      
380
      // Validate datasource configs structure
NEW
381
      const configValidation = validateDatasourceConfigs(datasourceConfigs, targetDatasourceIds);
×
NEW
382
      if (!configValidation.isValid) {
×
NEW
383
        return res.status(400).json({ error: configValidation.errors.join(', ') });
×
384
      }
385

NEW
386
      updateData.datasourceConfigs = normalizeDatasourceConfigs(datasourceConfigs, targetDatasourceIds);
×
387
      
NEW
388
      if (updateData.version === undefined) {
×
NEW
389
        updateData.version = linker.version + 1;
×
390
      }
NEW
391
      shouldInvalidateCache = true;
×
392
    }
393

394
    if (description !== undefined) {
4!
NEW
395
      updateData.description = description;
×
396
    }
397

398
    if (environment !== undefined) {
4!
NEW
399
      updateData.environment = environment;
×
400
    }
401

402
    if (isActive !== undefined) {
4!
NEW
403
      updateData.isActive = Boolean(isActive);
×
404
    }
405

406
    if (Object.keys(updateData).length === 0) {
4✔
407
      return res.status(400).json({ message: 'No valid fields provided for update.' });
1✔
408
    }
409

410
    updateData.updatedAt = new Date();
3✔
411

412
    await linker.update(updateData);
3✔
413

414
    // Invalidate cache if datasources or configs changed
415
    if (shouldInvalidateCache) {
3✔
416
      await invalidateLinkerCache(linker.id);
2✔
417
      logger.info(`Invalidated cache for linker ${linker.id} due to configuration changes`);
2✔
418
    }
419

420
    res.json({ 
3✔
421
      message: 'Linker updated successfully', 
422
      ...sanitizeLinker(linker, true) 
423
    });
424
  } catch (error) {
NEW
425
    logger.error('Error updating linker:', error);
×
NEW
426
    res.status(500).json({ message: 'Error updating linker', error: error.message });
×
427
  }
428
};
429

430
export const deleteLinker = async (req, res) => {
78✔
431
  try {
4✔
432
    const userId = req.user?.user_id;
4✔
433
    const { id } = req.params;
4✔
434

435
    const linker = await models.Linker.findByPk(id);
4✔
436
    if (!checkLinkerOwnership(linker, userId)) {
3✔
437
      return res.status(404).json({ message: 'Linker not found or access denied' });
2✔
438
    }
439

440
    // Invalidate cache before deleting
441
    await invalidateLinkerCache(linker.id);
1✔
442
    logger.info(`Invalidated cache for linker ${linker.id} before deletion`);
1✔
443

444
    await linker.destroy();
1✔
445
    res.status(204).send();
1✔
446
  } catch (error) {
447
    logger.error('Error deleting linker:', error);
1✔
448
    res.status(500).json({ message: 'Error deleting linker', error: error.message });
1✔
449
  }
450
};
451

452
export const executeLinker = async (req, res) => {
78✔
453
  // Generate trace-like IDs for correlation
NEW
454
  const { traceId, spanId } = generateCorrelationIds();
×
455
  
NEW
456
  try {
×
NEW
457
    const userId = req.user?.user_id;
×
NEW
458
    const { id } = req.params;
×
NEW
459
    const { options = {}, mergeStrategy = 'indexed' } = req.body;
×
460

NEW
461
    const linker = await models.Linker.findByPk(id);
×
NEW
462
    if (!checkLinkerOwnership(linker, userId)) {
×
NEW
463
      return res.status(404).json({ message: 'Linker not found or access denied' });
×
464
    }
465

466
    // Capture execution start time
NEW
467
    const executionStartTime = Date.now();
×
NEW
468
    const executionId = generateLinkerExecutionId(linker.id, executionStartTime);
×
469

NEW
470
    logger.debug(`[${executionId}] Starting linker execution`, {
×
471
      linkerId: linker.id,
472
      linkerName: linker.name,
473
      datasourceCount: linker.datasourceIds.length,
474
      userId,
475
      requestId: req.requestId || 'unknown',
×
476
      traceId,
477
      spanId,
478
      mergeStrategy
479
    });
480

481
    // Check cache first
NEW
482
    const cachedResult = await getCachedLinkerExecution(linker.id);
×
483
    
484
    // If cache is valid (exists and not stale - less than 1 hour old), use it
NEW
485
    if (cachedResult.data && !cachedResult.isStale) {
×
NEW
486
      logger.info(`Using cached data for linker ${linker.id}`, {
×
487
        linkerId: linker.id,
488
        cacheAge: cachedResult.cacheAge,
489
        cacheAgeMinutes: Math.floor(cachedResult.cacheAge / 60000)
490
      });
491

492
      // Always use indexed merge strategy for cached data
NEW
493
      const cachedMergedData = cachedResult.data;
×
494

495
      // Create telemetry context
NEW
496
      const telemetryContext = createTelemetryContext({
×
497
        traceId,
498
        spanId,
499
        operationName: `linker.execute.cached.${linker.id}`,
500
        correlationId: executionId
501
      });
502

NEW
503
      return res.json({
×
504
        message: 'Linker executed from cache',
505
        linkerId: linker.id,
506
        linkerName: linker.name,
507
        executionStatus: 'success',
508
        mergeStrategy: 'indexed',
509
        mergedData: cachedMergedData,
510
        fromCache: true,
511
        cacheAge: cachedResult.cacheAge,
512
        cacheAgeMinutes: Math.floor(cachedResult.cacheAge / 60000),
513
        cachedMetadata: cachedResult.metadata,
514
        telemetryContext
515
      });
516
    }
517

518
    // If cache is stale or doesn't exist, delete old cache and execute fresh
NEW
519
    if (cachedResult.data && cachedResult.isStale) {
×
NEW
520
      logger.info(`Cache is stale for linker ${linker.id}, invalidating and re-executing`, {
×
521
        linkerId: linker.id,
522
        cacheAge: cachedResult.cacheAge,
523
        cacheAgeMinutes: Math.floor(cachedResult.cacheAge / 60000)
524
      });
NEW
525
      await invalidateLinkerCache(linker.id);
×
526
    }
527

528
    // Update execution status to pending
NEW
529
    await linker.update({ 
×
530
      executionStatus: 'pending',
531
      lastExecutedAt: new Date()
532
    });
533

534
    // Execute linker using internal function
NEW
535
    const executionResult = await executeLinkerInternal(
×
536
      linker,
537
      userId,
538
      options,
539
      executionId,
540
      traceId,
541
      spanId
542
    );
543

544
    // Update linker execution status
NEW
545
    await linker.update({ 
×
546
      executionStatus: executionResult.overallStatus,
547
      lastExecutedAt: new Date()
548
    });
549

550
    // Cache the execution result with 2 weeks expiration (always using indexed strategy)
NEW
551
    await cacheLinkerExecution(linker.id, executionResult.mergedData, {
×
552
      executionId,
553
      executionStatus: executionResult.overallStatus,
554
      executionSummary: executionResult.executionSummary,
555
      traceId,
556
      spanId
557
    });
558

NEW
559
    logger.info(`Linker ${linker.id} executed and cached successfully`, {
×
560
      linkerId: linker.id,
561
      executionStatus: executionResult.overallStatus
562
    });
563

564
    // User requested merge strategy may differ from cached (indexed) strategy
565
    // Apply the user's requested merge strategy to the detailed results
NEW
566
    const userMergedData = mergeStrategy === 'indexed' 
×
567
      ? executionResult.mergedData 
568
      : mergeDatasourceResults(executionResult.detailedResults, mergeStrategy);
569

570
    // Create telemetry context
NEW
571
    const telemetryContext = createTelemetryContext({
×
572
      traceId,
573
      spanId,
574
      operationName: `linker.execute.${linker.id}`,
575
      correlationId: executionId
576
    });
577

NEW
578
    res.json({
×
579
      message: `Linker executed with status: ${executionResult.overallStatus}`,
580
      linkerId: linker.id,
581
      linkerName: linker.name,
582
      executionStatus: executionResult.overallStatus,
583
      mergeStrategy,
584
      mergedData: userMergedData,
585
      fromCache: false,
586
      executionMetadata: executionResult.executionMetadata,
587
      executionSummary: executionResult.executionSummary,
588
      detailedResults: executionResult.detailedResults,
589
      telemetryContext
590
    });
591

592
  } catch (error) {
NEW
593
    logger.error('Error executing linker:', error);
×
594
    
595
    // Update linker status to failure
NEW
596
    try {
×
NEW
597
      const linker = await models.Linker.findByPk(req.params.id);
×
NEW
598
      if (linker) {
×
NEW
599
        await linker.update({ executionStatus: 'failure' });
×
600
      }
601
    } catch (updateError) {
NEW
602
      logger.error('Error updating linker status after failure:', updateError);
×
603
    }
604

605
    // Create error telemetry context
NEW
606
    const telemetryContext = createTelemetryContext({
×
607
      traceId,
608
      spanId,
609
      operationName: 'linker.execute.error',
610
      correlationId: `error_${Date.now()}`
611
    });
612

NEW
613
    res.status(500).json({ 
×
614
      message: 'Error executing linker', 
615
      error: error.message,
616
      linkerId: req.params.id,
617
      telemetryContext
618
    });
619
  }
620
};
621

622
export const getLinkerDatasources = async (req, res) => {
78✔
623
  try {
5✔
624
    const userId = req.user?.user_id;
5✔
625
    const { id } = req.params;
5✔
626

627
    const linker = await models.Linker.findByPk(id);
5✔
628
    if (!checkLinkerOwnership(linker, userId)) {
5✔
629
      return res.status(404).json({ message: 'Linker not found or access denied' });
2✔
630
    }
631

632
    // Fetch all datasources
633
    const datasources = await models.Datasource.findAll({
3✔
634
      where: {
635
        id: linker.datasourceIds,
636
        ownerId: userId
637
      }
638
    });
639

640
    // Map datasources with their configs from the linker
641
    const datasourcesWithConfigs = datasources.map(ds => {
2✔
642
      const config = linker.datasourceConfigs?.[ds.id];
2✔
643
      return {
2✔
644
        id: ds.id,
645
        name: ds.name,
646
        definitionId: ds.definitionId,
647
        description: ds.description,
648
        environment: ds.environment,
649
        isActive: ds.isActive,
650
        testStatus: ds.testStatus,
651
        linkerConfig: config || null
3✔
652
      };
653
    });
654

655
    res.json({
2✔
656
      linkerId: linker.id,
657
      linkerName: linker.name,
658
      datasourceCount: datasources.length,
659
      datasources: datasourcesWithConfigs
660
    });
661

662
  } catch (error) {
663
    logger.error('Error getting linker datasources:', error);
1✔
664
    res.status(500).json({ 
1✔
665
      message: 'Error getting linker datasources', 
666
      error: error.message 
667
    });
668
  }
669
};
670

671
export { datasourceCatalog };
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