@friggframework/core 2.0.0-next.94 → 2.0.0-next.95
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/handlers/routers/reporting.js +9 -0
- package/index.js +8 -0
- package/package.json +5 -5
- package/reporting/index.js +17 -0
- package/reporting/reporting-router.js +48 -0
- package/reporting/repositories/reporting-repository-documentdb.js +127 -0
- package/reporting/repositories/reporting-repository-factory.js +35 -0
- package/reporting/repositories/reporting-repository-interface.js +16 -0
- package/reporting/repositories/reporting-repository-mongo.js +54 -0
- package/reporting/repositories/reporting-repository-postgres.js +70 -0
- package/reporting/use-cases/index.js +6 -0
- package/reporting/use-cases/list-integrations-report.js +137 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
const { createReportingRouter } = require('@friggframework/core');
|
|
2
|
+
const { createAppHandler } = require('./../app-handler-helpers');
|
|
3
|
+
|
|
4
|
+
const router = createReportingRouter();
|
|
5
|
+
|
|
6
|
+
// true → eager-connect Prisma; the reporting endpoints read the DB.
|
|
7
|
+
const handler = createAppHandler('HTTP Event: Reporting', router, true);
|
|
8
|
+
|
|
9
|
+
module.exports = { handler, router };
|
package/index.js
CHANGED
|
@@ -71,6 +71,10 @@ const {
|
|
|
71
71
|
getModulesDefinitionFromIntegrationClasses,
|
|
72
72
|
LoadIntegrationContextUseCase,
|
|
73
73
|
} = require('./integrations/index');
|
|
74
|
+
const {
|
|
75
|
+
createReportingRouter,
|
|
76
|
+
createReportingRepository,
|
|
77
|
+
} = require('./reporting/index');
|
|
74
78
|
const { TimeoutCatcher } = require('./lambda/index');
|
|
75
79
|
const { debug, initDebugLog, flushDebugLog } = require('./logs/index');
|
|
76
80
|
const {
|
|
@@ -139,6 +143,10 @@ module.exports = {
|
|
|
139
143
|
UpdateProcessMetrics,
|
|
140
144
|
GetProcess,
|
|
141
145
|
|
|
146
|
+
// reporting
|
|
147
|
+
createReportingRouter,
|
|
148
|
+
createReportingRepository,
|
|
149
|
+
|
|
142
150
|
// application - Command factories for integration developers
|
|
143
151
|
application,
|
|
144
152
|
createFriggCommands: application.createFriggCommands,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@friggframework/core",
|
|
3
3
|
"prettier": "@friggframework/prettier-config",
|
|
4
|
-
"version": "2.0.0-next.
|
|
4
|
+
"version": "2.0.0-next.95",
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"@aws-sdk/client-apigatewaymanagementapi": "^3.588.0",
|
|
7
7
|
"@aws-sdk/client-kms": "^3.588.0",
|
|
@@ -38,9 +38,9 @@
|
|
|
38
38
|
}
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
|
-
"@friggframework/eslint-config": "2.0.0-next.
|
|
42
|
-
"@friggframework/prettier-config": "2.0.0-next.
|
|
43
|
-
"@friggframework/test": "2.0.0-next.
|
|
41
|
+
"@friggframework/eslint-config": "2.0.0-next.95",
|
|
42
|
+
"@friggframework/prettier-config": "2.0.0-next.95",
|
|
43
|
+
"@friggframework/test": "2.0.0-next.95",
|
|
44
44
|
"@prisma/client": "^6.19.3",
|
|
45
45
|
"@types/lodash": "4.17.15",
|
|
46
46
|
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
|
@@ -80,5 +80,5 @@
|
|
|
80
80
|
"publishConfig": {
|
|
81
81
|
"access": "public"
|
|
82
82
|
},
|
|
83
|
-
"gitHead": "
|
|
83
|
+
"gitHead": "18d7e66ce7f397ad6e96d9006d81118cd96445ec"
|
|
84
84
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const { createReportingRouter } = require('./reporting-router');
|
|
2
|
+
const {
|
|
3
|
+
createReportingRepository,
|
|
4
|
+
ReportingRepositoryMongo,
|
|
5
|
+
ReportingRepositoryPostgres,
|
|
6
|
+
ReportingRepositoryDocumentDB,
|
|
7
|
+
} = require('./repositories/reporting-repository-factory');
|
|
8
|
+
const { ListIntegrationsReport } = require('./use-cases');
|
|
9
|
+
|
|
10
|
+
module.exports = {
|
|
11
|
+
createReportingRouter,
|
|
12
|
+
createReportingRepository,
|
|
13
|
+
ReportingRepositoryMongo,
|
|
14
|
+
ReportingRepositoryPostgres,
|
|
15
|
+
ReportingRepositoryDocumentDB,
|
|
16
|
+
ListIntegrationsReport,
|
|
17
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const catchAsyncError = require('express-async-handler');
|
|
3
|
+
const {
|
|
4
|
+
createReportingRepository,
|
|
5
|
+
} = require('./repositories/reporting-repository-factory');
|
|
6
|
+
const { ListIntegrationsReport } = require('./use-cases/list-integrations-report');
|
|
7
|
+
|
|
8
|
+
function createReportingRouter() {
|
|
9
|
+
const reportingRepository = createReportingRepository();
|
|
10
|
+
const listIntegrationsReport = new ListIntegrationsReport({
|
|
11
|
+
reportingRepository,
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
const router = express.Router();
|
|
15
|
+
router.use(validateApiKey);
|
|
16
|
+
|
|
17
|
+
router.get('/api/v2/reports', (_req, res) => {
|
|
18
|
+
res.json({ service: 'frigg-core-api', reports: ['integrations'] });
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
router.get(
|
|
22
|
+
'/api/v2/reports/integrations',
|
|
23
|
+
catchAsyncError(async (req, res) => {
|
|
24
|
+
const { status, type, userId } = req.query;
|
|
25
|
+
res.json(
|
|
26
|
+
await listIntegrationsReport.execute({ status, type, userId })
|
|
27
|
+
);
|
|
28
|
+
})
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
return router;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function validateApiKey(req, res, next) {
|
|
35
|
+
const apiKey = req.headers['x-frigg-reporting-api-key'];
|
|
36
|
+
|
|
37
|
+
if (!apiKey || apiKey !== process.env.REPORTING_API_KEY) {
|
|
38
|
+
console.error('Unauthorized access attempt to reporting endpoint');
|
|
39
|
+
return res.status(401).json({
|
|
40
|
+
status: 'error',
|
|
41
|
+
message: 'Unauthorized - x-frigg-reporting-api-key header required',
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
next();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
module.exports = { createReportingRouter, validateApiKey };
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
const { prisma } = require('../../database/prisma');
|
|
2
|
+
const { toObjectId, fromObjectId } = require('../../database/documentdb-utils');
|
|
3
|
+
const {
|
|
4
|
+
ReportingRepositoryInterface,
|
|
5
|
+
} = require('./reporting-repository-interface');
|
|
6
|
+
|
|
7
|
+
const DRAIN_BATCH_SIZE = 1000;
|
|
8
|
+
const MAX_BATCHES = 100000;
|
|
9
|
+
|
|
10
|
+
// Drains cursors via getMore rather than reusing documentdb-utils.findMany/
|
|
11
|
+
// aggregate, which return only the first batch (~101 docs) and would silently
|
|
12
|
+
// truncate a deployment-wide report.
|
|
13
|
+
class ReportingRepositoryDocumentDB extends ReportingRepositoryInterface {
|
|
14
|
+
constructor() {
|
|
15
|
+
super();
|
|
16
|
+
this.prisma = prisma;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async findIntegrationsForReport({ status, userId } = {}) {
|
|
20
|
+
const filter = {};
|
|
21
|
+
if (status) filter.status = status;
|
|
22
|
+
if (userId !== undefined && userId !== null) {
|
|
23
|
+
const objectId = toObjectId(userId);
|
|
24
|
+
// An invalid userId means no matches — must not fall through to an
|
|
25
|
+
// unfiltered query that returns the whole deployment.
|
|
26
|
+
if (!objectId) return [];
|
|
27
|
+
filter.userId = objectId;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const docs = await this._findDrained('Integration', filter);
|
|
31
|
+
|
|
32
|
+
return docs.map((doc) => {
|
|
33
|
+
const errors = this._extractErrors(doc);
|
|
34
|
+
return {
|
|
35
|
+
id: fromObjectId(doc?._id),
|
|
36
|
+
type: doc?.config?.type ?? null,
|
|
37
|
+
status: doc?.status ?? null,
|
|
38
|
+
userId: fromObjectId(doc?.userId) ?? null,
|
|
39
|
+
version: doc?.version ?? null,
|
|
40
|
+
errorCount: Array.isArray(errors) ? errors.length : 0,
|
|
41
|
+
moduleCount: Array.isArray(doc?.entityIds)
|
|
42
|
+
? doc.entityIds.length
|
|
43
|
+
: 0,
|
|
44
|
+
createdAt: doc?.createdAt ?? null,
|
|
45
|
+
updatedAt: doc?.updatedAt ?? null,
|
|
46
|
+
};
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async countMappingsByIntegrationIds(ids = []) {
|
|
51
|
+
const counts = new Map();
|
|
52
|
+
if (!ids || ids.length === 0) return counts;
|
|
53
|
+
|
|
54
|
+
// IntegrationMapping.integrationId is stored as a string in DocumentDB,
|
|
55
|
+
// so match by string — an ObjectId $in would never match (always 0).
|
|
56
|
+
const stringIds = ids.map(String);
|
|
57
|
+
|
|
58
|
+
const rows = await this._aggregateDrained('IntegrationMapping', [
|
|
59
|
+
{ $match: { integrationId: { $in: stringIds } } },
|
|
60
|
+
{ $group: { _id: '$integrationId', count: { $sum: 1 } } },
|
|
61
|
+
]);
|
|
62
|
+
|
|
63
|
+
for (const row of rows) {
|
|
64
|
+
counts.set(String(row?._id), row?.count ?? 0);
|
|
65
|
+
}
|
|
66
|
+
return counts;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async _findDrained(collection, filter) {
|
|
70
|
+
const first = await this.prisma.$runCommandRaw({
|
|
71
|
+
find: collection,
|
|
72
|
+
filter,
|
|
73
|
+
batchSize: DRAIN_BATCH_SIZE,
|
|
74
|
+
});
|
|
75
|
+
return this._drain(collection, first);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async _aggregateDrained(collection, pipeline) {
|
|
79
|
+
const first = await this.prisma.$runCommandRaw({
|
|
80
|
+
aggregate: collection,
|
|
81
|
+
pipeline,
|
|
82
|
+
cursor: { batchSize: DRAIN_BATCH_SIZE },
|
|
83
|
+
});
|
|
84
|
+
return this._drain(collection, first);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async _drain(collection, firstResult) {
|
|
88
|
+
const cursor = firstResult?.cursor || {};
|
|
89
|
+
const docs = [...(cursor.firstBatch || [])];
|
|
90
|
+
let cursorId = cursor.id;
|
|
91
|
+
let batches = 0;
|
|
92
|
+
|
|
93
|
+
while (this._cursorOpen(cursorId) && batches < MAX_BATCHES) {
|
|
94
|
+
batches += 1;
|
|
95
|
+
const next = await this.prisma.$runCommandRaw({
|
|
96
|
+
getMore: cursorId,
|
|
97
|
+
collection,
|
|
98
|
+
batchSize: DRAIN_BATCH_SIZE,
|
|
99
|
+
});
|
|
100
|
+
const nextCursor = next?.cursor || {};
|
|
101
|
+
const nextBatch = nextCursor.nextBatch || [];
|
|
102
|
+
docs.push(...nextBatch);
|
|
103
|
+
cursorId = nextCursor.id;
|
|
104
|
+
if (nextBatch.length === 0) break;
|
|
105
|
+
}
|
|
106
|
+
return docs;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
_cursorOpen(id) {
|
|
110
|
+
if (id === undefined || id === null) return false;
|
|
111
|
+
if (typeof id === 'number') return id !== 0;
|
|
112
|
+
if (typeof id === 'bigint') return id !== 0n;
|
|
113
|
+
// Extended JSON can surface a 64-bit cursor id as { $numberLong: "..." }.
|
|
114
|
+
if (typeof id === 'object' && id.$numberLong !== undefined) {
|
|
115
|
+
return id.$numberLong !== '0';
|
|
116
|
+
}
|
|
117
|
+
return String(id) !== '0';
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
_extractErrors(doc) {
|
|
121
|
+
if (Array.isArray(doc?.errors)) return doc.errors;
|
|
122
|
+
if (Array.isArray(doc?.messages?.errors)) return doc.messages.errors;
|
|
123
|
+
return [];
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
module.exports = { ReportingRepositoryDocumentDB };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
const { ReportingRepositoryMongo } = require('./reporting-repository-mongo');
|
|
2
|
+
const {
|
|
3
|
+
ReportingRepositoryPostgres,
|
|
4
|
+
} = require('./reporting-repository-postgres');
|
|
5
|
+
const {
|
|
6
|
+
ReportingRepositoryDocumentDB,
|
|
7
|
+
} = require('./reporting-repository-documentdb');
|
|
8
|
+
const config = require('../../database/config');
|
|
9
|
+
|
|
10
|
+
function createReportingRepository() {
|
|
11
|
+
const dbType = config.DB_TYPE;
|
|
12
|
+
|
|
13
|
+
switch (dbType) {
|
|
14
|
+
case 'mongodb':
|
|
15
|
+
return new ReportingRepositoryMongo();
|
|
16
|
+
|
|
17
|
+
case 'postgresql':
|
|
18
|
+
return new ReportingRepositoryPostgres();
|
|
19
|
+
|
|
20
|
+
case 'documentdb':
|
|
21
|
+
return new ReportingRepositoryDocumentDB();
|
|
22
|
+
|
|
23
|
+
default:
|
|
24
|
+
throw new Error(
|
|
25
|
+
`Unsupported database type: ${dbType}. Supported values: 'mongodb', 'documentdb', 'postgresql'`
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
module.exports = {
|
|
31
|
+
createReportingRepository,
|
|
32
|
+
ReportingRepositoryMongo,
|
|
33
|
+
ReportingRepositoryPostgres,
|
|
34
|
+
ReportingRepositoryDocumentDB,
|
|
35
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
class ReportingRepositoryInterface {
|
|
2
|
+
// returns: [{ id, type, status, userId, version, errorCount, moduleCount, createdAt, updatedAt }]
|
|
3
|
+
async findIntegrationsForReport(filter) {
|
|
4
|
+
throw new Error(
|
|
5
|
+
'Method findIntegrationsForReport must be implemented by subclass'
|
|
6
|
+
);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
async countMappingsByIntegrationIds(ids) {
|
|
10
|
+
throw new Error(
|
|
11
|
+
'Method countMappingsByIntegrationIds must be implemented by subclass'
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
module.exports = { ReportingRepositoryInterface };
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
const { prisma } = require('../../database/prisma');
|
|
2
|
+
const {
|
|
3
|
+
ReportingRepositoryInterface,
|
|
4
|
+
} = require('./reporting-repository-interface');
|
|
5
|
+
|
|
6
|
+
class ReportingRepositoryMongo extends ReportingRepositoryInterface {
|
|
7
|
+
constructor() {
|
|
8
|
+
super();
|
|
9
|
+
this.prisma = prisma;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async findIntegrationsForReport({ status, userId } = {}) {
|
|
13
|
+
const where = {};
|
|
14
|
+
if (status) where.status = status;
|
|
15
|
+
if (userId !== undefined && userId !== null) where.userId = userId;
|
|
16
|
+
|
|
17
|
+
const integrations = await this.prisma.integration.findMany({
|
|
18
|
+
where,
|
|
19
|
+
include: { entities: { select: { id: true } } },
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
return integrations.map((integration) => ({
|
|
23
|
+
id: integration.id,
|
|
24
|
+
type: integration.config?.type ?? null,
|
|
25
|
+
status: integration.status ?? null,
|
|
26
|
+
userId: integration.userId ?? null,
|
|
27
|
+
version: integration.version ?? null,
|
|
28
|
+
errorCount: Array.isArray(integration.errors)
|
|
29
|
+
? integration.errors.length
|
|
30
|
+
: 0,
|
|
31
|
+
moduleCount: integration.entities?.length ?? 0,
|
|
32
|
+
createdAt: integration.createdAt ?? null,
|
|
33
|
+
updatedAt: integration.updatedAt ?? null,
|
|
34
|
+
}));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async countMappingsByIntegrationIds(ids = []) {
|
|
38
|
+
const counts = new Map();
|
|
39
|
+
if (!ids || ids.length === 0) return counts;
|
|
40
|
+
|
|
41
|
+
const groups = await this.prisma.integrationMapping.groupBy({
|
|
42
|
+
by: ['integrationId'],
|
|
43
|
+
where: { integrationId: { in: ids } },
|
|
44
|
+
_count: { _all: true },
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
for (const group of groups) {
|
|
48
|
+
counts.set(group.integrationId, group._count._all);
|
|
49
|
+
}
|
|
50
|
+
return counts;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
module.exports = { ReportingRepositoryMongo };
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
const { prisma } = require('../../database/prisma');
|
|
2
|
+
const {
|
|
3
|
+
ReportingRepositoryInterface,
|
|
4
|
+
} = require('./reporting-repository-interface');
|
|
5
|
+
|
|
6
|
+
class ReportingRepositoryPostgres extends ReportingRepositoryInterface {
|
|
7
|
+
constructor() {
|
|
8
|
+
super();
|
|
9
|
+
this.prisma = prisma;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async findIntegrationsForReport({ status, userId } = {}) {
|
|
13
|
+
const where = {};
|
|
14
|
+
if (status) where.status = status;
|
|
15
|
+
if (userId !== undefined && userId !== null) {
|
|
16
|
+
where.userId = this._convertId(userId);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const integrations = await this.prisma.integration.findMany({
|
|
20
|
+
where,
|
|
21
|
+
include: { entities: { select: { id: true } } },
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
return integrations.map((integration) => ({
|
|
25
|
+
id: integration.id?.toString(),
|
|
26
|
+
type: integration.config?.type ?? null,
|
|
27
|
+
status: integration.status ?? null,
|
|
28
|
+
userId: integration.userId?.toString() ?? null,
|
|
29
|
+
version: integration.version ?? null,
|
|
30
|
+
errorCount: Array.isArray(integration.errors)
|
|
31
|
+
? integration.errors.length
|
|
32
|
+
: 0,
|
|
33
|
+
moduleCount: integration.entities?.length ?? 0,
|
|
34
|
+
createdAt: integration.createdAt ?? null,
|
|
35
|
+
updatedAt: integration.updatedAt ?? null,
|
|
36
|
+
}));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async countMappingsByIntegrationIds(ids = []) {
|
|
40
|
+
const counts = new Map();
|
|
41
|
+
if (!ids || ids.length === 0) return counts;
|
|
42
|
+
|
|
43
|
+
const intIds = ids.map((id) => this._convertId(id));
|
|
44
|
+
const groups = await this.prisma.integrationMapping.groupBy({
|
|
45
|
+
by: ['integrationId'],
|
|
46
|
+
where: { integrationId: { in: intIds } },
|
|
47
|
+
_count: { _all: true },
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
for (const group of groups) {
|
|
51
|
+
counts.set(group.integrationId?.toString(), group._count._all);
|
|
52
|
+
}
|
|
53
|
+
return counts;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
_convertId(id) {
|
|
57
|
+
if (id === null || id === undefined) return id;
|
|
58
|
+
// Reject anything that isn't an exact integer — parseInt would coerce
|
|
59
|
+
// '12abc'/'12.9' to 12 and return the wrong record.
|
|
60
|
+
const str = String(id).trim();
|
|
61
|
+
if (!/^-?\d+$/.test(str)) {
|
|
62
|
+
throw new TypeError(
|
|
63
|
+
`Invalid ID: ${id} cannot be converted to integer`
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
return Number.parseInt(str, 10);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
module.exports = { ReportingRepositoryPostgres };
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
const Boom = require('@hapi/boom');
|
|
2
|
+
|
|
3
|
+
const SCHEMA_VERSION = 1;
|
|
4
|
+
const SERVICE = 'frigg-core-api';
|
|
5
|
+
|
|
6
|
+
// Seeded so every known status appears (even at 0); unknown values added to
|
|
7
|
+
// the schema later are still counted dynamically.
|
|
8
|
+
const KNOWN_STATUSES = [
|
|
9
|
+
'ENABLED',
|
|
10
|
+
'ERROR',
|
|
11
|
+
'NEEDS_CONFIG',
|
|
12
|
+
'PROCESSING',
|
|
13
|
+
'DISABLED',
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
class ListIntegrationsReport {
|
|
17
|
+
constructor({ reportingRepository } = {}) {
|
|
18
|
+
if (!reportingRepository) {
|
|
19
|
+
throw new Error('reportingRepository is required');
|
|
20
|
+
}
|
|
21
|
+
this.reportingRepository = reportingRepository;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async execute(query = {}) {
|
|
25
|
+
const { status, type, userId } = this._validateQuery(query);
|
|
26
|
+
|
|
27
|
+
const rows = await this.reportingRepository.findIntegrationsForReport({
|
|
28
|
+
status,
|
|
29
|
+
userId,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// type lives in config.type (a JSON path not portably groupable across
|
|
33
|
+
// DBs), so it is filtered here rather than in the repository query.
|
|
34
|
+
const filtered =
|
|
35
|
+
type === undefined
|
|
36
|
+
? rows
|
|
37
|
+
: rows.filter((row) => (row.type ?? 'unknown') === type);
|
|
38
|
+
|
|
39
|
+
const ids = filtered.map((row) => row.id);
|
|
40
|
+
const mappingCounts = ids.length
|
|
41
|
+
? await this.reportingRepository.countMappingsByIntegrationIds(ids)
|
|
42
|
+
: new Map();
|
|
43
|
+
|
|
44
|
+
const integrations = filtered.map((row) => ({
|
|
45
|
+
id: row.id,
|
|
46
|
+
type: row.type ?? 'unknown',
|
|
47
|
+
status: row.status ?? null,
|
|
48
|
+
userId: row.userId ?? null,
|
|
49
|
+
version: row.version ?? null,
|
|
50
|
+
moduleCount: row.moduleCount ?? 0,
|
|
51
|
+
errorCount: row.errorCount ?? 0,
|
|
52
|
+
mappedRecordCount: mappingCounts.get(row.id) ?? 0,
|
|
53
|
+
createdAt: toIso(row.createdAt),
|
|
54
|
+
updatedAt: toIso(row.updatedAt),
|
|
55
|
+
}));
|
|
56
|
+
|
|
57
|
+
const byStatus = emptyStatusCounts();
|
|
58
|
+
const byTypeMap = new Map();
|
|
59
|
+
for (const integration of integrations) {
|
|
60
|
+
// sentinel so a missing status never becomes a literal "null" key
|
|
61
|
+
const statusKey = integration.status ?? 'UNKNOWN';
|
|
62
|
+
byStatus[statusKey] = (byStatus[statusKey] ?? 0) + 1;
|
|
63
|
+
|
|
64
|
+
if (!byTypeMap.has(integration.type)) {
|
|
65
|
+
byTypeMap.set(integration.type, {
|
|
66
|
+
type: integration.type,
|
|
67
|
+
total: 0,
|
|
68
|
+
byStatus: emptyStatusCounts(),
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
const bucket = byTypeMap.get(integration.type);
|
|
72
|
+
bucket.total += 1;
|
|
73
|
+
bucket.byStatus[statusKey] = (bucket.byStatus[statusKey] ?? 0) + 1;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
schemaVersion: SCHEMA_VERSION,
|
|
78
|
+
service: SERVICE,
|
|
79
|
+
generatedAt: new Date().toISOString(),
|
|
80
|
+
filters: {
|
|
81
|
+
status: status ?? null,
|
|
82
|
+
type: type ?? null,
|
|
83
|
+
userId: userId ?? null,
|
|
84
|
+
},
|
|
85
|
+
metrics: {
|
|
86
|
+
total: integrations.length,
|
|
87
|
+
byStatus,
|
|
88
|
+
byType: Array.from(byTypeMap.values()),
|
|
89
|
+
integrations,
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
_validateQuery({ status, type, userId } = {}) {
|
|
95
|
+
for (const [key, value] of Object.entries({ status, type, userId })) {
|
|
96
|
+
if (value !== undefined && value !== null && typeof value !== 'string') {
|
|
97
|
+
throw Boom.badRequest(
|
|
98
|
+
`Invalid query parameter '${key}': expected a string`
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
const normalize = (value) => value || undefined;
|
|
103
|
+
const normalized = {
|
|
104
|
+
status: normalize(status),
|
|
105
|
+
type: normalize(type),
|
|
106
|
+
userId: normalize(userId),
|
|
107
|
+
};
|
|
108
|
+
if (normalized.status && !KNOWN_STATUSES.includes(normalized.status)) {
|
|
109
|
+
throw Boom.badRequest(
|
|
110
|
+
`Invalid status '${normalized.status}'. Expected one of: ${KNOWN_STATUSES.join(
|
|
111
|
+
', '
|
|
112
|
+
)}`
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
return normalized;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function emptyStatusCounts() {
|
|
120
|
+
return KNOWN_STATUSES.reduce((acc, status) => {
|
|
121
|
+
acc[status] = 0;
|
|
122
|
+
return acc;
|
|
123
|
+
}, {});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function toIso(value) {
|
|
127
|
+
if (!value) return null;
|
|
128
|
+
if (value instanceof Date) return value.toISOString();
|
|
129
|
+
// DocumentDB raw reads surface dates as { $date: ... }
|
|
130
|
+
if (typeof value === 'object' && value.$date) {
|
|
131
|
+
const date = new Date(value.$date);
|
|
132
|
+
return Number.isNaN(date.getTime()) ? null : date.toISOString();
|
|
133
|
+
}
|
|
134
|
+
return String(value);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
module.exports = { ListIntegrationsReport, SCHEMA_VERSION };
|