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

statuscompliance / status-backend / 19679635932

25 Nov 2025 06:13PM UTC coverage: 86.181% (-2.4%) from 88.591%
19679635932

Pull #246

github

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

1420 of 1684 branches covered (84.32%)

Branch coverage included in aggregate %.

628 of 808 new or added lines in 21 files covered. (77.72%)

2646 of 3034 relevant lines covered (87.21%)

25.79 hits per line

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

87.11
/src/controllers/databinder.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
  sanitizeDatasource, 
6
  checkOwnership, 
7
  normalizeName, 
8
  generateInstanceId, 
9
  generateExecutionId,
10
  extractResultData,
11
  validateDatasourceInput, 
12
  validateDatasourceUpdateInput, 
13
  validateDefinitionExists, 
14
  validateDatasourceConfig, 
15
  validateMethodExists,
16
  performPrimaryTest, 
17
  performAdditionalTests, 
18
  determineOverallTestStatus, 
19
  createTestSummary, 
20
  createTestDetails, 
21
  createTestResults,
22
  applyPropertyMapping, 
23
  createMappingMetadata,
24
  generateCorrelationIds, 
25
  createHttpCallDetails, 
26
  createSpanAttributes, 
27
  createCallInfo, 
28
  createResponseMetadata, 
29
  createLogMetadata, 
30
  createTelemetryContext,
31
  getMethodDescription, 
32
  createMethodsInfo 
33
} from '../utils/databinder/index.js';
34

35
// Get the initialized DatasourceCatalog
36
const datasourceCatalog = getDatabinderCatalog();
76✔
37

38
export const listDatasources = async (req, res) => {
76✔
39
  try {
3✔
40
    const userId = req.user?.user_id;
3✔
41
    if (!userId) return res.status(401).json({ message: 'Unauthorized' });
3✔
42

43
    const datasources = await models.Datasource.findAll({
2✔
44
      where: { ownerId: userId },
45
    });
46

47
    const sanitized = datasources.map((ds) => sanitizeDatasource(ds));
1✔
48
    res.json(sanitized);
1✔
49
  } catch (error) {
50
    logger.error('Error getting datasource methods:', error);
1✔
51
    res.status(500).json({ message: 'Error getting datasource methods', error: error.message });
1✔
52
  }
53
};
54

55
export const getDatasource = async (req, res) => {
76✔
56
  try {
3✔
57
    const userId = req.user?.user_id;
3✔
58
    const { id } = req.params;
3✔
59

60
    const datasource = await models.Datasource.findByPk(id);
3✔
61

62
    if (!checkOwnership(datasource, userId)) {
2✔
63
      return res.status(404).json({ message: 'Datasource not found or access denied' });
1✔
64
    }
65

66
    res.json(sanitizeDatasource(datasource, true));
1✔
67
  } catch (error) {
68
    logger.error('Error fetching datasource:', error);
1✔
69
    res.status(500).json({ message: 'Error fetching datasource', error: error.message });
1✔
70
  }
71
};
72

