@friggframework/core 2.0.0-next.103 → 2.0.0-next.105
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/admin-scripts/repositories/admin-script-execution-repository-interface.js +7 -2
- package/admin-scripts/repositories/admin-script-execution-repository-mongo.js +16 -6
- package/admin-scripts/repositories/admin-script-execution-repository-postgres.js +16 -6
- package/application/commands/admin-script-commands.js +2 -1
- package/application/commands/credential-commands.js +17 -0
- package/application/commands/integration-commands.js +14 -1
- package/application/commands/integration-mapping-commands.js +25 -0
- package/application/commands/report-commands.js +188 -0
- package/artifacts/repositories/artifact-repository-factory.js +19 -0
- package/artifacts/repositories/artifact-repository-interface.js +27 -0
- package/artifacts/repositories/artifact-repository-local.js +42 -0
- package/artifacts/repositories/artifact-repository-s3.js +61 -0
- package/credential/repositories/credential-active-type.js +32 -0
- package/credential/repositories/credential-repository-documentdb.js +51 -0
- package/credential/repositories/credential-repository-interface.js +15 -0
- package/credential/repositories/credential-repository-mongo.js +25 -0
- package/credential/repositories/credential-repository-postgres.js +25 -0
- package/database/documentdb-utils.js +56 -0
- package/database/use-cases/resolve-migration-via-worker-use-case.js +49 -0
- package/database/utils/prisma-runner.js +16 -2
- package/handlers/app-definition-loader.js +3 -2
- package/handlers/routers/db-migration.js +36 -18
- package/handlers/workers/db-migration.js +75 -0
- package/index.js +8 -4
- package/integrations/repositories/integration-mapping-repository-documentdb.js +23 -0
- package/integrations/repositories/integration-mapping-repository-interface.js +14 -0
- package/integrations/repositories/integration-mapping-repository-mongo.js +22 -0
- package/integrations/repositories/integration-mapping-repository-postgres.js +28 -0
- package/integrations/repositories/integration-repository-documentdb.js +38 -0
- package/integrations/repositories/integration-repository-interface.js +16 -0
- package/integrations/repositories/integration-repository-mongo.js +36 -0
- package/integrations/repositories/integration-repository-postgres.js +40 -0
- package/integrations/repositories/report-id.js +13 -0
- package/package.json +7 -5
- package/reporting/README.md +109 -48
- package/reporting/builtin-reports.js +6 -0
- package/reporting/index.js +9 -13
- package/reporting/report-base.js +49 -0
- package/reporting/{use-cases/list-integrations-report.js → reports/integrations-report.js} +69 -37
- package/handlers/routers/reporting.js +0 -9
- package/reporting/reporting-router.js +0 -84
- package/reporting/repositories/reporting-repository-documentdb.js +0 -127
- package/reporting/repositories/reporting-repository-factory.js +0 -35
- package/reporting/repositories/reporting-repository-interface.js +0 -16
- package/reporting/repositories/reporting-repository-mongo.js +0 -54
- package/reporting/repositories/reporting-repository-postgres.js +0 -70
- package/reporting/use-cases/index.js +0 -6
|
@@ -89,6 +89,42 @@ class IntegrationRepositoryMongo extends IntegrationRepositoryInterface {
|
|
|
89
89
|
}));
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
/**
|
|
93
|
+
* Find every integration in a report-shaped projection.
|
|
94
|
+
*
|
|
95
|
+
* type lives in config.type (a JSON path not portably groupable across
|
|
96
|
+
* DBs); it is left in the row for the caller to bucket.
|
|
97
|
+
*
|
|
98
|
+
* @param {Object} [filter={}]
|
|
99
|
+
* @param {string} [filter.status] - Integration status
|
|
100
|
+
* @param {string} [filter.userId] - Owning user ID (ObjectId as string)
|
|
101
|
+
* @returns {Promise<Array>} Report-shaped integration rows
|
|
102
|
+
*/
|
|
103
|
+
async findAllForReport({ status, userId } = {}) {
|
|
104
|
+
const where = {};
|
|
105
|
+
if (status) where.status = status;
|
|
106
|
+
if (userId !== undefined && userId !== null) where.userId = userId;
|
|
107
|
+
|
|
108
|
+
const integrations = await this.prisma.integration.findMany({
|
|
109
|
+
where,
|
|
110
|
+
include: { entities: { select: { id: true } } },
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
return integrations.map((integration) => ({
|
|
114
|
+
id: integration.id,
|
|
115
|
+
type: integration.config?.type ?? null,
|
|
116
|
+
status: integration.status ?? null,
|
|
117
|
+
userId: integration.userId ?? null,
|
|
118
|
+
version: integration.version ?? null,
|
|
119
|
+
errorCount: Array.isArray(integration.errors)
|
|
120
|
+
? integration.errors.length
|
|
121
|
+
: 0,
|
|
122
|
+
moduleCount: integration.entities?.length ?? 0,
|
|
123
|
+
createdAt: integration.createdAt ?? null,
|
|
124
|
+
updatedAt: integration.updatedAt ?? null,
|
|
125
|
+
}));
|
|
126
|
+
}
|
|
127
|
+
|
|
92
128
|
/**
|
|
93
129
|
* Delete integration by ID
|
|
94
130
|
* Replaces: IntegrationModel.deleteOne({ _id: integrationId })
|
|
@@ -3,6 +3,7 @@ const {
|
|
|
3
3
|
IntegrationRepositoryInterface,
|
|
4
4
|
} = require('./integration-repository-interface');
|
|
5
5
|
const { validateConfigPatch } = require('./config-patch-shared');
|
|
6
|
+
const { strictIntId } = require('./report-id');
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* PostgreSQL Integration Repository Adapter
|
|
@@ -129,6 +130,45 @@ class IntegrationRepositoryPostgres extends IntegrationRepositoryInterface {
|
|
|
129
130
|
});
|
|
130
131
|
}
|
|
131
132
|
|
|
133
|
+
/**
|
|
134
|
+
* Find every integration in a report-shaped projection.
|
|
135
|
+
*
|
|
136
|
+
* type lives in config.type (a JSON path not portably groupable across
|
|
137
|
+
* DBs); it is left in the row for the caller to bucket.
|
|
138
|
+
*
|
|
139
|
+
* @param {Object} [filter={}]
|
|
140
|
+
* @param {string} [filter.status] - Integration status
|
|
141
|
+
* @param {string|number} [filter.userId] - Owning user ID
|
|
142
|
+
* @returns {Promise<Array>} Report-shaped integration rows
|
|
143
|
+
*/
|
|
144
|
+
async findAllForReport({ status, userId } = {}) {
|
|
145
|
+
const where = {};
|
|
146
|
+
if (status) where.status = status;
|
|
147
|
+
if (userId !== undefined && userId !== null) {
|
|
148
|
+
// Strict: parseInt would coerce '12abc'/'12.9' to 12 and read the wrong user.
|
|
149
|
+
where.userId = strictIntId(userId);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const integrations = await this.prisma.integration.findMany({
|
|
153
|
+
where,
|
|
154
|
+
include: { entities: { select: { id: true } } },
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
return integrations.map((integration) => ({
|
|
158
|
+
id: integration.id?.toString(),
|
|
159
|
+
type: integration.config?.type ?? null,
|
|
160
|
+
status: integration.status ?? null,
|
|
161
|
+
userId: integration.userId?.toString() ?? null,
|
|
162
|
+
version: integration.version ?? null,
|
|
163
|
+
errorCount: Array.isArray(integration.errors)
|
|
164
|
+
? integration.errors.length
|
|
165
|
+
: 0,
|
|
166
|
+
moduleCount: integration.entities?.length ?? 0,
|
|
167
|
+
createdAt: integration.createdAt ?? null,
|
|
168
|
+
updatedAt: integration.updatedAt ?? null,
|
|
169
|
+
}));
|
|
170
|
+
}
|
|
171
|
+
|
|
132
172
|
/**
|
|
133
173
|
* Delete integration by ID
|
|
134
174
|
*
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Coerces an id to an integer, rejecting partially-numeric input ('12abc',
|
|
3
|
+
* '12.9') that parseInt would silently truncate to 12 and read the wrong record.
|
|
4
|
+
*/
|
|
5
|
+
function strictIntId(id) {
|
|
6
|
+
const str = String(id).trim();
|
|
7
|
+
if (!/^-?\d+$/.test(str)) {
|
|
8
|
+
throw new TypeError(`Invalid ID: ${id} cannot be converted to integer`);
|
|
9
|
+
}
|
|
10
|
+
return Number.parseInt(str, 10);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
module.exports = { strictIntId };
|
package/package.json
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
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.105",
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"@aws-sdk/client-apigatewaymanagementapi": "^3.588.0",
|
|
7
7
|
"@aws-sdk/client-kms": "^3.588.0",
|
|
8
8
|
"@aws-sdk/client-lambda": "^3.714.0",
|
|
9
|
+
"@aws-sdk/client-s3": "^3.588.0",
|
|
9
10
|
"@aws-sdk/client-sqs": "^3.588.0",
|
|
10
11
|
"@aws-sdk/client-ssm": "^3.588.0",
|
|
12
|
+
"@aws-sdk/s3-request-presigner": "^3.588.0",
|
|
11
13
|
"@hapi/boom": "^10.0.1",
|
|
12
14
|
"@opentelemetry/api": "^1.9.1",
|
|
13
15
|
"@opentelemetry/context-async-hooks": "^2.9.0",
|
|
@@ -46,9 +48,9 @@
|
|
|
46
48
|
}
|
|
47
49
|
},
|
|
48
50
|
"devDependencies": {
|
|
49
|
-
"@friggframework/eslint-config": "2.0.0-next.
|
|
50
|
-
"@friggframework/prettier-config": "2.0.0-next.
|
|
51
|
-
"@friggframework/test": "2.0.0-next.
|
|
51
|
+
"@friggframework/eslint-config": "2.0.0-next.105",
|
|
52
|
+
"@friggframework/prettier-config": "2.0.0-next.105",
|
|
53
|
+
"@friggframework/test": "2.0.0-next.105",
|
|
52
54
|
"@prisma/client": "^6.19.3",
|
|
53
55
|
"@types/lodash": "4.17.15",
|
|
54
56
|
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
|
@@ -88,5 +90,5 @@
|
|
|
88
90
|
"publishConfig": {
|
|
89
91
|
"access": "public"
|
|
90
92
|
},
|
|
91
|
-
"gitHead": "
|
|
93
|
+
"gitHead": "d200bd6b1385a33736b67f48e3aa5ac3f5e167c8"
|
|
92
94
|
}
|
package/reporting/README.md
CHANGED
|
@@ -1,27 +1,94 @@
|
|
|
1
1
|
# Reporting
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
Deployment-wide reports for `@friggframework/core`, modelled as **admin
|
|
4
|
+
operations** (ADR-010) — a sibling of the Admin Script Runner (ADR-005) on the
|
|
5
|
+
same primitives: one registry, the shared admin API key, sync/async execution,
|
|
6
|
+
the isolated admin execution store, and EventBridge scheduling.
|
|
7
|
+
|
|
8
|
+
A report is a definition (`ReportBase`) whose output is its payload. **Core
|
|
9
|
+
ships built-in reports; adopters register their own** — both are discovered,
|
|
10
|
+
listed, and executed through the same runner. This package provides `ReportBase`,
|
|
11
|
+
the built-in reports, and `createReportCommands()`. The HTTP router, runner, and
|
|
12
|
+
executor live in `@friggframework/admin-scripts`; the infrastructure (queue,
|
|
13
|
+
executor Lambda, artifact bucket, scheduler) is generated by the devtools
|
|
14
|
+
`AdminScriptBuilder`.
|
|
15
|
+
|
|
16
|
+
## Registering reports
|
|
17
|
+
|
|
18
|
+
```js
|
|
19
|
+
// app definition — same shape as adminScripts
|
|
20
|
+
const Definition = {
|
|
21
|
+
name: 'my-app',
|
|
22
|
+
integrations: [HubSpotIntegration, SalesforceIntegration],
|
|
23
|
+
reports: [ConnectedAccountsActivity], // adopter-defined
|
|
24
|
+
admin: { includeBuiltinReports: true }, // + core built-ins (integrations, ...)
|
|
25
|
+
};
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
A report extends `ReportBase` and reads only through the injected admin command
|
|
29
|
+
bundle (`frigg` / `this.context.commands`), never a repository directly:
|
|
30
|
+
|
|
31
|
+
```js
|
|
32
|
+
const { ReportBase } = require('@friggframework/core');
|
|
33
|
+
|
|
34
|
+
class ConnectedAccountsActivity extends ReportBase {
|
|
35
|
+
static Definition = {
|
|
36
|
+
name: 'connected-accounts-activity',
|
|
37
|
+
version: '1.0.0',
|
|
38
|
+
runModes: ['snapshot', 'recorded', 'live'], // first is the default
|
|
39
|
+
inputSchema: { type: 'object', properties: {
|
|
40
|
+
windowDays: { type: 'integer', enum: [30, 60, 90], default: 30 } } },
|
|
41
|
+
output: { format: 'json' }, // 'csv'|'pdf'|'zip' → artifact storage
|
|
42
|
+
schedule: { enabled: true, cron: 'cron(0 6 * * ? *)', mode: 'snapshot' }, // default mode only; activate via PUT
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
async execute(frigg, params) {
|
|
46
|
+
const since = daysAgo(params.windowDays ?? 30);
|
|
47
|
+
const byType = await frigg.credentials.countActiveByType({ since });
|
|
48
|
+
return { windowDays: params.windowDays ?? 30, byType };
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
```
|
|
6
52
|
|
|
7
53
|
## Auth
|
|
8
54
|
|
|
9
|
-
|
|
10
|
-
|
|
55
|
+
All report endpoints use the **shared admin API key** (ADR-005): the
|
|
56
|
+
`x-frigg-admin-api-key` header, validated against `ADMIN_API_KEY`. The dedicated
|
|
57
|
+
reporting key (`x-frigg-reporting-api-key` / `REPORTING_API_KEY`) is **retired**.
|
|
58
|
+
|
|
59
|
+
## Run modes
|
|
60
|
+
|
|
61
|
+
Each invocation picks a mode; persistence follows from it. All three run through
|
|
62
|
+
the one runner and the isolated admin execution store (`AdminScriptExecution`,
|
|
63
|
+
`type: 'REPORT'` — no user/integration FK, so a user-scoped query can never
|
|
64
|
+
return a report record).
|
|
65
|
+
|
|
66
|
+
| Mode | Persists | Use it for |
|
|
67
|
+
| --- | --- | --- |
|
|
68
|
+
| `live` | nothing — computes and returns inline | cheap, always-fresh queries |
|
|
69
|
+
| `recorded` | an execution record (input + results + logs) | audit trail, async polling, expensive scans |
|
|
70
|
+
| `snapshot` | a recorded run tagged into a named series | trends over time |
|
|
71
|
+
|
|
72
|
+
Non-JSON `output.format` (`csv`/`pdf`/`zip`) is written to artifact storage (S3,
|
|
73
|
+
private + signed URL) instead of inline; JSON stays inline.
|
|
11
74
|
|
|
12
75
|
## Endpoints
|
|
13
76
|
|
|
14
77
|
| Method | Path | Description |
|
|
15
78
|
| --- | --- | --- |
|
|
16
|
-
| `GET` | `/api/v2/reports` |
|
|
17
|
-
| `GET` | `/api/v2/reports
|
|
79
|
+
| `GET` | `/api/v2/reports` | List registered report definitions. |
|
|
80
|
+
| `GET` | `/api/v2/reports/:name` | Report definition detail (run modes, input schema, schedule). |
|
|
81
|
+
| `POST` | `/api/v2/reports/:name/run` | Run `{ mode, params }`. `live` → `200` inline; `recorded`/`snapshot` → `202 { executionId }` (queued). |
|
|
82
|
+
| `GET` | `/api/v2/reports/:name/snapshots?from=&to=` | A report's snapshot series (trend); each point carries a signed `artifactUrl` when the run produced a non-JSON artifact. |
|
|
83
|
+
| `GET` | `/api/v2/reports/executions/:id` | Fetch one report execution (guarded to `type: 'REPORT'`); a stored artifact is returned as a signed `results.artifactUrl`. |
|
|
84
|
+
| `GET`/`PUT`/`DELETE` | `/api/v2/reports/:name/schedule` | Manage a report's recurring schedule. |
|
|
85
|
+
| `GET` | `/api/v2/reports/integrations` | Deprecated back-compat for PR #607 — runs the built-in `integrations` report in `live` mode and returns its payload directly. Prefer `POST /:name/run`. |
|
|
18
86
|
|
|
19
|
-
|
|
87
|
+
## Built-in: `integrations`
|
|
20
88
|
|
|
21
|
-
|
|
22
|
-
(`config.type` slug),
|
|
23
|
-
|
|
24
|
-
## Response shape
|
|
89
|
+
Integrations by status and type, with per-type usage columns. Input query params
|
|
90
|
+
(all strings): `status` (an `IntegrationStatus`), `type` (`config.type` slug),
|
|
91
|
+
`userId`. Response shape (`schemaVersion: 1`):
|
|
25
92
|
|
|
26
93
|
```jsonc
|
|
27
94
|
{
|
|
@@ -37,7 +104,8 @@ Optional query params (all strings): `status` (an `IntegrationStatus`), `type`
|
|
|
37
104
|
"type": "hubspot",
|
|
38
105
|
"label": "HubSpot CRM",
|
|
39
106
|
"total": 5,
|
|
40
|
-
"byStatus": { "ENABLED": 4, "ERROR": 1, "NEEDS_CONFIG": 0, "PROCESSING": 0, "DISABLED": 0 }
|
|
107
|
+
"byStatus": { "ENABLED": 4, "ERROR": 1, "NEEDS_CONFIG": 0, "PROCESSING": 0, "DISABLED": 0 },
|
|
108
|
+
"usage": { "records.synced": 4200, "webhooks.received": 118 }
|
|
41
109
|
}
|
|
42
110
|
],
|
|
43
111
|
"typeLabels": { "hubspot": "HubSpot CRM" },
|
|
@@ -46,7 +114,7 @@ Optional query params (all strings): `status` (an `IntegrationStatus`), `type`
|
|
|
46
114
|
}
|
|
47
115
|
```
|
|
48
116
|
|
|
49
|
-
|
|
117
|
+
### Field reference
|
|
50
118
|
|
|
51
119
|
- `schemaVersion` — contract version; branch on it. Additive changes do **not** bump it.
|
|
52
120
|
- `filters` — echoes the applied filters (nulls when omitted).
|
|
@@ -54,40 +122,33 @@ Optional query params (all strings): `status` (an `IntegrationStatus`), `type`
|
|
|
54
122
|
- `metrics.byStatus` — counts keyed by `IntegrationStatus` value.
|
|
55
123
|
- `metrics.byType[]` — per `type` breakdown: `{ type, label, total, byStatus, usage? }`.
|
|
56
124
|
- `type` — the `config.type` slug. Integrations with no type bucket as `"unknown"`.
|
|
57
|
-
- `usage` — **additive** (ADR-011): present only when the deployment has the
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
via `metrics.typeLabels`.
|
|
76
|
-
|
|
77
|
-
## How labels are sourced
|
|
78
|
-
|
|
79
|
-
`createReportingRouter()` loads the app's registered integration classes via
|
|
80
|
-
`loadAppDefinition()` and builds a `{ slug → label }` map from each class's
|
|
81
|
-
`Definition.display.label`. Loading is wrapped in try/catch, so reporting still
|
|
82
|
-
works (labels fall back to slugs) if the app definition can't be loaded. The
|
|
83
|
-
`ListIntegrationsReport` use case stays storage-agnostic — it just reads the
|
|
84
|
-
injected `typeLabels` map, so all database adapters (PostgreSQL, MongoDB,
|
|
85
|
-
DocumentDB) get labels with zero adapter changes.
|
|
125
|
+
- `usage` — **additive** (ADR-011): present only when the deployment has the usage
|
|
126
|
+
store configured. A map of canonical counter → total for that type (`0` when a
|
|
127
|
+
type has no data). Read only from the Frigg usage store; a usage-store read
|
|
128
|
+
failure never fails the structural report. Populated by the counters an
|
|
129
|
+
integration opts into via `Definition.usage` — see [`../telemetry/README.md`](../telemetry/README.md).
|
|
130
|
+
- `label` — human-readable name from the integration class's `Definition.display.label`,
|
|
131
|
+
falling back to the `type` slug when no registered class supplies one.
|
|
132
|
+
- `metrics.typeLabels` — map of `type` slug → label, so callers label
|
|
133
|
+
`integrations[].type` rows without bloating each row.
|
|
134
|
+
- `metrics.integrations[]` — lightweight per-integration rows (`id`, `type`, `status`,
|
|
135
|
+
`userId`, `version`, `moduleCount`, `errorCount`, `mappedRecordCount`, `createdAt`,
|
|
136
|
+
`updatedAt`). Resolve display names via `metrics.typeLabels`.
|
|
137
|
+
|
|
138
|
+
The built-in report reads its data through the admin command bundle
|
|
139
|
+
(`frigg.integrations.listForReport`, `frigg.integrationMappings.countByIntegrationIds`,
|
|
140
|
+
`frigg.usage.getTotalsByDimension`) — the retired reporting repository triad's
|
|
141
|
+
logic now lives in the canonical integration/mapping repositories, so all
|
|
142
|
+
database adapters (PostgreSQL, MongoDB, DocumentDB) are served through one path.
|
|
86
143
|
|
|
87
144
|
## Caveats
|
|
88
145
|
|
|
89
|
-
-
|
|
90
|
-
Treat the
|
|
91
|
-
-
|
|
92
|
-
|
|
93
|
-
|
|
146
|
+
- Report output is sensitive (integration types, counts, user IDs, versions).
|
|
147
|
+
Treat the admin API key as an admin secret.
|
|
148
|
+
- The `integrations` back-compat alias is transitional; migrate to
|
|
149
|
+
`POST /api/v2/reports/integrations/run { "mode": "live" }`.
|
|
150
|
+
- Labels only appear once the app registers the integration classes.
|
|
151
|
+
- A `schedule` block in a report's Definition only supplies the default scheduled
|
|
152
|
+
**mode**. It does not create a recurring trigger on its own — activate the
|
|
153
|
+
schedule with `PUT /api/v2/reports/:name/schedule`; the DB schedule
|
|
154
|
+
(`ScriptSchedule`) is the single source of truth.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
const { IntegrationsReport } = require('./reports/integrations-report');
|
|
2
|
+
|
|
3
|
+
// Registered by the admin-scripts bootstrap when the app definition sets `admin.includeBuiltinReports`.
|
|
4
|
+
const BUILTIN_REPORTS = [IntegrationsReport];
|
|
5
|
+
|
|
6
|
+
module.exports = { BUILTIN_REPORTS };
|
package/reporting/index.js
CHANGED
|
@@ -1,17 +1,13 @@
|
|
|
1
|
-
const {
|
|
1
|
+
const { ReportBase } = require('./report-base');
|
|
2
|
+
const { IntegrationsReport } = require('./reports/integrations-report');
|
|
3
|
+
const { BUILTIN_REPORTS } = require('./builtin-reports');
|
|
2
4
|
const {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
ReportingRepositoryPostgres,
|
|
6
|
-
ReportingRepositoryDocumentDB,
|
|
7
|
-
} = require('./repositories/reporting-repository-factory');
|
|
8
|
-
const { ListIntegrationsReport } = require('./use-cases');
|
|
5
|
+
createReportCommands,
|
|
6
|
+
} = require('../application/commands/report-commands');
|
|
9
7
|
|
|
10
8
|
module.exports = {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
ReportingRepositoryDocumentDB,
|
|
16
|
-
ListIntegrationsReport,
|
|
9
|
+
ReportBase,
|
|
10
|
+
IntegrationsReport,
|
|
11
|
+
BUILTIN_REPORTS,
|
|
12
|
+
createReportCommands,
|
|
17
13
|
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ReportBase — a report is an admin operation whose output is its payload.
|
|
3
|
+
*
|
|
4
|
+
* A report Definition declares:
|
|
5
|
+
* - runModes: allowed modes; the first is the default.
|
|
6
|
+
* 'live' — compute and return inline; persist nothing.
|
|
7
|
+
* 'recorded' — persist an execution record (input + results + logs).
|
|
8
|
+
* 'snapshot' — a recorded run tagged as a point in a named series.
|
|
9
|
+
* - output.format: 'json' returns inline; 'csv'|'pdf'|'zip' go to artifact storage.
|
|
10
|
+
* - schedule: optional recurring run via EventBridge.
|
|
11
|
+
*
|
|
12
|
+
* execute(frigg, params): `frigg` is the admin command bundle
|
|
13
|
+
* (=== this.context.commands); do data reads through it, never a repository.
|
|
14
|
+
*/
|
|
15
|
+
class ReportBase {
|
|
16
|
+
static Definition = {
|
|
17
|
+
name: 'Report Name',
|
|
18
|
+
version: '0.0.0',
|
|
19
|
+
description: 'What this report computes',
|
|
20
|
+
source: 'USER_DEFINED', // 'BUILTIN' | 'USER_DEFINED'
|
|
21
|
+
|
|
22
|
+
runModes: ['live', 'recorded', 'snapshot'],
|
|
23
|
+
inputSchema: null,
|
|
24
|
+
outputSchema: null,
|
|
25
|
+
output: { format: 'json' },
|
|
26
|
+
schedule: { enabled: false, cron: null, mode: 'snapshot' },
|
|
27
|
+
|
|
28
|
+
config: {
|
|
29
|
+
timeout: 300000,
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
display: {
|
|
33
|
+
category: 'reporting',
|
|
34
|
+
icon: null,
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
constructor(params = {}) {
|
|
39
|
+
this.context = params.context || null;
|
|
40
|
+
this.executionId = params.executionId || null;
|
|
41
|
+
this.integrationFactory = params.integrationFactory || null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async execute(frigg, params) {
|
|
45
|
+
throw new Error('ReportBase.execute() must be implemented by subclass');
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
module.exports = { ReportBase };
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
const
|
|
1
|
+
const { ReportBase } = require('../report-base');
|
|
2
2
|
const { CANONICAL_COUNTERS } = require('../../telemetry/canonical-counters');
|
|
3
|
+
const { loadAppDefinition } = require('../../handlers/app-definition-loader');
|
|
3
4
|
|
|
4
5
|
const SCHEMA_VERSION = 1;
|
|
5
6
|
const SERVICE = 'frigg-core-api';
|
|
6
7
|
|
|
7
|
-
// Seeded so every known status appears
|
|
8
|
-
// the schema later are still counted dynamically.
|
|
8
|
+
// Seeded so every known status appears in output even at count 0.
|
|
9
9
|
const KNOWN_STATUSES = [
|
|
10
10
|
'IN_CREATION',
|
|
11
11
|
'ENABLED',
|
|
@@ -16,33 +16,37 @@ const KNOWN_STATUSES = [
|
|
|
16
16
|
'DISABLED',
|
|
17
17
|
];
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
19
|
+
// IntegrationBase.Definition default — skip it so the slug is used instead.
|
|
20
|
+
const PLACEHOLDER_DISPLAY_NAME = 'Integration Name';
|
|
21
|
+
|
|
22
|
+
class IntegrationsReport extends ReportBase {
|
|
23
|
+
static Definition = {
|
|
24
|
+
name: 'integrations',
|
|
25
|
+
version: '1.0.0',
|
|
26
|
+
description: 'Integrations by status and type, with per-type usage counts',
|
|
27
|
+
source: 'BUILTIN',
|
|
28
|
+
runModes: ['live', 'recorded', 'snapshot'],
|
|
29
|
+
inputSchema: {
|
|
30
|
+
type: 'object',
|
|
31
|
+
additionalProperties: false,
|
|
32
|
+
properties: {
|
|
33
|
+
status: { type: 'string' },
|
|
34
|
+
type: { type: 'string' },
|
|
35
|
+
userId: { type: 'string' },
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
output: { format: 'json' },
|
|
39
|
+
schedule: { enabled: false, cron: null, mode: 'snapshot' },
|
|
40
|
+
display: { category: 'reporting', icon: null },
|
|
41
|
+
};
|
|
35
42
|
|
|
36
|
-
async execute(
|
|
37
|
-
const { status, type, userId } = this._validateQuery(
|
|
43
|
+
async execute(frigg, params = {}) {
|
|
44
|
+
const { status, type, userId } = this._validateQuery(params);
|
|
45
|
+
const typeLabels = buildTypeLabels();
|
|
38
46
|
|
|
39
|
-
const rows = await
|
|
40
|
-
status,
|
|
41
|
-
userId,
|
|
42
|
-
});
|
|
47
|
+
const rows = await frigg.integrations.listForReport({ status, userId });
|
|
43
48
|
|
|
44
|
-
// type lives in config.type
|
|
45
|
-
// DBs), so it is filtered here rather than in the repository query.
|
|
49
|
+
// type lives in config.type, a JSON path not portably groupable across DBs, so filter here not in the query.
|
|
46
50
|
const filtered =
|
|
47
51
|
type === undefined
|
|
48
52
|
? rows
|
|
@@ -50,7 +54,7 @@ class ListIntegrationsReport {
|
|
|
50
54
|
|
|
51
55
|
const ids = filtered.map((row) => row.id);
|
|
52
56
|
const mappingCounts = ids.length
|
|
53
|
-
? await
|
|
57
|
+
? await frigg.integrationMappings.countByIntegrationIds(ids)
|
|
54
58
|
: new Map();
|
|
55
59
|
|
|
56
60
|
const integrations = filtered.map((row) => ({
|
|
@@ -76,8 +80,7 @@ class ListIntegrationsReport {
|
|
|
76
80
|
if (!byTypeMap.has(integration.type)) {
|
|
77
81
|
byTypeMap.set(integration.type, {
|
|
78
82
|
type: integration.type,
|
|
79
|
-
label:
|
|
80
|
-
this.typeLabels[integration.type] || integration.type,
|
|
83
|
+
label: typeLabels[integration.type] || integration.type,
|
|
81
84
|
total: 0,
|
|
82
85
|
byStatus: emptyStatusCounts(),
|
|
83
86
|
});
|
|
@@ -87,7 +90,7 @@ class ListIntegrationsReport {
|
|
|
87
90
|
bucket.byStatus[statusKey] = (bucket.byStatus[statusKey] ?? 0) + 1;
|
|
88
91
|
}
|
|
89
92
|
|
|
90
|
-
await this._attachUsageColumns(byTypeMap);
|
|
93
|
+
await this._attachUsageColumns(byTypeMap, frigg);
|
|
91
94
|
|
|
92
95
|
return {
|
|
93
96
|
schemaVersion: SCHEMA_VERSION,
|
|
@@ -102,18 +105,18 @@ class ListIntegrationsReport {
|
|
|
102
105
|
total: integrations.length,
|
|
103
106
|
byStatus,
|
|
104
107
|
byType: Array.from(byTypeMap.values()),
|
|
105
|
-
typeLabels: { ...
|
|
108
|
+
typeLabels: { ...typeLabels },
|
|
106
109
|
integrations,
|
|
107
110
|
},
|
|
108
111
|
};
|
|
109
112
|
}
|
|
110
113
|
|
|
111
|
-
async _attachUsageColumns(byTypeMap) {
|
|
114
|
+
async _attachUsageColumns(byTypeMap, frigg) {
|
|
112
115
|
const metrics = Object.keys(CANONICAL_COUNTERS);
|
|
113
116
|
try {
|
|
114
117
|
const totalsByMetric = await Promise.all(
|
|
115
118
|
metrics.map(async (metric) => {
|
|
116
|
-
const totals = await
|
|
119
|
+
const totals = await frigg.usage.getTotalsByDimension({
|
|
117
120
|
metric,
|
|
118
121
|
groupBy: 'integrationType',
|
|
119
122
|
});
|
|
@@ -146,7 +149,7 @@ class ListIntegrationsReport {
|
|
|
146
149
|
value !== null &&
|
|
147
150
|
typeof value !== 'string'
|
|
148
151
|
) {
|
|
149
|
-
throw
|
|
152
|
+
throw invalidInput(
|
|
150
153
|
`Invalid query parameter '${key}': expected a string`
|
|
151
154
|
);
|
|
152
155
|
}
|
|
@@ -158,7 +161,7 @@ class ListIntegrationsReport {
|
|
|
158
161
|
userId: normalize(userId),
|
|
159
162
|
};
|
|
160
163
|
if (normalized.status && !KNOWN_STATUSES.includes(normalized.status)) {
|
|
161
|
-
throw
|
|
164
|
+
throw invalidInput(
|
|
162
165
|
`Invalid status '${
|
|
163
166
|
normalized.status
|
|
164
167
|
}'. Expected one of: ${KNOWN_STATUSES.join(', ')}`
|
|
@@ -168,6 +171,13 @@ class ListIntegrationsReport {
|
|
|
168
171
|
}
|
|
169
172
|
}
|
|
170
173
|
|
|
174
|
+
// INVALID_INPUT keeps the report protocol-agnostic; the runner/router map it to a 400.
|
|
175
|
+
function invalidInput(message) {
|
|
176
|
+
const error = new Error(message);
|
|
177
|
+
error.code = 'INVALID_INPUT';
|
|
178
|
+
return error;
|
|
179
|
+
}
|
|
180
|
+
|
|
171
181
|
function emptyStatusCounts() {
|
|
172
182
|
return KNOWN_STATUSES.reduce((acc, status) => {
|
|
173
183
|
acc[status] = 0;
|
|
@@ -186,4 +196,26 @@ function toIso(value) {
|
|
|
186
196
|
return String(value);
|
|
187
197
|
}
|
|
188
198
|
|
|
189
|
-
|
|
199
|
+
function buildTypeLabels() {
|
|
200
|
+
try {
|
|
201
|
+
const { integrations = [] } = loadAppDefinition();
|
|
202
|
+
const labels = {};
|
|
203
|
+
for (const IntegrationClass of integrations) {
|
|
204
|
+
const def = IntegrationClass?.Definition;
|
|
205
|
+
if (!def?.name) continue;
|
|
206
|
+
const label = def.display?.label;
|
|
207
|
+
if (label && label !== PLACEHOLDER_DISPLAY_NAME) {
|
|
208
|
+
labels[def.name] = label;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return labels;
|
|
212
|
+
} catch (error) {
|
|
213
|
+
console.error(
|
|
214
|
+
'Reporting: failed to load integration labels:',
|
|
215
|
+
error.message
|
|
216
|
+
);
|
|
217
|
+
return {};
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
module.exports = { IntegrationsReport, SCHEMA_VERSION };
|
|
@@ -1,9 +0,0 @@
|
|
|
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 };
|