@friggframework/core 2.0.0-next.97 → 2.0.0-next.98

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/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.97",
4
+ "version": "2.0.0-next.98",
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.97",
42
- "@friggframework/prettier-config": "2.0.0-next.97",
43
- "@friggframework/test": "2.0.0-next.97",
41
+ "@friggframework/eslint-config": "2.0.0-next.98",
42
+ "@friggframework/prettier-config": "2.0.0-next.98",
43
+ "@friggframework/test": "2.0.0-next.98",
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": "5595f797e6f65bb5a66dc96bfa1154f0d4c39504"
83
+ "gitHead": "da2c1977216b9f939121b18d8461cc4e8a5b2b94"
84
84
  }
@@ -0,0 +1,86 @@
1
+ # Reporting
2
+
3
+ Read-only, deployment-wide reporting endpoints for `@friggframework/core`.
4
+ Gated by a dedicated reporting API key — **not** the per-user auth used by the
5
+ rest of the Management API.
6
+
7
+ ## Auth
8
+
9
+ Send the reporting key in the `x-frigg-reporting-api-key` header. It is validated
10
+ against the `REPORTING_API_KEY` environment variable. Missing or wrong key → `401`.
11
+
12
+ ## Endpoints
13
+
14
+ | Method | Path | Description |
15
+ | --- | --- | --- |
16
+ | `GET` | `/api/v2/reports` | Index of available reports. |
17
+ | `GET` | `/api/v2/reports/integrations` | Integrations report (see below). |
18
+
19
+ ### `GET /api/v2/reports/integrations`
20
+
21
+ Optional query params (all strings): `status` (an `IntegrationStatus`), `type`
22
+ (`config.type` slug), `userId`.
23
+
24
+ ## Response shape
25
+
26
+ ```jsonc
27
+ {
28
+ "schemaVersion": 1,
29
+ "service": "frigg-core-api",
30
+ "generatedAt": "2026-06-29T20:45:05.142Z",
31
+ "filters": { "status": null, "type": null, "userId": null },
32
+ "metrics": {
33
+ "total": 12,
34
+ "byStatus": { "ENABLED": 9, "ERROR": 2, "NEEDS_CONFIG": 1, "PROCESSING": 0, "DISABLED": 0 },
35
+ "byType": [
36
+ {
37
+ "type": "hubspot",
38
+ "label": "HubSpot CRM",
39
+ "total": 5,
40
+ "byStatus": { "ENABLED": 4, "ERROR": 1, "NEEDS_CONFIG": 0, "PROCESSING": 0, "DISABLED": 0 }
41
+ }
42
+ ],
43
+ "typeLabels": { "hubspot": "HubSpot CRM" },
44
+ "integrations": [ /* lightweight per-integration rows */ ]
45
+ }
46
+ }
47
+ ```
48
+
49
+ ## Field reference
50
+
51
+ - `schemaVersion` — contract version; branch on it. Additive changes do **not** bump it.
52
+ - `filters` — echoes the applied filters (nulls when omitted).
53
+ - `metrics.total` — count of integrations matching the filters.
54
+ - `metrics.byStatus` — counts keyed by `IntegrationStatus` value.
55
+ - `metrics.byType[]` — per `type` breakdown: `{ type, label, total, byStatus }`.
56
+ - `type` — the `config.type` slug. Integrations with no type bucket as `"unknown"`.
57
+ - `label` — human-readable name from the integration class's
58
+ `Definition.display.label`. Falls back to the `type` slug when no registered
59
+ class supplies a label (e.g. an integration that was removed, or run on an
60
+ older app that doesn't register it). Additive — `type` is unchanged.
61
+ - `metrics.typeLabels` — map of `type` slug → human-readable label, so callers can
62
+ label `integrations[].type` rows without bloating each row. Contains only types
63
+ whose registered class supplies a non-default `display.label` (classes still
64
+ carrying the IntegrationBase default `'Integration Name'` are excluded).
65
+ - `metrics.integrations[]` — lightweight per-integration rows (`id`, `type`,
66
+ `status`, `userId`, `version`, `moduleCount`, `errorCount`, `mappedRecordCount`,
67
+ `createdAt`, `updatedAt`). These rows carry `type` only — resolve display names
68
+ via `metrics.typeLabels`.
69
+
70
+ ## How labels are sourced
71
+
72
+ `createReportingRouter()` loads the app's registered integration classes via
73
+ `loadAppDefinition()` and builds a `{ slug → label }` map from each class's
74
+ `Definition.display.label`. Loading is wrapped in try/catch, so reporting still
75
+ works (labels fall back to slugs) if the app definition can't be loaded. The
76
+ `ListIntegrationsReport` use case stays storage-agnostic — it just reads the
77
+ injected `typeLabels` map, so all database adapters (PostgreSQL, MongoDB,
78
+ DocumentDB) get labels with zero adapter changes.
79
+
80
+ ## Caveats
81
+
82
+ - The response is sensitive (exposes integration types, counts, user IDs, versions).
83
+ Treat the reporting key as an admin secret.
84
+ - Read-only — no mutation endpoints.
85
+ - Labels only appear once the deployment runs a `@friggframework/core` version that
86
+ includes this field and the app registers the integration classes.
@@ -4,11 +4,40 @@ const {
4
4
  createReportingRepository,
5
5
  } = require('./repositories/reporting-repository-factory');
6
6
  const { ListIntegrationsReport } = require('./use-cases/list-integrations-report');
7
+ const { loadAppDefinition } = require('../handlers/app-definition-loader');
8
+
9
+ // IntegrationBase.Definition default — skip it so the slug is used instead.
10
+ const PLACEHOLDER_DISPLAY_NAME = 'Integration Name';
11
+
12
+ // Map each integration's config.type slug to its human-readable display label.
13
+ // Wrapped so reporting still works if the app definition fails to load.
14
+ function buildTypeLabels() {
15
+ try {
16
+ const { integrations = [] } = loadAppDefinition();
17
+ const labels = {};
18
+ for (const IntegrationClass of integrations) {
19
+ const def = IntegrationClass?.Definition;
20
+ if (!def?.name) continue;
21
+ const label = def.display?.label;
22
+ if (label && label !== PLACEHOLDER_DISPLAY_NAME) {
23
+ labels[def.name] = label;
24
+ }
25
+ }
26
+ return labels;
27
+ } catch (error) {
28
+ console.error(
29
+ 'Reporting: failed to load integration labels:',
30
+ error.message
31
+ );
32
+ return {};
33
+ }
34
+ }
7
35
 
8
36
  function createReportingRouter() {
9
37
  const reportingRepository = createReportingRepository();
10
38
  const listIntegrationsReport = new ListIntegrationsReport({
11
39
  reportingRepository,
40
+ typeLabels: buildTypeLabels(),
12
41
  });
13
42
 
14
43
  const router = express.Router();
@@ -14,11 +14,12 @@ const KNOWN_STATUSES = [
14
14
  ];
15
15
 
16
16
  class ListIntegrationsReport {
17
- constructor({ reportingRepository } = {}) {
17
+ constructor({ reportingRepository, typeLabels = {} } = {}) {
18
18
  if (!reportingRepository) {
19
19
  throw new Error('reportingRepository is required');
20
20
  }
21
21
  this.reportingRepository = reportingRepository;
22
+ this.typeLabels = typeLabels;
22
23
  }
23
24
 
24
25
  async execute(query = {}) {
@@ -64,6 +65,7 @@ class ListIntegrationsReport {
64
65
  if (!byTypeMap.has(integration.type)) {
65
66
  byTypeMap.set(integration.type, {
66
67
  type: integration.type,
68
+ label: this.typeLabels[integration.type] || integration.type,
67
69
  total: 0,
68
70
  byStatus: emptyStatusCounts(),
69
71
  });
@@ -86,6 +88,7 @@ class ListIntegrationsReport {
86
88
  total: integrations.length,
87
89
  byStatus,
88
90
  byType: Array.from(byTypeMap.values()),
91
+ typeLabels: { ...this.typeLabels },
89
92
  integrations,
90
93
  },
91
94
  };