73
export const createDatasource = async (req, res) => {
76✔
74
  try {
7✔
75
    const userId = req.user?.user_id;
7✔
76
    if (!userId) return res.status(401).json({ message: 'Unauthorized' });
7✔
77

78
    const { name, definitionId, config, description, environment } = req.body;
6✔
79

80
    // Validate input
81
    const validation = validateDatasourceInput({ name, definitionId, config });
6✔
82
    if (!validation.isValid) {
6✔
83
      return res.status(400).json({ error: validation.errors.join(', ') });
1✔
84
    }
85

86
    // Validate that the definitionId exists
87
    const availableDefinitions = datasourceCatalog.listDatasourceDefinitions();
5✔
88
    const definitionValidation = validateDefinitionExists(definitionId, availableDefinitions);
5✔
89
    if (!definitionValidation.isValid) {
5✔
90
      return res.status(400).json({ error: definitionValidation.error });
1✔
91
    }
92

93
    const normalizedName = normalizeName(name);
4✔
94

95
    // Check if datasource with same name already exists for this user
96
    const existing = await models.Datasource.findOne({
4✔
97
      where: { name: normalizedName, ownerId: userId }
98
    });
99
    if (existing) {
3✔
100
      return res.status(409).json({ message: 'A datasource with this name already exists.' });
1✔
101
    }
102

103
    // Generate a unique instanceId based on user and name
104
    const instanceId = generateInstanceId(userId, normalizedName);
2✔
105

106
    // Try to create a datasource instance using the catalog to validate config
107
    const configValidation = validateDatasourceConfig(
2✔
108
      datasourceCatalog.createDatasourceInstance.bind(datasourceCatalog),
109
      definitionId,
110
      config,
111
      instanceId
112
    );
113

114
    if (!configValidation.isValid) {
2✔
115
      return res.status(400).json({ 
1✔
116
        message: 'Invalid datasource configuration', 
117
        error: configValidation.error 
118
      });
119
    }
120

121
    // Save to database
122
    const datasourceData = {
1✔
123
      name: normalizedName,
124
      definitionId,
125
      config,
126
      description: description || null,
2✔
127
      environment: environment || 'production',
2✔
128
      isActive: true,
129
      createdBy: req.user.username,
130
      version: 1,
131
      ownerId: userId,
132
    };
133

134
    const newDatasource = await models.Datasource.create(datasourceData);
1✔
135

136
    const response = sanitizeDatasource(newDatasource, true);
1✔
137
    res.status(201).json({ 
1✔
138
      message: 'Datasource created successfully', 
139
      instanceId: configValidation.instance.id,
140
      availableMethods: definitionValidation.definition.availableMethods || Object.keys(configValidation.instance.methods),
2✔
141
      ...response 
142
    });
143
  } catch (error) {
144
    logger.error('Error creating datasource:', error);
1✔
145
    res.status(500).json({ message: 'Error creating datasource', error: error.message });
1✔
146
  }
147
};
148

149
export const updateDatasource = async (req, res) => {
76✔
150
  try {
7✔
151
    const userId = req.user?.user_id;
7✔
152
    const { id } = req.params;
7✔
153
    const { name, definitionId, config, description, environment, isActive } = req.body;
7✔
154

155
    const datasource = await models.Datasource.findByPk(id);
7✔
156
    if (!checkOwnership(datasource, userId)) {
6✔
157
      return res.status(404).json({ message: 'Datasource not found or access denied' });
1✔
158
    }
159

160
    // Validate input
161
    const validation = validateDatasourceUpdateInput({ name, definitionId, config });
5✔
162
    if (!validation.isValid) {
5✔
163
      return res.status(400).json({ error: validation.errors.join(', ') });
1✔
164
    }
165

166
    const updateData = {};
4✔
167

168
    if (name !== undefined) {
4✔
169
      updateData.name = normalizeName(name);
1✔
170
    }
171

172
    if (definitionId !== undefined) {
4!
NEW
173
      updateData.definitionId = definitionId;
×
174
    }
175

176
    if (config !== undefined) {
4✔
177
      // Validate new config using the catalog
178
      const testDefinitionId = definitionId || datasource.definitionId;
2✔
179
      const configValidation = validateDatasourceConfig(
2✔
180
        datasourceCatalog.createDatasourceInstance.bind(datasourceCatalog),
181
        testDefinitionId,
182
        config,
183
        `test_${Date.now()}`
184
      );
185

186
      if (!configValidation.isValid) {
2✔
187
        return res.status(400).json({ 
1✔
188
          message: 'Invalid datasource configuration', 
189
          error: configValidation.error 
190
        });
191
      }
192
      
193
      updateData.config = config;
1✔
194
      updateData.version = datasource.version + 1;
1✔
195
      updateData.testStatus = 'not_tested';
1✔
196
    }
197

198
    if (description !== undefined) {
3!
NEW
199
      updateData.description = description;
×
200
    }
201

202
    if (environment !== undefined) {
3!
NEW
203
      updateData.environment = environment;
×
204
    }
205

206
    if (isActive !== undefined) {
3!
NEW
207
      updateData.isActive = Boolean(isActive);
×
208
    }
209

210
    if (Object.keys(updateData).length === 0) {
3✔
211
      return res.status(400).json({ message: 'No valid fields provided for update.' });
1✔
212
    }
213

214
    updateData.updatedAt = new Date();
2✔
215

216
    await datasource.update(updateData);
2✔
217

218
    res.json({ 
2✔
219
      message: 'Datasource updated successfully', 
220
      ...sanitizeDatasource(datasource, true) 
221
    });
222
  } catch (error) {
223
    logger.error('Error updating datasource:', error);
1✔
224
    res.status(500).json({ message: 'Error updating datasource', error: error.message });
1✔
225
  }
226
};
227

