@pipeline-builder/pipeline-data 4.1.0 → 4.1.2
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/lib/api/reporting/caches.d.ts +2 -0
- package/lib/api/reporting/caches.js +14 -0
- package/lib/api/reporting/dora-scoring.d.ts +71 -0
- package/lib/api/reporting/dora-scoring.js +126 -0
- package/lib/api/reporting/dora.d.ts +47 -0
- package/lib/api/reporting/dora.js +501 -0
- package/lib/api/reporting/query-scope.d.ts +21 -0
- package/lib/api/reporting/query-scope.js +62 -0
- package/lib/api/reporting/retention.d.ts +44 -0
- package/lib/api/reporting/retention.js +64 -0
- package/lib/api/reporting/sql-helpers.d.ts +47 -0
- package/lib/api/reporting/sql-helpers.js +65 -0
- package/lib/api/reporting/types.d.ts +469 -0
- package/lib/api/reporting/types.js +4 -0
- package/lib/api/reporting-service.d.ts +7 -500
- package/lib/api/reporting-service.js +46 -760
- package/package.json +2 -2
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// Copyright 2026 Pipeline Builder Contributors
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
/**
|
|
4
|
+
* Tenancy scoping and cache/context policy for reporting reads.
|
|
5
|
+
*
|
|
6
|
+
* These were private methods on ReportingService. They hold no instance state —
|
|
7
|
+
* only the module-level logger and timeseries cache — so they are plain
|
|
8
|
+
* functions here, which is what lets the per-domain query modules (dora.ts)
|
|
9
|
+
* use the identical scoping rules without reaching back into the class.
|
|
10
|
+
*/
|
|
11
|
+
import { createLogger } from '@pipeline-builder/api-core';
|
|
12
|
+
import { sql } from 'drizzle-orm';
|
|
13
|
+
import { timeseriesCache } from './caches.js';
|
|
14
|
+
import { runWithTenantContext } from '../../database/tenancy.js';
|
|
15
|
+
const logger = createLogger('reporting-service');
|
|
16
|
+
/**
|
|
17
|
+
* Build the org-scope predicate for a report query. With `orgIds` (the
|
|
18
|
+
* org → team rollup — a parent's `[self, ...descendants]`) it becomes an
|
|
19
|
+
* `IN (...)` over the subtree; otherwise the single-org `= $org`. Returns a
|
|
20
|
+
* `multi` flag so callers can run multi-org reads under sysadmin context
|
|
21
|
+
* (the subtree spans orgs outside the request's RLS scope) and salt the
|
|
22
|
+
* cache key.
|
|
23
|
+
*/
|
|
24
|
+
export function orgScope(orgId, orgIds) {
|
|
25
|
+
// Defense-in-depth: the multi-org rollup runs under sysadmin context (RLS
|
|
26
|
+
// OFF, see runReport), so `orgIds` is the ONLY tenancy gate on the read. The
|
|
27
|
+
// route resolves it from the platform's authoritative descendants endpoint as
|
|
28
|
+
// `[self, ...descendants]`, but validate the subtree defensively here before
|
|
29
|
+
// the bypass: the requesting (anchor) org MUST appear in the set. A set that
|
|
30
|
+
// omits the caller's own org is a malformed/spoofed rollup — collapse to the
|
|
31
|
+
// safe single-org scope rather than reading an arbitrary org under sysadmin.
|
|
32
|
+
// (Full descendant-membership validation needs the org tree, which this
|
|
33
|
+
// service does not hold — see the report FLAG.)
|
|
34
|
+
const anchor = orgId.toLowerCase();
|
|
35
|
+
const provided = (orgIds ?? []).filter((id) => typeof id === 'string' && id.length > 0);
|
|
36
|
+
const anchored = provided.some((id) => id.toLowerCase() === anchor);
|
|
37
|
+
if (provided.length > 0 && !anchored) {
|
|
38
|
+
logger.warn('Reporting rollup rejected: requesting org absent from resolved subtree; using single-org scope', {
|
|
39
|
+
orgId: anchor, subtreeSize: provided.length,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
const ids = provided.length > 0 && anchored ? provided : [orgId];
|
|
43
|
+
const multi = ids.length > 1;
|
|
44
|
+
const pred = multi
|
|
45
|
+
? sql `IN (${sql.join(ids.map((id) => sql `${id}`), sql `, `)})`
|
|
46
|
+
: sql `= ${ids[0]}`;
|
|
47
|
+
return { pred, multi };
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Run a report read. Single-org reads use the per-org cache (invalidated on
|
|
51
|
+
* that org's event ingest). Rollup (multi-org) reads **bypass the cache** and
|
|
52
|
+
* run under sysadmin context: they're admin-only and lower-frequency, and a
|
|
53
|
+
* child org's event ingest can't invalidate a parent's rollup entry (reporting
|
|
54
|
+
* has no org tree), so caching them would serve stale aggregates. Always fresh.
|
|
55
|
+
*/
|
|
56
|
+
export function runReport(cacheKey, multi, exec) {
|
|
57
|
+
return multi
|
|
58
|
+
? runWithTenantContext({ isSuperAdmin: true }, exec)
|
|
59
|
+
: timeseriesCache.getOrSet(cacheKey, exec);
|
|
60
|
+
}
|
|
61
|
+
/** 1.1 Execution count per pipeline with status breakdown. */
|
|
62
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicXVlcnktc2NvcGUuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi9zcmMvYXBpL3JlcG9ydGluZy9xdWVyeS1zY29wZS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSwrQ0FBK0M7QUFDL0Msc0NBQXNDO0FBRXRDOzs7Ozs7O0dBT0c7QUFFSCxPQUFPLEVBQUUsWUFBWSxFQUFFLE1BQU0sNEJBQTRCLENBQUM7QUFDMUQsT0FBTyxFQUFFLEdBQUcsRUFBRSxNQUFNLGFBQWEsQ0FBQztBQUNsQyxPQUFPLEVBQUUsZUFBZSxFQUFFLE1BQU0sYUFBYSxDQUFDO0FBQzlDLE9BQU8sRUFBRSxvQkFBb0IsRUFBRSxNQUFNLDJCQUEyQixDQUFDO0FBRWpFLE1BQU0sTUFBTSxHQUFHLFlBQVksQ0FBQyxtQkFBbUIsQ0FBQyxDQUFDO0FBRWpEOzs7Ozs7O0dBT0c7QUFDSCxNQUFNLFVBQVUsUUFBUSxDQUFDLEtBQWEsRUFBRSxNQUFpQjtJQUN2RCwwRUFBMEU7SUFDMUUsNkVBQTZFO0lBQzdFLDhFQUE4RTtJQUM5RSw2RUFBNkU7SUFDN0UsNkVBQTZFO0lBQzdFLDZFQUE2RTtJQUM3RSw2RUFBNkU7SUFDN0Usd0VBQXdFO0lBQ3hFLGdEQUFnRDtJQUNoRCxNQUFNLE1BQU0sR0FBRyxLQUFLLENBQUMsV0FBVyxFQUFFLENBQUM7SUFDbkMsTUFBTSxRQUFRLEdBQUcsQ0FBQyxNQUFNLElBQUksRUFBRSxDQUFDLENBQUMsTUFBTSxDQUFDLENBQUMsRUFBRSxFQUFFLEVBQUUsQ0FBQyxPQUFPLEVBQUUsS0FBSyxRQUFRLElBQUksRUFBRSxDQUFDLE1BQU0sR0FBRyxDQUFDLENBQUMsQ0FBQztJQUN4RixNQUFNLFFBQVEsR0FBRyxRQUFRLENBQUMsSUFBSSxDQUFDLENBQUMsRUFBRSxFQUFFLEVBQUUsQ0FBQyxFQUFFLENBQUMsV0FBVyxFQUFFLEtBQUssTUFBTSxDQUFDLENBQUM7SUFDcEUsSUFBSSxRQUFRLENBQUMsTUFBTSxHQUFHLENBQUMsSUFBSSxDQUFDLFFBQVEsRUFBRSxDQUFDO1FBQ3JDLE1BQU0sQ0FBQyxJQUFJLENBQUMsZ0dBQWdHLEVBQUU7WUFDNUcsS0FBSyxFQUFFLE1BQU0sRUFBRSxXQUFXLEVBQUUsUUFBUSxDQUFDLE1BQU07U0FDNUMsQ0FBQyxDQUFDO0lBQ0wsQ0FBQztJQUNELE1BQU0sR0FBRyxHQUFHLFFBQVEsQ0FBQyxNQUFNLEdBQUcsQ0FBQyxJQUFJLFFBQVEsQ0FBQyxDQUFDLENBQUMsUUFBUSxDQUFDLENBQUMsQ0FBQyxDQUFDLEtBQUssQ0FBQyxDQUFDO0lBQ2pFLE1BQU0sS0FBSyxHQUFHLEdBQUcsQ0FBQyxNQUFNLEdBQUcsQ0FBQyxDQUFDO0lBQzdCLE1BQU0sSUFBSSxHQUFHLEtBQUs7UUFDaEIsQ0FBQyxDQUFDLEdBQUcsQ0FBQSxPQUFPLEdBQUcsQ0FBQyxJQUFJLENBQUMsR0FBRyxDQUFDLEdBQUcsQ0FBQyxDQUFDLEVBQUUsRUFBRSxFQUFFLENBQUMsR0FBRyxDQUFBLEdBQUcsRUFBRSxFQUFFLENBQUMsRUFBRSxHQUFHLENBQUEsSUFBSSxDQUFDLEdBQUc7UUFDN0QsQ0FBQyxDQUFDLEdBQUcsQ0FBQSxLQUFLLEdBQUcsQ0FBQyxDQUFDLENBQUMsRUFBRSxDQUFDO0lBQ3JCLE9BQU8sRUFBRSxJQUFJLEVBQUUsS0FBSyxFQUFFLENBQUM7QUFDekIsQ0FBQztBQUVEOzs7Ozs7R0FNRztBQUNILE1BQU0sVUFBVSxTQUFTLENBQUksUUFBZ0IsRUFBRSxLQUFjLEVBQUUsSUFBc0I7SUFDbkYsT0FBTyxLQUFLO1FBQ1YsQ0FBQyxDQUFDLG9CQUFvQixDQUFDLEVBQUUsWUFBWSxFQUFFLElBQUksRUFBRSxFQUFFLElBQUksQ0FBQztRQUNwRCxDQUFDLENBQUMsZUFBZSxDQUFDLFFBQVEsQ0FBQyxRQUFRLEVBQUUsSUFBSSxDQUFDLENBQUM7QUFDL0MsQ0FBQztBQUVELDhEQUE4RCIsInNvdXJjZXNDb250ZW50IjpbIi8vIENvcHlyaWdodCAyMDI2IFBpcGVsaW5lIEJ1aWxkZXIgQ29udHJpYnV0b3JzXG4vLyBTUERYLUxpY2Vuc2UtSWRlbnRpZmllcjogQXBhY2hlLTIuMFxuXG4vKipcbiAqIFRlbmFuY3kgc2NvcGluZyBhbmQgY2FjaGUvY29udGV4dCBwb2xpY3kgZm9yIHJlcG9ydGluZyByZWFkcy5cbiAqXG4gKiBUaGVzZSB3ZXJlIHByaXZhdGUgbWV0aG9kcyBvbiBSZXBvcnRpbmdTZXJ2aWNlLiBUaGV5IGhvbGQgbm8gaW5zdGFuY2Ugc3RhdGUg4oCUXG4gKiBvbmx5IHRoZSBtb2R1bGUtbGV2ZWwgbG9nZ2VyIGFuZCB0aW1lc2VyaWVzIGNhY2hlIOKAlCBzbyB0aGV5IGFyZSBwbGFpblxuICogZnVuY3Rpb25zIGhlcmUsIHdoaWNoIGlzIHdoYXQgbGV0cyB0aGUgcGVyLWRvbWFpbiBxdWVyeSBtb2R1bGVzIChkb3JhLnRzKVxuICogdXNlIHRoZSBpZGVudGljYWwgc2NvcGluZyBydWxlcyB3aXRob3V0IHJlYWNoaW5nIGJhY2sgaW50byB0aGUgY2xhc3MuXG4gKi9cblxuaW1wb3J0IHsgY3JlYXRlTG9nZ2VyIH0gZnJvbSAnQHBpcGVsaW5lLWJ1aWxkZXIvYXBpLWNvcmUnO1xuaW1wb3J0IHsgc3FsIH0gZnJvbSAnZHJpenpsZS1vcm0nO1xuaW1wb3J0IHsgdGltZXNlcmllc0NhY2hlIH0gZnJvbSAnLi9jYWNoZXMuanMnO1xuaW1wb3J0IHsgcnVuV2l0aFRlbmFudENvbnRleHQgfSBmcm9tICcuLi8uLi9kYXRhYmFzZS90ZW5hbmN5LmpzJztcblxuY29uc3QgbG9nZ2VyID0gY3JlYXRlTG9nZ2VyKCdyZXBvcnRpbmctc2VydmljZScpO1xuXG4vKipcbiAqIEJ1aWxkIHRoZSBvcmctc2NvcGUgcHJlZGljYXRlIGZvciBhIHJlcG9ydCBxdWVyeS4gV2l0aCBgb3JnSWRzYCAodGhlXG4gKiBvcmcg4oaSIHRlYW0gcm9sbHVwIOKAlCBhIHBhcmVudCdzIGBbc2VsZiwgLi4uZGVzY2VuZGFudHNdYCkgaXQgYmVjb21lcyBhblxuICogYElOICguLi4pYCBvdmVyIHRoZSBzdWJ0cmVlOyBvdGhlcndpc2UgdGhlIHNpbmdsZS1vcmcgYD0gJG9yZ2AuIFJldHVybnMgYVxuICogYG11bHRpYCBmbGFnIHNvIGNhbGxlcnMgY2FuIHJ1biBtdWx0aS1vcmcgcmVhZHMgdW5kZXIgc3lzYWRtaW4gY29udGV4dFxuICogKHRoZSBzdWJ0cmVlIHNwYW5zIG9yZ3Mgb3V0c2lkZSB0aGUgcmVxdWVzdCdzIFJMUyBzY29wZSkgYW5kIHNhbHQgdGhlXG4gKiBjYWNoZSBrZXkuXG4gKi9cbmV4cG9ydCBmdW5jdGlvbiBvcmdTY29wZShvcmdJZDogc3RyaW5nLCBvcmdJZHM/OiBzdHJpbmdbXSkge1xuICAvLyBEZWZlbnNlLWluLWRlcHRoOiB0aGUgbXVsdGktb3JnIHJvbGx1cCBydW5zIHVuZGVyIHN5c2FkbWluIGNvbnRleHQgKFJMU1xuICAvLyBPRkYsIHNlZSBydW5SZXBvcnQpLCBzbyBgb3JnSWRzYCBpcyB0aGUgT05MWSB0ZW5hbmN5IGdhdGUgb24gdGhlIHJlYWQuIFRoZVxuICAvLyByb3V0ZSByZXNvbHZlcyBpdCBmcm9tIHRoZSBwbGF0Zm9ybSdzIGF1dGhvcml0YXRpdmUgZGVzY2VuZGFudHMgZW5kcG9pbnQgYXNcbiAgLy8gYFtzZWxmLCAuLi5kZXNjZW5kYW50c11gLCBidXQgdmFsaWRhdGUgdGhlIHN1YnRyZWUgZGVmZW5zaXZlbHkgaGVyZSBiZWZvcmVcbiAgLy8gdGhlIGJ5cGFzczogdGhlIHJlcXVlc3RpbmcgKGFuY2hvcikgb3JnIE1VU1QgYXBwZWFyIGluIHRoZSBzZXQuIEEgc2V0IHRoYXRcbiAgLy8gb21pdHMgdGhlIGNhbGxlcidzIG93biBvcmcgaXMgYSBtYWxmb3JtZWQvc3Bvb2ZlZCByb2xsdXAg4oCUIGNvbGxhcHNlIHRvIHRoZVxuICAvLyBzYWZlIHNpbmdsZS1vcmcgc2NvcGUgcmF0aGVyIHRoYW4gcmVhZGluZyBhbiBhcmJpdHJhcnkgb3JnIHVuZGVyIHN5c2FkbWluLlxuICAvLyAoRnVsbCBkZXNjZW5kYW50LW1lbWJlcnNoaXAgdmFsaWRhdGlvbiBuZWVkcyB0aGUgb3JnIHRyZWUsIHdoaWNoIHRoaXNcbiAgLy8gc2VydmljZSBkb2VzIG5vdCBob2xkIOKAlCBzZWUgdGhlIHJlcG9ydCBGTEFHLilcbiAgY29uc3QgYW5jaG9yID0gb3JnSWQudG9Mb3dlckNhc2UoKTtcbiAgY29uc3QgcHJvdmlkZWQgPSAob3JnSWRzID8/IFtdKS5maWx0ZXIoKGlkKSA9PiB0eXBlb2YgaWQgPT09ICdzdHJpbmcnICYmIGlkLmxlbmd0aCA+IDApO1xuICBjb25zdCBhbmNob3JlZCA9IHByb3ZpZGVkLnNvbWUoKGlkKSA9PiBpZC50b0xvd2VyQ2FzZSgpID09PSBhbmNob3IpO1xuICBpZiAocHJvdmlkZWQubGVuZ3RoID4gMCAmJiAhYW5jaG9yZWQpIHtcbiAgICBsb2dnZXIud2FybignUmVwb3J0aW5nIHJvbGx1cCByZWplY3RlZDogcmVxdWVzdGluZyBvcmcgYWJzZW50IGZyb20gcmVzb2x2ZWQgc3VidHJlZTsgdXNpbmcgc2luZ2xlLW9yZyBzY29wZScsIHtcbiAgICAgIG9yZ0lkOiBhbmNob3IsIHN1YnRyZWVTaXplOiBwcm92aWRlZC5sZW5ndGgsXG4gICAgfSk7XG4gIH1cbiAgY29uc3QgaWRzID0gcHJvdmlkZWQubGVuZ3RoID4gMCAmJiBhbmNob3JlZCA/IHByb3ZpZGVkIDogW29yZ0lkXTtcbiAgY29uc3QgbXVsdGkgPSBpZHMubGVuZ3RoID4gMTtcbiAgY29uc3QgcHJlZCA9IG11bHRpXG4gICAgPyBzcWxgSU4gKCR7c3FsLmpvaW4oaWRzLm1hcCgoaWQpID0+IHNxbGAke2lkfWApLCBzcWxgLCBgKX0pYFxuICAgIDogc3FsYD0gJHtpZHNbMF19YDtcbiAgcmV0dXJuIHsgcHJlZCwgbXVsdGkgfTtcbn1cblxuLyoqXG4gKiBSdW4gYSByZXBvcnQgcmVhZC4gU2luZ2xlLW9yZyByZWFkcyB1c2UgdGhlIHBlci1vcmcgY2FjaGUgKGludmFsaWRhdGVkIG9uXG4gKiB0aGF0IG9yZydzIGV2ZW50IGluZ2VzdCkuIFJvbGx1cCAobXVsdGktb3JnKSByZWFkcyAqKmJ5cGFzcyB0aGUgY2FjaGUqKiBhbmRcbiAqIHJ1biB1bmRlciBzeXNhZG1pbiBjb250ZXh0OiB0aGV5J3JlIGFkbWluLW9ubHkgYW5kIGxvd2VyLWZyZXF1ZW5jeSwgYW5kIGFcbiAqIGNoaWxkIG9yZydzIGV2ZW50IGluZ2VzdCBjYW4ndCBpbnZhbGlkYXRlIGEgcGFyZW50J3Mgcm9sbHVwIGVudHJ5IChyZXBvcnRpbmdcbiAqIGhhcyBubyBvcmcgdHJlZSksIHNvIGNhY2hpbmcgdGhlbSB3b3VsZCBzZXJ2ZSBzdGFsZSBhZ2dyZWdhdGVzLiBBbHdheXMgZnJlc2guXG4gKi9cbmV4cG9ydCBmdW5jdGlvbiBydW5SZXBvcnQ8VD4oY2FjaGVLZXk6IHN0cmluZywgbXVsdGk6IGJvb2xlYW4sIGV4ZWM6ICgpID0+IFByb21pc2U8VD4pOiBQcm9taXNlPFQ+IHtcbiAgcmV0dXJuIG11bHRpXG4gICAgPyBydW5XaXRoVGVuYW50Q29udGV4dCh7IGlzU3VwZXJBZG1pbjogdHJ1ZSB9LCBleGVjKVxuICAgIDogdGltZXNlcmllc0NhY2hlLmdldE9yU2V0KGNhY2hlS2V5LCBleGVjKTtcbn1cblxuLyoqIDEuMSBFeGVjdXRpb24gY291bnQgcGVyIHBpcGVsaW5lIHdpdGggc3RhdHVzIGJyZWFrZG93bi4gKi9cbiJdfQ==
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Retention windows for reporting data: env-configurable, clamped to a sane
|
|
3
|
+
* range, and overridable per-org by the settings row.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Reporting retention windows (Phase 7). Records in `pipeline_events`,
|
|
7
|
+
* `deployment_outcomes`, and `incidents` grow unbounded without a sweep, so a
|
|
8
|
+
* split, per-org retention purge (see {@link ReportingService.purgeExpiredReportingData})
|
|
9
|
+
* hard-deletes rows older than these windows, by `created_at`:
|
|
10
|
+
* - **Standard events** — `pipeline_events` with `environment IS NULL`
|
|
11
|
+
* (non-deploy STAGE/ACTION/build). High volume → short default (30 days).
|
|
12
|
+
* - **DORA source** — `pipeline_events` with `environment IS NOT NULL` (deploy
|
|
13
|
+
* stages) plus all of `deployment_outcomes` and `incidents`. Low volume →
|
|
14
|
+
* longer default (180 days). DORA history is therefore bounded by this window;
|
|
15
|
+
* the report query still hard-caps at 365 days (a longer per-org override only
|
|
16
|
+
* retains raw source rows, never widens a report).
|
|
17
|
+
* `ingest_health` and `dora_settings` are never purged. Env-overridable globals;
|
|
18
|
+
* a per-org override in `dora_settings` wins (see {@link resolveEventRetentionDays}).
|
|
19
|
+
*/
|
|
20
|
+
export declare const RETENTION_MIN_DAYS = 1;
|
|
21
|
+
export declare const RETENTION_MAX_DAYS = 730;
|
|
22
|
+
/** Parse a positive-int env day-count, clamped to [MIN, MAX], else `fallback`. */
|
|
23
|
+
export declare function retentionEnvDays(name: string, fallback: number): number;
|
|
24
|
+
export declare const REPORTING_EVENT_RETENTION_DAYS: number;
|
|
25
|
+
export declare const REPORTING_DORA_RETENTION_DAYS: number;
|
|
26
|
+
/** Effective standard-event retention (days): a valid per-org override wins,
|
|
27
|
+
* else the global env default. Shared by the sweep + the settings surface so
|
|
28
|
+
* they can't drift. The `-1` unlimited sentinel (Phase 8) passes straight
|
|
29
|
+
* through — the sweep reads it as "keep forever, skip this org's standard
|
|
30
|
+
* events". A positive override clamps to [1, RETENTION_MAX_DAYS]; a `null`, a
|
|
31
|
+
* non-integer, or any other out-of-range value (e.g. 0, -2) falls back to the
|
|
32
|
+
* env default. */
|
|
33
|
+
export declare function resolveEventRetentionDays(override?: number | null): number;
|
|
34
|
+
/** Effective DORA-source retention (days): a valid per-org override wins, else
|
|
35
|
+
* the global env default. The `-1` unlimited sentinel (Phase 8) passes straight
|
|
36
|
+
* through — the sweep reads it as "keep forever, skip this org's DORA-source
|
|
37
|
+
* windows". A positive override clamps to [1, RETENTION_MAX_DAYS]; a `null`, a
|
|
38
|
+
* non-integer, or any other out-of-range value (e.g. 0, -2) falls back to the
|
|
39
|
+
* env default. */
|
|
40
|
+
export declare function resolveDoraRetentionDays(override?: number | null): number;
|
|
41
|
+
/** The cutoff instant for a retention window: rows with `created_at` strictly
|
|
42
|
+
* before this (`< cutoff`) are expired. Deterministic for a fixed `now`. */
|
|
43
|
+
export declare function retentionCutoff(now: Date, days: number): Date;
|
|
44
|
+
/** Per-org retention override row (Phase 7). Both nullable ⇒ global defaults. */
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// Copyright 2026 Pipeline Builder Contributors
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
/**
|
|
4
|
+
* Retention windows for reporting data: env-configurable, clamped to a sane
|
|
5
|
+
* range, and overridable per-org by the settings row.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Reporting retention windows (Phase 7). Records in `pipeline_events`,
|
|
9
|
+
* `deployment_outcomes`, and `incidents` grow unbounded without a sweep, so a
|
|
10
|
+
* split, per-org retention purge (see {@link ReportingService.purgeExpiredReportingData})
|
|
11
|
+
* hard-deletes rows older than these windows, by `created_at`:
|
|
12
|
+
* - **Standard events** — `pipeline_events` with `environment IS NULL`
|
|
13
|
+
* (non-deploy STAGE/ACTION/build). High volume → short default (30 days).
|
|
14
|
+
* - **DORA source** — `pipeline_events` with `environment IS NOT NULL` (deploy
|
|
15
|
+
* stages) plus all of `deployment_outcomes` and `incidents`. Low volume →
|
|
16
|
+
* longer default (180 days). DORA history is therefore bounded by this window;
|
|
17
|
+
* the report query still hard-caps at 365 days (a longer per-org override only
|
|
18
|
+
* retains raw source rows, never widens a report).
|
|
19
|
+
* `ingest_health` and `dora_settings` are never purged. Env-overridable globals;
|
|
20
|
+
* a per-org override in `dora_settings` wins (see {@link resolveEventRetentionDays}).
|
|
21
|
+
*/
|
|
22
|
+
export const RETENTION_MIN_DAYS = 1;
|
|
23
|
+
export const RETENTION_MAX_DAYS = 730;
|
|
24
|
+
/** Parse a positive-int env day-count, clamped to [MIN, MAX], else `fallback`. */
|
|
25
|
+
export function retentionEnvDays(name, fallback) {
|
|
26
|
+
const raw = Number(process.env[name]);
|
|
27
|
+
if (!Number.isFinite(raw) || !Number.isInteger(raw) || raw < RETENTION_MIN_DAYS)
|
|
28
|
+
return fallback;
|
|
29
|
+
return Math.min(raw, RETENTION_MAX_DAYS);
|
|
30
|
+
}
|
|
31
|
+
export const REPORTING_EVENT_RETENTION_DAYS = retentionEnvDays('REPORTING_EVENT_RETENTION_DAYS', 30);
|
|
32
|
+
export const REPORTING_DORA_RETENTION_DAYS = retentionEnvDays('REPORTING_DORA_RETENTION_DAYS', 180);
|
|
33
|
+
/** Effective standard-event retention (days): a valid per-org override wins,
|
|
34
|
+
* else the global env default. Shared by the sweep + the settings surface so
|
|
35
|
+
* they can't drift. The `-1` unlimited sentinel (Phase 8) passes straight
|
|
36
|
+
* through — the sweep reads it as "keep forever, skip this org's standard
|
|
37
|
+
* events". A positive override clamps to [1, RETENTION_MAX_DAYS]; a `null`, a
|
|
38
|
+
* non-integer, or any other out-of-range value (e.g. 0, -2) falls back to the
|
|
39
|
+
* env default. */
|
|
40
|
+
export function resolveEventRetentionDays(override) {
|
|
41
|
+
if (override === -1)
|
|
42
|
+
return -1;
|
|
43
|
+
return override != null && Number.isInteger(override) && override >= RETENTION_MIN_DAYS
|
|
44
|
+
? Math.min(override, RETENTION_MAX_DAYS) : REPORTING_EVENT_RETENTION_DAYS;
|
|
45
|
+
}
|
|
46
|
+
/** Effective DORA-source retention (days): a valid per-org override wins, else
|
|
47
|
+
* the global env default. The `-1` unlimited sentinel (Phase 8) passes straight
|
|
48
|
+
* through — the sweep reads it as "keep forever, skip this org's DORA-source
|
|
49
|
+
* windows". A positive override clamps to [1, RETENTION_MAX_DAYS]; a `null`, a
|
|
50
|
+
* non-integer, or any other out-of-range value (e.g. 0, -2) falls back to the
|
|
51
|
+
* env default. */
|
|
52
|
+
export function resolveDoraRetentionDays(override) {
|
|
53
|
+
if (override === -1)
|
|
54
|
+
return -1;
|
|
55
|
+
return override != null && Number.isInteger(override) && override >= RETENTION_MIN_DAYS
|
|
56
|
+
? Math.min(override, RETENTION_MAX_DAYS) : REPORTING_DORA_RETENTION_DAYS;
|
|
57
|
+
}
|
|
58
|
+
/** The cutoff instant for a retention window: rows with `created_at` strictly
|
|
59
|
+
* before this (`< cutoff`) are expired. Deterministic for a fixed `now`. */
|
|
60
|
+
export function retentionCutoff(now, days) {
|
|
61
|
+
return new Date(now.getTime() - days * 86_400_000);
|
|
62
|
+
}
|
|
63
|
+
/** Per-org retention override row (Phase 7). Both nullable ⇒ global defaults. */
|
|
64
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicmV0ZW50aW9uLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vc3JjL2FwaS9yZXBvcnRpbmcvcmV0ZW50aW9uLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLCtDQUErQztBQUMvQyxzQ0FBc0M7QUFFdEM7OztHQUdHO0FBRUg7Ozs7Ozs7Ozs7Ozs7O0dBY0c7QUFDSCxNQUFNLENBQUMsTUFBTSxrQkFBa0IsR0FBRyxDQUFDLENBQUM7QUFDcEMsTUFBTSxDQUFDLE1BQU0sa0JBQWtCLEdBQUcsR0FBRyxDQUFDO0FBRXRDLGtGQUFrRjtBQUNsRixNQUFNLFVBQVUsZ0JBQWdCLENBQUMsSUFBWSxFQUFFLFFBQWdCO0lBQzdELE1BQU0sR0FBRyxHQUFHLE1BQU0sQ0FBQyxPQUFPLENBQUMsR0FBRyxDQUFDLElBQUksQ0FBQyxDQUFDLENBQUM7SUFDdEMsSUFBSSxDQUFDLE1BQU0sQ0FBQyxRQUFRLENBQUMsR0FBRyxDQUFDLElBQUksQ0FBQyxNQUFNLENBQUMsU0FBUyxDQUFDLEdBQUcsQ0FBQyxJQUFJLEdBQUcsR0FBRyxrQkFBa0I7UUFBRSxPQUFPLFFBQVEsQ0FBQztJQUNqRyxPQUFPLElBQUksQ0FBQyxHQUFHLENBQUMsR0FBRyxFQUFFLGtCQUFrQixDQUFDLENBQUM7QUFDM0MsQ0FBQztBQUVELE1BQU0sQ0FBQyxNQUFNLDhCQUE4QixHQUFHLGdCQUFnQixDQUFDLGdDQUFnQyxFQUFFLEVBQUUsQ0FBQyxDQUFDO0FBQ3JHLE1BQU0sQ0FBQyxNQUFNLDZCQUE2QixHQUFHLGdCQUFnQixDQUFDLCtCQUErQixFQUFFLEdBQUcsQ0FBQyxDQUFDO0FBRXBHOzs7Ozs7bUJBTW1CO0FBQ25CLE1BQU0sVUFBVSx5QkFBeUIsQ0FBQyxRQUF3QjtJQUNoRSxJQUFJLFFBQVEsS0FBSyxDQUFDLENBQUM7UUFBRSxPQUFPLENBQUMsQ0FBQyxDQUFDO0lBQy9CLE9BQU8sUUFBUSxJQUFJLElBQUksSUFBSSxNQUFNLENBQUMsU0FBUyxDQUFDLFFBQVEsQ0FBQyxJQUFJLFFBQVEsSUFBSSxrQkFBa0I7UUFDckYsQ0FBQyxDQUFDLElBQUksQ0FBQyxHQUFHLENBQUMsUUFBUSxFQUFFLGtCQUFrQixDQUFDLENBQUMsQ0FBQyxDQUFDLDhCQUE4QixDQUFDO0FBQzlFLENBQUM7QUFFRDs7Ozs7bUJBS21CO0FBQ25CLE1BQU0sVUFBVSx3QkFBd0IsQ0FBQyxRQUF3QjtJQUMvRCxJQUFJLFFBQVEsS0FBSyxDQUFDLENBQUM7UUFBRSxPQUFPLENBQUMsQ0FBQyxDQUFDO0lBQy9CLE9BQU8sUUFBUSxJQUFJLElBQUksSUFBSSxNQUFNLENBQUMsU0FBUyxDQUFDLFFBQVEsQ0FBQyxJQUFJLFFBQVEsSUFBSSxrQkFBa0I7UUFDckYsQ0FBQyxDQUFDLElBQUksQ0FBQyxHQUFHLENBQUMsUUFBUSxFQUFFLGtCQUFrQixDQUFDLENBQUMsQ0FBQyxDQUFDLDZCQUE2QixDQUFDO0FBQzdFLENBQUM7QUFFRDs2RUFDNkU7QUFDN0UsTUFBTSxVQUFVLGVBQWUsQ0FBQyxHQUFTLEVBQUUsSUFBWTtJQUNyRCxPQUFPLElBQUksSUFBSSxDQUFDLEdBQUcsQ0FBQyxPQUFPLEVBQUUsR0FBRyxJQUFJLEdBQUcsVUFBVSxDQUFDLENBQUM7QUFDckQsQ0FBQztBQUVELGlGQUFpRiIsInNvdXJjZXNDb250ZW50IjpbIi8vIENvcHlyaWdodCAyMDI2IFBpcGVsaW5lIEJ1aWxkZXIgQ29udHJpYnV0b3JzXG4vLyBTUERYLUxpY2Vuc2UtSWRlbnRpZmllcjogQXBhY2hlLTIuMFxuXG4vKipcbiAqIFJldGVudGlvbiB3aW5kb3dzIGZvciByZXBvcnRpbmcgZGF0YTogZW52LWNvbmZpZ3VyYWJsZSwgY2xhbXBlZCB0byBhIHNhbmVcbiAqIHJhbmdlLCBhbmQgb3ZlcnJpZGFibGUgcGVyLW9yZyBieSB0aGUgc2V0dGluZ3Mgcm93LlxuICovXG5cbi8qKlxuICogUmVwb3J0aW5nIHJldGVudGlvbiB3aW5kb3dzIChQaGFzZSA3KS4gUmVjb3JkcyBpbiBgcGlwZWxpbmVfZXZlbnRzYCxcbiAqIGBkZXBsb3ltZW50X291dGNvbWVzYCwgYW5kIGBpbmNpZGVudHNgIGdyb3cgdW5ib3VuZGVkIHdpdGhvdXQgYSBzd2VlcCwgc28gYVxuICogc3BsaXQsIHBlci1vcmcgcmV0ZW50aW9uIHB1cmdlIChzZWUge0BsaW5rIFJlcG9ydGluZ1NlcnZpY2UucHVyZ2VFeHBpcmVkUmVwb3J0aW5nRGF0YX0pXG4gKiBoYXJkLWRlbGV0ZXMgcm93cyBvbGRlciB0aGFuIHRoZXNlIHdpbmRvd3MsIGJ5IGBjcmVhdGVkX2F0YDpcbiAqICAtICoqU3RhbmRhcmQgZXZlbnRzKiog4oCUIGBwaXBlbGluZV9ldmVudHNgIHdpdGggYGVudmlyb25tZW50IElTIE5VTExgXG4gKiAgICAobm9uLWRlcGxveSBTVEFHRS9BQ1RJT04vYnVpbGQpLiBIaWdoIHZvbHVtZSDihpIgc2hvcnQgZGVmYXVsdCAoMzAgZGF5cykuXG4gKiAgLSAqKkRPUkEgc291cmNlKiog4oCUIGBwaXBlbGluZV9ldmVudHNgIHdpdGggYGVudmlyb25tZW50IElTIE5PVCBOVUxMYCAoZGVwbG95XG4gKiAgICBzdGFnZXMpIHBsdXMgYWxsIG9mIGBkZXBsb3ltZW50X291dGNvbWVzYCBhbmQgYGluY2lkZW50c2AuIExvdyB2b2x1bWUg4oaSXG4gKiAgICBsb25nZXIgZGVmYXVsdCAoMTgwIGRheXMpLiBET1JBIGhpc3RvcnkgaXMgdGhlcmVmb3JlIGJvdW5kZWQgYnkgdGhpcyB3aW5kb3c7XG4gKiAgICB0aGUgcmVwb3J0IHF1ZXJ5IHN0aWxsIGhhcmQtY2FwcyBhdCAzNjUgZGF5cyAoYSBsb25nZXIgcGVyLW9yZyBvdmVycmlkZSBvbmx5XG4gKiAgICByZXRhaW5zIHJhdyBzb3VyY2Ugcm93cywgbmV2ZXIgd2lkZW5zIGEgcmVwb3J0KS5cbiAqIGBpbmdlc3RfaGVhbHRoYCBhbmQgYGRvcmFfc2V0dGluZ3NgIGFyZSBuZXZlciBwdXJnZWQuIEVudi1vdmVycmlkYWJsZSBnbG9iYWxzO1xuICogYSBwZXItb3JnIG92ZXJyaWRlIGluIGBkb3JhX3NldHRpbmdzYCB3aW5zIChzZWUge0BsaW5rIHJlc29sdmVFdmVudFJldGVudGlvbkRheXN9KS5cbiAqL1xuZXhwb3J0IGNvbnN0IFJFVEVOVElPTl9NSU5fREFZUyA9IDE7XG5leHBvcnQgY29uc3QgUkVURU5USU9OX01BWF9EQVlTID0gNzMwO1xuXG4vKiogUGFyc2UgYSBwb3NpdGl2ZS1pbnQgZW52IGRheS1jb3VudCwgY2xhbXBlZCB0byBbTUlOLCBNQVhdLCBlbHNlIGBmYWxsYmFja2AuICovXG5leHBvcnQgZnVuY3Rpb24gcmV0ZW50aW9uRW52RGF5cyhuYW1lOiBzdHJpbmcsIGZhbGxiYWNrOiBudW1iZXIpOiBudW1iZXIge1xuICBjb25zdCByYXcgPSBOdW1iZXIocHJvY2Vzcy5lbnZbbmFtZV0pO1xuICBpZiAoIU51bWJlci5pc0Zpbml0ZShyYXcpIHx8ICFOdW1iZXIuaXNJbnRlZ2VyKHJhdykgfHwgcmF3IDwgUkVURU5USU9OX01JTl9EQVlTKSByZXR1cm4gZmFsbGJhY2s7XG4gIHJldHVybiBNYXRoLm1pbihyYXcsIFJFVEVOVElPTl9NQVhfREFZUyk7XG59XG5cbmV4cG9ydCBjb25zdCBSRVBPUlRJTkdfRVZFTlRfUkVURU5USU9OX0RBWVMgPSByZXRlbnRpb25FbnZEYXlzKCdSRVBPUlRJTkdfRVZFTlRfUkVURU5USU9OX0RBWVMnLCAzMCk7XG5leHBvcnQgY29uc3QgUkVQT1JUSU5HX0RPUkFfUkVURU5USU9OX0RBWVMgPSByZXRlbnRpb25FbnZEYXlzKCdSRVBPUlRJTkdfRE9SQV9SRVRFTlRJT05fREFZUycsIDE4MCk7XG5cbi8qKiBFZmZlY3RpdmUgc3RhbmRhcmQtZXZlbnQgcmV0ZW50aW9uIChkYXlzKTogYSB2YWxpZCBwZXItb3JnIG92ZXJyaWRlIHdpbnMsXG4gKiAgZWxzZSB0aGUgZ2xvYmFsIGVudiBkZWZhdWx0LiBTaGFyZWQgYnkgdGhlIHN3ZWVwICsgdGhlIHNldHRpbmdzIHN1cmZhY2Ugc29cbiAqICB0aGV5IGNhbid0IGRyaWZ0LiBUaGUgYC0xYCB1bmxpbWl0ZWQgc2VudGluZWwgKFBoYXNlIDgpIHBhc3NlcyBzdHJhaWdodFxuICogIHRocm91Z2gg4oCUIHRoZSBzd2VlcCByZWFkcyBpdCBhcyBcImtlZXAgZm9yZXZlciwgc2tpcCB0aGlzIG9yZydzIHN0YW5kYXJkXG4gKiAgZXZlbnRzXCIuIEEgcG9zaXRpdmUgb3ZlcnJpZGUgY2xhbXBzIHRvIFsxLCBSRVRFTlRJT05fTUFYX0RBWVNdOyBhIGBudWxsYCwgYVxuICogIG5vbi1pbnRlZ2VyLCBvciBhbnkgb3RoZXIgb3V0LW9mLXJhbmdlIHZhbHVlIChlLmcuIDAsIC0yKSBmYWxscyBiYWNrIHRvIHRoZVxuICogIGVudiBkZWZhdWx0LiAqL1xuZXhwb3J0IGZ1bmN0aW9uIHJlc29sdmVFdmVudFJldGVudGlvbkRheXMob3ZlcnJpZGU/OiBudW1iZXIgfCBudWxsKTogbnVtYmVyIHtcbiAgaWYgKG92ZXJyaWRlID09PSAtMSkgcmV0dXJuIC0xO1xuICByZXR1cm4gb3ZlcnJpZGUgIT0gbnVsbCAmJiBOdW1iZXIuaXNJbnRlZ2VyKG92ZXJyaWRlKSAmJiBvdmVycmlkZSA+PSBSRVRFTlRJT05fTUlOX0RBWVNcbiAgICA/IE1hdGgubWluKG92ZXJyaWRlLCBSRVRFTlRJT05fTUFYX0RBWVMpIDogUkVQT1JUSU5HX0VWRU5UX1JFVEVOVElPTl9EQVlTO1xufVxuXG4vKiogRWZmZWN0aXZlIERPUkEtc291cmNlIHJldGVudGlvbiAoZGF5cyk6IGEgdmFsaWQgcGVyLW9yZyBvdmVycmlkZSB3aW5zLCBlbHNlXG4gKiAgdGhlIGdsb2JhbCBlbnYgZGVmYXVsdC4gVGhlIGAtMWAgdW5saW1pdGVkIHNlbnRpbmVsIChQaGFzZSA4KSBwYXNzZXMgc3RyYWlnaHRcbiAqICB0aHJvdWdoIOKAlCB0aGUgc3dlZXAgcmVhZHMgaXQgYXMgXCJrZWVwIGZvcmV2ZXIsIHNraXAgdGhpcyBvcmcncyBET1JBLXNvdXJjZVxuICogIHdpbmRvd3NcIi4gQSBwb3NpdGl2ZSBvdmVycmlkZSBjbGFtcHMgdG8gWzEsIFJFVEVOVElPTl9NQVhfREFZU107IGEgYG51bGxgLCBhXG4gKiAgbm9uLWludGVnZXIsIG9yIGFueSBvdGhlciBvdXQtb2YtcmFuZ2UgdmFsdWUgKGUuZy4gMCwgLTIpIGZhbGxzIGJhY2sgdG8gdGhlXG4gKiAgZW52IGRlZmF1bHQuICovXG5leHBvcnQgZnVuY3Rpb24gcmVzb2x2ZURvcmFSZXRlbnRpb25EYXlzKG92ZXJyaWRlPzogbnVtYmVyIHwgbnVsbCk6IG51bWJlciB7XG4gIGlmIChvdmVycmlkZSA9PT0gLTEpIHJldHVybiAtMTtcbiAgcmV0dXJuIG92ZXJyaWRlICE9IG51bGwgJiYgTnVtYmVyLmlzSW50ZWdlcihvdmVycmlkZSkgJiYgb3ZlcnJpZGUgPj0gUkVURU5USU9OX01JTl9EQVlTXG4gICAgPyBNYXRoLm1pbihvdmVycmlkZSwgUkVURU5USU9OX01BWF9EQVlTKSA6IFJFUE9SVElOR19ET1JBX1JFVEVOVElPTl9EQVlTO1xufVxuXG4vKiogVGhlIGN1dG9mZiBpbnN0YW50IGZvciBhIHJldGVudGlvbiB3aW5kb3c6IHJvd3Mgd2l0aCBgY3JlYXRlZF9hdGAgc3RyaWN0bHlcbiAqICBiZWZvcmUgdGhpcyAoYDwgY3V0b2ZmYCkgYXJlIGV4cGlyZWQuIERldGVybWluaXN0aWMgZm9yIGEgZml4ZWQgYG5vd2AuICovXG5leHBvcnQgZnVuY3Rpb24gcmV0ZW50aW9uQ3V0b2ZmKG5vdzogRGF0ZSwgZGF5czogbnVtYmVyKTogRGF0ZSB7XG4gIHJldHVybiBuZXcgRGF0ZShub3cuZ2V0VGltZSgpIC0gZGF5cyAqIDg2XzQwMF8wMDApO1xufVxuXG4vKiogUGVyLW9yZyByZXRlbnRpb24gb3ZlcnJpZGUgcm93IChQaGFzZSA3KS4gQm90aCBudWxsYWJsZSDih5IgZ2xvYmFsIGRlZmF1bHRzLiAqL1xuIl19
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { sql } from 'drizzle-orm';
|
|
2
|
+
/**
|
|
3
|
+
* Defense-in-depth guard for the `DATE_TRUNC(${interval}, …)` bucket argument.
|
|
4
|
+
* The route layer (`parseReportInterval`) is the security boundary and already
|
|
5
|
+
* rejects unknown intervals; this assert makes an invalid value fail fast at the
|
|
6
|
+
* service boundary with a clear error instead of surfacing a raw Postgres error
|
|
7
|
+
* if a caller ever bypasses the route validation. Shares api-core's
|
|
8
|
+
* REPORT_INTERVALS allow-list so the two can't drift.
|
|
9
|
+
*/
|
|
10
|
+
export declare function assertReportInterval(interval: string): void;
|
|
11
|
+
/**
|
|
12
|
+
* Scrub AWS identifiers from an optional free-form string, preserving `undefined`
|
|
13
|
+
* (so an absent field stays absent rather than becoming an empty scrubbed string).
|
|
14
|
+
* The DURABLE persistence boundary for the ingest paths — every user/AWS-derived
|
|
15
|
+
* string field is funneled through this before insert.
|
|
16
|
+
*/
|
|
17
|
+
export declare function scrubOptional(value: string | undefined): string | undefined;
|
|
18
|
+
/**
|
|
19
|
+
* Cache for reporting aggregations. Two tiers:
|
|
20
|
+
* - Inventory queries (plugin summary/distribution/versions): 5 min TTL — changes on plugin CRUD
|
|
21
|
+
* - Execution/build queries with date ranges: 2 min TTL — new events arrive continuously
|
|
22
|
+
*/
|
|
23
|
+
/**
|
|
24
|
+
* Roll a group of deploy/stage events (grouped per execution) up to one terminal
|
|
25
|
+
* status: FAILED wins, then SUCCEEDED, else OTHER (still-running / non-terminal).
|
|
26
|
+
* Shared by the three per-(env|stage, execution) rollup CTEs — DORA metrics, DORA
|
|
27
|
+
* trend, and build health — so the precedence can't drift between them. Assumes the
|
|
28
|
+
* grouped event alias is `e`.
|
|
29
|
+
*/
|
|
30
|
+
export declare const terminalStatusRollup: import("drizzle-orm").SQL<unknown>;
|
|
31
|
+
/** Optional `[from,to]` window on `e.started_at` for the execution reports —
|
|
32
|
+
* a no-op `sql` fragment when either bound is absent (all-time). Shared by the
|
|
33
|
+
* execution-count and per-pipeline-execution reports so they narrow identically. */
|
|
34
|
+
export declare function optionalStartedAtRange(range?: {
|
|
35
|
+
from?: string;
|
|
36
|
+
to?: string;
|
|
37
|
+
}): ReturnType<typeof sql>;
|
|
38
|
+
/**
|
|
39
|
+
* Per-execution terminal deploy row (D1 — one row per (environment, execution),
|
|
40
|
+
* NOT per stage). The SQL rolls every deploy STAGE targeting an env within an
|
|
41
|
+
* execution up to a single terminal status; the JS shaping buckets these by
|
|
42
|
+
* environment. `commit_ts` (D2) is the execution's EARLIEST commit time across
|
|
43
|
+
* all its events — commit enrichment rides the PIPELINE/source event, not the
|
|
44
|
+
* deploy STAGE — joined by execution_id (null when unresolved → lead `unknown`).
|
|
45
|
+
* `in_window` is false for a look-back-only row kept solely so incidents opened
|
|
46
|
+
* near `from` can correlate to a deploy that completed just before the window.
|
|
47
|
+
*/
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// Copyright 2026 Pipeline Builder Contributors
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
/**
|
|
4
|
+
* Small SQL fragments and input guards shared across the reporting queries.
|
|
5
|
+
*/
|
|
6
|
+
import { REPORT_INTERVALS, scrubAwsIdentifiersFromString } from '@pipeline-builder/api-core';
|
|
7
|
+
import { sql } from 'drizzle-orm';
|
|
8
|
+
/**
|
|
9
|
+
* Defense-in-depth guard for the `DATE_TRUNC(${interval}, …)` bucket argument.
|
|
10
|
+
* The route layer (`parseReportInterval`) is the security boundary and already
|
|
11
|
+
* rejects unknown intervals; this assert makes an invalid value fail fast at the
|
|
12
|
+
* service boundary with a clear error instead of surfacing a raw Postgres error
|
|
13
|
+
* if a caller ever bypasses the route validation. Shares api-core's
|
|
14
|
+
* REPORT_INTERVALS allow-list so the two can't drift.
|
|
15
|
+
*/
|
|
16
|
+
export function assertReportInterval(interval) {
|
|
17
|
+
if (!REPORT_INTERVALS.includes(interval)) {
|
|
18
|
+
throw new Error(`Invalid report interval: ${interval}. Expected one of: ${REPORT_INTERVALS.join(', ')}`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Scrub AWS identifiers from an optional free-form string, preserving `undefined`
|
|
23
|
+
* (so an absent field stays absent rather than becoming an empty scrubbed string).
|
|
24
|
+
* The DURABLE persistence boundary for the ingest paths — every user/AWS-derived
|
|
25
|
+
* string field is funneled through this before insert.
|
|
26
|
+
*/
|
|
27
|
+
export function scrubOptional(value) {
|
|
28
|
+
return value !== undefined ? scrubAwsIdentifiersFromString(value) : undefined;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Cache for reporting aggregations. Two tiers:
|
|
32
|
+
* - Inventory queries (plugin summary/distribution/versions): 5 min TTL — changes on plugin CRUD
|
|
33
|
+
* - Execution/build queries with date ranges: 2 min TTL — new events arrive continuously
|
|
34
|
+
*/
|
|
35
|
+
/**
|
|
36
|
+
* Roll a group of deploy/stage events (grouped per execution) up to one terminal
|
|
37
|
+
* status: FAILED wins, then SUCCEEDED, else OTHER (still-running / non-terminal).
|
|
38
|
+
* Shared by the three per-(env|stage, execution) rollup CTEs — DORA metrics, DORA
|
|
39
|
+
* trend, and build health — so the precedence can't drift between them. Assumes the
|
|
40
|
+
* grouped event alias is `e`.
|
|
41
|
+
*/
|
|
42
|
+
export const terminalStatusRollup = sql `CASE
|
|
43
|
+
WHEN bool_or(e.status = 'FAILED') THEN 'FAILED'
|
|
44
|
+
WHEN bool_or(e.status = 'SUCCEEDED') THEN 'SUCCEEDED'
|
|
45
|
+
ELSE 'OTHER'
|
|
46
|
+
END`;
|
|
47
|
+
/** Optional `[from,to]` window on `e.started_at` for the execution reports —
|
|
48
|
+
* a no-op `sql` fragment when either bound is absent (all-time). Shared by the
|
|
49
|
+
* execution-count and per-pipeline-execution reports so they narrow identically. */
|
|
50
|
+
export function optionalStartedAtRange(range) {
|
|
51
|
+
return range?.from && range?.to
|
|
52
|
+
? sql `AND e.started_at >= ${range.from}::timestamptz AND e.started_at <= ${range.to}::timestamptz`
|
|
53
|
+
: sql ``;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Per-execution terminal deploy row (D1 — one row per (environment, execution),
|
|
57
|
+
* NOT per stage). The SQL rolls every deploy STAGE targeting an env within an
|
|
58
|
+
* execution up to a single terminal status; the JS shaping buckets these by
|
|
59
|
+
* environment. `commit_ts` (D2) is the execution's EARLIEST commit time across
|
|
60
|
+
* all its events — commit enrichment rides the PIPELINE/source event, not the
|
|
61
|
+
* deploy STAGE — joined by execution_id (null when unresolved → lead `unknown`).
|
|
62
|
+
* `in_window` is false for a look-back-only row kept solely so incidents opened
|
|
63
|
+
* near `from` can correlate to a deploy that completed just before the window.
|
|
64
|
+
*/
|
|
65
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic3FsLWhlbHBlcnMuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi9zcmMvYXBpL3JlcG9ydGluZy9zcWwtaGVscGVycy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSwrQ0FBK0M7QUFDL0Msc0NBQXNDO0FBRXRDOztHQUVHO0FBRUgsT0FBTyxFQUFFLGdCQUFnQixFQUFFLDZCQUE2QixFQUFFLE1BQU0sNEJBQTRCLENBQUM7QUFDN0YsT0FBTyxFQUFFLEdBQUcsRUFBRSxNQUFNLGFBQWEsQ0FBQztBQUVsQzs7Ozs7OztHQU9HO0FBQ0gsTUFBTSxVQUFVLG9CQUFvQixDQUFDLFFBQWdCO0lBQ25ELElBQUksQ0FBRSxnQkFBc0MsQ0FBQyxRQUFRLENBQUMsUUFBUSxDQUFDLEVBQUUsQ0FBQztRQUNoRSxNQUFNLElBQUksS0FBSyxDQUFDLDRCQUE0QixRQUFRLHNCQUFzQixnQkFBZ0IsQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFDLEVBQUUsQ0FBQyxDQUFDO0lBQzNHLENBQUM7QUFDSCxDQUFDO0FBRUQ7Ozs7O0dBS0c7QUFDSCxNQUFNLFVBQVUsYUFBYSxDQUFDLEtBQXlCO0lBQ3JELE9BQU8sS0FBSyxLQUFLLFNBQVMsQ0FBQyxDQUFDLENBQUMsNkJBQTZCLENBQUMsS0FBSyxDQUFDLENBQUMsQ0FBQyxDQUFDLFNBQVMsQ0FBQztBQUNoRixDQUFDO0FBRUQ7Ozs7R0FJRztBQUVIOzs7Ozs7R0FNRztBQUNILE1BQU0sQ0FBQyxNQUFNLG9CQUFvQixHQUFHLEdBQUcsQ0FBQTs7OztnQkFJdkIsQ0FBQztBQUVqQjs7cUZBRXFGO0FBQ3JGLE1BQU0sVUFBVSxzQkFBc0IsQ0FBQyxLQUFzQztJQUMzRSxPQUFPLEtBQUssRUFBRSxJQUFJLElBQUksS0FBSyxFQUFFLEVBQUU7UUFDN0IsQ0FBQyxDQUFDLEdBQUcsQ0FBQSx1QkFBdUIsS0FBSyxDQUFDLElBQUkscUNBQXFDLEtBQUssQ0FBQyxFQUFFLGVBQWU7UUFDbEcsQ0FBQyxDQUFDLEdBQUcsQ0FBQSxFQUFFLENBQUM7QUFDWixDQUFDO0FBRUQ7Ozs7Ozs7OztHQVNHIiwic291cmNlc0NvbnRlbnQiOlsiLy8gQ29weXJpZ2h0IDIwMjYgUGlwZWxpbmUgQnVpbGRlciBDb250cmlidXRvcnNcbi8vIFNQRFgtTGljZW5zZS1JZGVudGlmaWVyOiBBcGFjaGUtMi4wXG5cbi8qKlxuICogU21hbGwgU1FMIGZyYWdtZW50cyBhbmQgaW5wdXQgZ3VhcmRzIHNoYXJlZCBhY3Jvc3MgdGhlIHJlcG9ydGluZyBxdWVyaWVzLlxuICovXG5cbmltcG9ydCB7IFJFUE9SVF9JTlRFUlZBTFMsIHNjcnViQXdzSWRlbnRpZmllcnNGcm9tU3RyaW5nIH0gZnJvbSAnQHBpcGVsaW5lLWJ1aWxkZXIvYXBpLWNvcmUnO1xuaW1wb3J0IHsgc3FsIH0gZnJvbSAnZHJpenpsZS1vcm0nO1xuXG4vKipcbiAqIERlZmVuc2UtaW4tZGVwdGggZ3VhcmQgZm9yIHRoZSBgREFURV9UUlVOQygke2ludGVydmFsfSwg4oCmKWAgYnVja2V0IGFyZ3VtZW50LlxuICogVGhlIHJvdXRlIGxheWVyIChgcGFyc2VSZXBvcnRJbnRlcnZhbGApIGlzIHRoZSBzZWN1cml0eSBib3VuZGFyeSBhbmQgYWxyZWFkeVxuICogcmVqZWN0cyB1bmtub3duIGludGVydmFsczsgdGhpcyBhc3NlcnQgbWFrZXMgYW4gaW52YWxpZCB2YWx1ZSBmYWlsIGZhc3QgYXQgdGhlXG4gKiBzZXJ2aWNlIGJvdW5kYXJ5IHdpdGggYSBjbGVhciBlcnJvciBpbnN0ZWFkIG9mIHN1cmZhY2luZyBhIHJhdyBQb3N0Z3JlcyBlcnJvclxuICogaWYgYSBjYWxsZXIgZXZlciBieXBhc3NlcyB0aGUgcm91dGUgdmFsaWRhdGlvbi4gU2hhcmVzIGFwaS1jb3JlJ3NcbiAqIFJFUE9SVF9JTlRFUlZBTFMgYWxsb3ctbGlzdCBzbyB0aGUgdHdvIGNhbid0IGRyaWZ0LlxuICovXG5leHBvcnQgZnVuY3Rpb24gYXNzZXJ0UmVwb3J0SW50ZXJ2YWwoaW50ZXJ2YWw6IHN0cmluZyk6IHZvaWQge1xuICBpZiAoIShSRVBPUlRfSU5URVJWQUxTIGFzIHJlYWRvbmx5IHN0cmluZ1tdKS5pbmNsdWRlcyhpbnRlcnZhbCkpIHtcbiAgICB0aHJvdyBuZXcgRXJyb3IoYEludmFsaWQgcmVwb3J0IGludGVydmFsOiAke2ludGVydmFsfS4gRXhwZWN0ZWQgb25lIG9mOiAke1JFUE9SVF9JTlRFUlZBTFMuam9pbignLCAnKX1gKTtcbiAgfVxufVxuXG4vKipcbiAqIFNjcnViIEFXUyBpZGVudGlmaWVycyBmcm9tIGFuIG9wdGlvbmFsIGZyZWUtZm9ybSBzdHJpbmcsIHByZXNlcnZpbmcgYHVuZGVmaW5lZGBcbiAqIChzbyBhbiBhYnNlbnQgZmllbGQgc3RheXMgYWJzZW50IHJhdGhlciB0aGFuIGJlY29taW5nIGFuIGVtcHR5IHNjcnViYmVkIHN0cmluZykuXG4gKiBUaGUgRFVSQUJMRSBwZXJzaXN0ZW5jZSBib3VuZGFyeSBmb3IgdGhlIGluZ2VzdCBwYXRocyDigJQgZXZlcnkgdXNlci9BV1MtZGVyaXZlZFxuICogc3RyaW5nIGZpZWxkIGlzIGZ1bm5lbGVkIHRocm91Z2ggdGhpcyBiZWZvcmUgaW5zZXJ0LlxuICovXG5leHBvcnQgZnVuY3Rpb24gc2NydWJPcHRpb25hbCh2YWx1ZTogc3RyaW5nIHwgdW5kZWZpbmVkKTogc3RyaW5nIHwgdW5kZWZpbmVkIHtcbiAgcmV0dXJuIHZhbHVlICE9PSB1bmRlZmluZWQgPyBzY3J1YkF3c0lkZW50aWZpZXJzRnJvbVN0cmluZyh2YWx1ZSkgOiB1bmRlZmluZWQ7XG59XG5cbi8qKlxuICogQ2FjaGUgZm9yIHJlcG9ydGluZyBhZ2dyZWdhdGlvbnMuIFR3byB0aWVyczpcbiAqIC0gSW52ZW50b3J5IHF1ZXJpZXMgKHBsdWdpbiBzdW1tYXJ5L2Rpc3RyaWJ1dGlvbi92ZXJzaW9ucyk6IDUgbWluIFRUTCDigJQgY2hhbmdlcyBvbiBwbHVnaW4gQ1JVRFxuICogLSBFeGVjdXRpb24vYnVpbGQgcXVlcmllcyB3aXRoIGRhdGUgcmFuZ2VzOiAyIG1pbiBUVEwg4oCUIG5ldyBldmVudHMgYXJyaXZlIGNvbnRpbnVvdXNseVxuICovXG5cbi8qKlxuICogUm9sbCBhIGdyb3VwIG9mIGRlcGxveS9zdGFnZSBldmVudHMgKGdyb3VwZWQgcGVyIGV4ZWN1dGlvbikgdXAgdG8gb25lIHRlcm1pbmFsXG4gKiBzdGF0dXM6IEZBSUxFRCB3aW5zLCB0aGVuIFNVQ0NFRURFRCwgZWxzZSBPVEhFUiAoc3RpbGwtcnVubmluZyAvIG5vbi10ZXJtaW5hbCkuXG4gKiBTaGFyZWQgYnkgdGhlIHRocmVlIHBlci0oZW52fHN0YWdlLCBleGVjdXRpb24pIHJvbGx1cCBDVEVzIOKAlCBET1JBIG1ldHJpY3MsIERPUkFcbiAqIHRyZW5kLCBhbmQgYnVpbGQgaGVhbHRoIOKAlCBzbyB0aGUgcHJlY2VkZW5jZSBjYW4ndCBkcmlmdCBiZXR3ZWVuIHRoZW0uIEFzc3VtZXMgdGhlXG4gKiBncm91cGVkIGV2ZW50IGFsaWFzIGlzIGBlYC5cbiAqL1xuZXhwb3J0IGNvbnN0IHRlcm1pbmFsU3RhdHVzUm9sbHVwID0gc3FsYENBU0VcbiAgICAgICAgICAgICAgV0hFTiBib29sX29yKGUuc3RhdHVzID0gJ0ZBSUxFRCcpIFRIRU4gJ0ZBSUxFRCdcbiAgICAgICAgICAgICAgV0hFTiBib29sX29yKGUuc3RhdHVzID0gJ1NVQ0NFRURFRCcpIFRIRU4gJ1NVQ0NFRURFRCdcbiAgICAgICAgICAgICAgRUxTRSAnT1RIRVInXG4gICAgICAgICAgICBFTkRgO1xuXG4vKiogT3B0aW9uYWwgYFtmcm9tLHRvXWAgd2luZG93IG9uIGBlLnN0YXJ0ZWRfYXRgIGZvciB0aGUgZXhlY3V0aW9uIHJlcG9ydHMg4oCUXG4gKiAgYSBuby1vcCBgc3FsYCBmcmFnbWVudCB3aGVuIGVpdGhlciBib3VuZCBpcyBhYnNlbnQgKGFsbC10aW1lKS4gU2hhcmVkIGJ5IHRoZVxuICogIGV4ZWN1dGlvbi1jb3VudCBhbmQgcGVyLXBpcGVsaW5lLWV4ZWN1dGlvbiByZXBvcnRzIHNvIHRoZXkgbmFycm93IGlkZW50aWNhbGx5LiAqL1xuZXhwb3J0IGZ1bmN0aW9uIG9wdGlvbmFsU3RhcnRlZEF0UmFuZ2UocmFuZ2U/OiB7IGZyb20/OiBzdHJpbmc7IHRvPzogc3RyaW5nIH0pOiBSZXR1cm5UeXBlPHR5cGVvZiBzcWw+IHtcbiAgcmV0dXJuIHJhbmdlPy5mcm9tICYmIHJhbmdlPy50b1xuICAgID8gc3FsYEFORCBlLnN0YXJ0ZWRfYXQgPj0gJHtyYW5nZS5mcm9tfTo6dGltZXN0YW1wdHogQU5EIGUuc3RhcnRlZF9hdCA8PSAke3JhbmdlLnRvfTo6dGltZXN0YW1wdHpgXG4gICAgOiBzcWxgYDtcbn1cblxuLyoqXG4gKiBQZXItZXhlY3V0aW9uIHRlcm1pbmFsIGRlcGxveSByb3cgKEQxIOKAlCBvbmUgcm93IHBlciAoZW52aXJvbm1lbnQsIGV4ZWN1dGlvbiksXG4gKiBOT1QgcGVyIHN0YWdlKS4gVGhlIFNRTCByb2xscyBldmVyeSBkZXBsb3kgU1RBR0UgdGFyZ2V0aW5nIGFuIGVudiB3aXRoaW4gYW5cbiAqIGV4ZWN1dGlvbiB1cCB0byBhIHNpbmdsZSB0ZXJtaW5hbCBzdGF0dXM7IHRoZSBKUyBzaGFwaW5nIGJ1Y2tldHMgdGhlc2UgYnlcbiAqIGVudmlyb25tZW50LiBgY29tbWl0X3RzYCAoRDIpIGlzIHRoZSBleGVjdXRpb24ncyBFQVJMSUVTVCBjb21taXQgdGltZSBhY3Jvc3NcbiAqIGFsbCBpdHMgZXZlbnRzIOKAlCBjb21taXQgZW5yaWNobWVudCByaWRlcyB0aGUgUElQRUxJTkUvc291cmNlIGV2ZW50LCBub3QgdGhlXG4gKiBkZXBsb3kgU1RBR0Ug4oCUIGpvaW5lZCBieSBleGVjdXRpb25faWQgKG51bGwgd2hlbiB1bnJlc29sdmVkIOKGkiBsZWFkIGB1bmtub3duYCkuXG4gKiBgaW5fd2luZG93YCBpcyBmYWxzZSBmb3IgYSBsb29rLWJhY2stb25seSByb3cga2VwdCBzb2xlbHkgc28gaW5jaWRlbnRzIG9wZW5lZFxuICogbmVhciBgZnJvbWAgY2FuIGNvcnJlbGF0ZSB0byBhIGRlcGxveSB0aGF0IGNvbXBsZXRlZCBqdXN0IGJlZm9yZSB0aGUgd2luZG93LlxuICovXG4iXX0=
|