228
export const deleteDatasource = async (req, res) => {
76✔
229
  try {
3✔
230
    const userId = req.user?.user_id;
3✔
231
    const { id } = req.params;
3✔
232

233
    const datasource = await models.Datasource.findByPk(id);
3✔
234
    if (!checkOwnership(datasource, userId)) {
2✔
235
      return res.status(404).json({ message: 'Datasource not found or access denied' });
1✔
236
    }
237

238
    await datasource.destroy();
1✔
239
    res.status(204).send();
1✔
240
  } catch (error) {
241
    logger.error('Error deleting datasource:', error);
1✔
242
    res.status(500).json({ message: 'Error deleting datasource', error: error.message });
1✔
243
  }
244
};
245

246
export const testDatasource = async (req, res) => {
76✔
247
  try {
4✔
248
    const userId = req.user?.user_id;
4✔
249
    const { id } = req.params;
4✔
250

251
    const datasource = await models.Datasource.findByPk(id);
4✔
252
    if (!checkOwnership(datasource, userId)) {
3✔
253
      return res.status(404).json({ message: 'Datasource not found or access denied' });
1✔
254
    }
255

256
    // Update test status to pending
257
    await datasource.update({ 
2✔
258
      testStatus: 'pending',
259
      lastTestedAt: new Date()
260
    });
261

262
    try {
2✔
263
      // Create datasource instance and test it
264
      const instance = datasourceCatalog.createDatasourceInstance(
2✔
265
        datasource.definitionId,
266
        datasource.config,
267
        `test_${datasource.id}_${Date.now()}`
268
      );
269

270
      // Get available methods for this datasource type
271
      const availableMethods = Object.keys(instance.methods);
2✔
272

273
      // Perform primary test (pass datasource config for context)
274
      const { primaryTestMethod, testResult: primaryResult } = await performPrimaryTest(instance, datasource.config);
2✔
275

276
      // Perform additional tests (pass datasource config for context)
277
      const additionalResults = await performAdditionalTests(instance, datasource.definitionId, availableMethods, datasource.config);
2✔
278

279
      // Combine all results
280
      const allResults = [primaryResult, ...additionalResults].filter(Boolean);
2✔
281
      const testResults = createTestResults(primaryResult, additionalResults);
2✔
282
      const testDetails = createTestDetails(datasource.definitionId, availableMethods, allResults);
2✔
283

284
      // Determine overall test status
285
      const overallStatus = determineOverallTestStatus(allResults);
2✔
286

287
      await datasource.update({ 
2✔
288
        testStatus: overallStatus,
289
        lastTestedAt: new Date()
290
      });
291

292
      res.json({ 
2✔
293
        message: `Datasource test completed with status: ${overallStatus}`,
294
        testStatus: overallStatus,
295
        testDetails,
296
        testResults,
297
        summary: createTestSummary(allResults, primaryTestMethod)
298
      });
299
    } catch (testError) {
NEW
300
      await datasource.update({ 
×
301
        testStatus: 'failure',
302
        lastTestedAt: new Date()
303
      });
304

NEW
305
      res.status(400).json({ 
×
306
        message: 'Datasource test failed',
307
        testStatus: 'failure',
308
        error: testError.message,
309
        testDetails: {
310
          datasourceType: datasource.definitionId,
311
          errorDuringSetup: true,
312
          setupError: testError.message
313
        }
314
      });
315
    }
316
  } catch (error) {
317
    logger.error('Error testing datasource:', error);
1✔
318
    res.status(500).json({ message: 'Error testing datasource', error: error.message });
1✔
319
  }
320
};
321

322
export const listAvailableDefinitions = async (req, res) => {
76✔
323
  try {
2✔
324
    const definitions = datasourceCatalog.listDatasourceDefinitions();
2✔
325
    
326
    const formattedDefinitions = definitions.map(def => ({
4✔
327
      id: def.id,
328
      name: def.name,
329
      description: def.description,
330
      configSchema: def.configSchema,
331
      availableMethods: def.availableMethods
332
    }));
333

334
    res.json(formattedDefinitions);
2✔
335
  } catch (error) {
NEW
336
    logger.error('Error listing datasource definitions:', error);
×
NEW
337
    res.status(500).json({ message: 'Error listing datasource definitions', error: error.message });
×
338
  }
339
};
340

341
export const fetchFromDatasource = async (req, res) => {
76✔
342
  // Generate trace-like IDs for correlation
343
  const { traceId, spanId } = generateCorrelationIds();
5✔
344
  
345
  try {
5✔
346
    const userId = req.user?.user_id;
5✔
347
    const { id } = req.params;
5✔
348
    const { methodName = 'default', options = {}, propertyMapping = null } = req.body;
5✔
349

350
    const datasource = await models.Datasource.findByPk(id);
5✔
351
    if (!checkOwnership(datasource, userId)) {
4✔
352
      return res.status(404).json({ message: 'Datasource not found or access denied' });
1✔
353
    }
354

355
    // Capture call start time and generate execution ID
356
    const callStartTime = new Date();
3✔
357
    const executionId = generateExecutionId(datasource.id, callStartTime.getTime());
3✔
358

359
    // Create datasource instance
360
    const instanceId = generateInstanceId(datasource.id, Date.now(), 'fetch');
3✔
361
    const instance = datasourceCatalog.createDatasourceInstance(
3✔
362
      datasource.definitionId,
363
      datasource.config,
364
      instanceId
365
    );
366

367
    // Validate method exists
368
    const methodValidation = validateMethodExists(instance, methodName);
3✔
369
    if (!methodValidation.isValid) {
3✔
370
      return res.status(400).json({ 
1✔
371
        message: methodValidation.error,
372
        availableMethods: methodValidation.availableMethods
373
      });
374
    }
375

376
    // Determine the actual HTTP method and endpoint based on datasource type and config
377
    const httpCallDetails = createHttpCallDetails(datasource.definitionId, datasource.config, options);
2✔
378

379
    logger.debug(`[${executionId}] Starting datasource fetch`, {
2✔
380
      datasourceId: datasource.id,
381
      methodName,
382
      datasourceType: datasource.definitionId,
383
      httpCallDetails,
384
      userId,
385
      requestId: req.requestId || 'unknown',
4✔
386
      traceId,
387
      spanId,
388
      propertyMappingProvided: !!propertyMapping,
389
      mappingRules: propertyMapping || null,
3✔
390
      // Telemetry-style metadata
391
      'databinder.datasource': datasource.definitionId,
392
      'databinder.datasource.id': datasource.id,
393
      'databinder.method': methodName
394
    });
395

396
    // Execute the method and capture timing
397
    const result = await instance.methods[methodName](options);
2✔
NEW
398
    const callEndTime = new Date();
×
NEW
399
    const executionDuration = callEndTime.getTime() - callStartTime.getTime();
×
400

401
    // Extract data from nested structure if it exists
NEW
402
    const extractedResult = extractResultData(result);
×
403

404
    // Apply property mapping if provided
NEW
405
    const finalResult = propertyMapping ? applyPropertyMapping(extractedResult, propertyMapping) : extractedResult;
×
406

NEW
407
    logger.debug(`[${executionId}] Datasource fetch completed successfully`, {
×
408
      executionDuration: `${executionDuration}ms`,
409
      resultSize: JSON.stringify(finalResult).length,
410
      datasourceId: datasource.id,
411
      traceId,
412
      spanId,
413
      propertyMappingApplied: !!propertyMapping,
414
      dataExtracted: result !== extractedResult,
415
      originalStructureHadData: result && typeof result === 'object' && result.data !== undefined
×
416
    });
417

418
    // Create span attributes
NEW
419
    const spanAttributes = createSpanAttributes({
×
420
      httpCallDetails,
421
      datasource,
422
      methodName
423
    });
424

425
    // Create call info
NEW
426
    const callInfo = createCallInfo({
×
427
      executionId,
428
      executionDuration: `${executionDuration}ms`,
429
      callStartTime,
430
      callEndTime,
431
      httpCallDetails,
432
      requestId: req.requestId || 'unknown',
×
433
      traceId,
434
      spanId,
435
      spanAttributes
436
    });
437

438
    // Create telemetry context
NEW
439
    const telemetryContext = createTelemetryContext({
×
440
      traceId,
441
      spanId,
442
      operationName: `databinder.fetch.${datasource.definitionId}.${methodName}`,
443
      correlationId: executionId
444
    });
445

446
    // Create log metadata
NEW
447
    const logMetadata = createLogMetadata({
×
448
      datasourceId: datasource.id,
449
      methodName,
450
      userId,
451
      operationType: 'fetch',
452
      success: true
453
    });
454

455
    // Create mapping metadata
NEW
456
    const mappingMetadata = createMappingMetadata(result, finalResult, propertyMapping);
×
457

458
    // Create response metadata
NEW
459
    const metadata = createResponseMetadata({
×
460
      datasource,
461
      instanceId,
462
      finalResult,
463
      originalResult: result,
464
      extractedResult,
465
      propertyMapping: mappingMetadata,
466
      logMetadata,
467
      telemetryContext
468
    });
469

NEW
470
    res.json({
×
471
      message: 'Data fetched successfully',
472
      datasourceId: datasource.id,
473
      datasourceName: datasource.name,
474
      methodUsed: methodName,
475
      result: finalResult,
476
      callInfo,
477
      metadata
478
    });
479

480
  } catch (error) {
481
    const callEndTime = new Date();
3✔
482
    logger.error('Error fetching from datasource:', error);
3✔
483
    
484
    // Log the error with call context if available
485
    const errorContext = {
3✔
486
      datasourceId: req.params.id,
487
      methodName: req.body?.methodName || 'default',
6✔
488
      error: error.message,
489
      requestId: req.requestId || 'unknown',
6✔
490
      traceId,
491
      spanId
492
    };
493

494
    // Create error span attributes
495
    const spanAttributes = createSpanAttributes({
3✔
496
      httpCallDetails: null,
497
      datasource: { definitionId: req.body?.datasourceType || 'unknown' },
6✔
498
      methodName: req.body?.methodName || 'default',
6✔
499
      error: error.message
500
    });
501

502
    // Create error call info
503
    const callInfo = createCallInfo({
3✔
504
      executionId: `error_${Date.now()}`,
505
      executionDuration: '0ms',
506
      callStartTime: callEndTime,
507
      callEndTime,
508
      httpCallDetails: null,
509
      requestId: req.requestId || 'unknown',
6✔
510
      traceId,
511
      spanId,
512
      spanAttributes,
513
      failed: true,
514
      errorContext
515
    });
516

517
    // Create error telemetry context
518
    const telemetryContext = createTelemetryContext({
3✔
519
      traceId,
520
      spanId,
521
      operationName: 'databinder.fetch.error',
522
      correlationId: `error_${Date.now()}`
523
    });
524

525
    // Create error log metadata
526
    const logMetadata = createLogMetadata({
3✔
527
      datasourceId: req.params.id,
528
      methodName: req.body?.methodName || 'default',
6✔
529
      userId: req.user?.user_id || 'unknown',
3!
530
      operationType: 'fetch',
531
      success: false,
532
      errorMessage: error.message
533
    });
534

535
    res.status(500).json({ 
3✔
536
      message: 'Error fetching from datasource', 
537
      error: error.message,
538
      datasourceId: req.params.id,
539
      callInfo,
540
      metadata: {
541
        timestamp: callEndTime.toISOString(),
542
        logMetadata,
543
        telemetryContext
544
      }
545
    });
546
  }
547
};
548

549
export const getDatasourceMethods = async (req, res) => {
76✔
550
  try {
3✔
551
    const userId = req.user?.user_id;
3✔
552
    const { id } = req.params;
3✔
553

554
    const datasource = await models.Datasource.findByPk(id);
3✔
555
    if (!checkOwnership(datasource, userId)) {
2✔
556
      return res.status(404).json({ message: 'Datasource not found or access denied' });
1✔
557
    }
558

559
    // Create a temporary instance to get available methods
560
    const instance = datasourceCatalog.createDatasourceInstance(
1✔
561
      datasource.definitionId,
562
      datasource.config,
563
      `methods_${datasource.id}_${Date.now()}`
564
    );
565

566
    // Create methods info using utility
567
    const methodsInfo = createMethodsInfo(instance, (methodName) => 
1✔
NEW
568
      getMethodDescription(
×
569
        datasourceCatalog.listDatasourceDefinitions.bind(datasourceCatalog),
570
        datasource.definitionId,
571
        methodName
572
      )
573
    );
574

575
    res.json({
1✔
576
      datasourceId: datasource.id,
577
      datasourceName: datasource.name,
578
      definitionId: datasource.definitionId,
579
      availableMethods: methodsInfo,
580
      methodCount: Object.keys(methodsInfo).length
581
    });
582

583
  } catch (error) {
584
    logger.error('Error getting datasource methods:', error);
1✔
585
    res.status(500).json({ 
1✔
586
      message: 'Error getting datasource methods', 
587
      error: error.message 
588
    });
589
  }
590
};
591

592
export { datasourceCatalog };
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc