Class: SavedReportService
Defined in: server/insights/services/saved-report-service.ts:109
Saved-report CRUD on top of SavedReportsRepository.
Three invariants the repository alone does not give you:
- Validate on write.
create/updateDefinitionrun BOTHreportDefinitionSchema(shape) andvalidateDefinition(semantics, against the CURRENTLY registered model) before anything is stored, so a definition that references a since-removed field is rejected at write time instead of exploding on some later run. Failures surface as SavedReportValidationError carrying every issue. - Normalize on read. Every returned row's definition is re-run through
normalizeSavedReportDefinition, so a corrupt payload always surfaces as a typed error — never a raw Zod throw and never a half-parsed object. - Owner + visibility, twice. The repository enforces them authoritatively inside its own transaction; the service re-checks org scope, private-visibility and ownership on every path (defence in depth). Non-owner access to a private report is reported as SavedReportNotFoundError — including on the mutating paths, so a write attempt can't be used to probe for a report's existence.
Read-mostly, but not read-only: all writes go through the ambient PRIMARY client the repository holds, never a replica.
Corrupt rows stay recoverable. Normalize-on-read applies to what this
service HANDS BACK, not to whether a caller may write — which is why every
mutation authorizes through the repository's non-normalizing
getAuthorizationFields, never through get. A row whose stored
definition no longer parses can still be deleted (returns void, so
nothing needs parsing) and still be repaired with updateDefinition (which
replaces the definition, so its response parses). rename and
setVisibility are the residual: they authorize and write fine, but their
return value is the row itself, so a still-corrupt definition surfaces as
SavedReportValidationError AFTER the write has landed. Repair with
updateDefinition rather than renaming a broken row.
Constructors
Constructor
new SavedReportService(deps: SavedReportServiceDeps): SavedReportService;Defined in: server/insights/services/saved-report-service.ts:110
Parameters
| Parameter | Type |
|---|---|
deps | SavedReportServiceDeps |
Returns
SavedReportService
Methods
authorizeRead()
authorizeRead(id: string, ctx: SavedReportContext): Promise<void>;Defined in: server/insights/services/saved-report-service.ts:147
Assert that a report is visible without reading or parsing its definition.
Use this when another resource is authorized through its report id but
does not return or execute the report itself (for example schedule
metadata). A corrupt definition must still fail get/list, but it must
not make otherwise visible, independently valid metadata unreadable.
Parameters
| Parameter | Type |
|---|---|
id | string |
ctx | SavedReportContext |
Returns
Promise<void>
authorizeWrite()
authorizeWrite(id: string, ctx: SavedReportContext): Promise<void>;Defined in: server/insights/services/saved-report-service.ts:161
Assert that the caller owns a report without parsing its definition. Cross-resource mutators (for example delivery schedules) use this to share the exact same write-authorization model as the report they belong to.
Parameters
| Parameter | Type |
|---|---|
id | string |
ctx | SavedReportContext |
Returns
Promise<void>
create()
create(input: {
definition: {
aggregations?: {
alias: string;
field?: string;
fn: "min" | "max" | "count" | "avg" | "sum";
}[];
columns: string[];
filter?: FilterNode;
groupBy?: {
bucket?: "year" | "month" | "day" | "week" | "quarter";
field: string;
}[];
schemaVersion: 1;
sort: {
by: string;
direction: "asc" | "desc";
}[];
subject: string;
timezone: string;
totals?: boolean;
};
description?: string | null;
name: string;
subjectKey: string;
}, ctx: SavedReportContext): Promise<SavedReport>;Defined in: server/insights/services/saved-report-service.ts:166
Create a report owned by ctx.userId. Private until explicitly shared.
Parameters
| Parameter | Type | Description |
|---|---|---|
input | { definition: { aggregations?: { alias: string; field?: string; fn: "min" | "max" | "count" | "avg" | "sum"; }[]; columns: string[]; filter?: FilterNode; groupBy?: { bucket?: "year" | "month" | "day" | "week" | "quarter"; field: string; }[]; schemaVersion: 1; sort: { by: string; direction: "asc" | "desc"; }[]; subject: string; timezone: string; totals?: boolean; }; description?: string | null; name: string; subjectKey: string; } | - |
input.definition | { aggregations?: { alias: string; field?: string; fn: "min" | "max" | "count" | "avg" | "sum"; }[]; columns: string[]; filter?: FilterNode; groupBy?: { bucket?: "year" | "month" | "day" | "week" | "quarter"; field: string; }[]; schemaVersion: 1; sort: { by: string; direction: "asc" | "desc"; }[]; subject: string; timezone: string; totals?: boolean; } | - |
input.definition.aggregations? | { alias: string; field?: string; fn: "min" | "max" | "count" | "avg" | "sum"; }[] | - |
input.definition.columns | string[] | The SELECT list — and the ONLY source of output columns, for grouped runs as much as detail runs: the compiler derives its projection from columns alone and never from groupBy/aggregations. So an empty columns has no compilable projection at all (not merely a semantically odd one), which makes it a STRUCTURAL failure and puts the check here rather than in validateDefinition. Keeping it in the schema also means every surface that parses a definition — run input, saved-report create/update, normalizeSavedReportDefinition on read, canned reports in the registry — fails closed without having to remember a second validation pass. |
input.definition.filter? | FilterNode | - |
input.definition.groupBy? | { bucket?: "year" | "month" | "day" | "week" | "quarter"; field: string; }[] | - |
input.definition.schemaVersion | 1 | - |
input.definition.sort | { by: string; direction: "asc" | "desc"; }[] | - |
input.definition.subject | string | - |
input.definition.timezone | string | - |
input.definition.totals? | boolean | - |
input.description? | string | null | - |
input.name | string | - |
input.subjectKey | string | - |
ctx | SavedReportContext | - |
Returns
Promise<SavedReport>
delete()
delete(id: string, ctx: SavedReportContext): Promise<void>;Defined in: server/insights/services/saved-report-service.ts:247
Soft-delete a report the caller owns.
Parameters
| Parameter | Type |
|---|---|
id | string |
ctx | SavedReportContext |
Returns
Promise<void>
get()
get(id: string, ctx: SavedReportContext): Promise<SavedReport>;Defined in: server/insights/services/saved-report-service.ts:130
One report by id. A report in another org, a soft-deleted one, or another user's PRIVATE one all surface as SavedReportNotFoundError (no existence leak).
Parameters
| Parameter | Type |
|---|---|
id | string |
ctx | SavedReportContext |
Returns
Promise<SavedReport>
list()
list(filter: ListSavedReportsFilter, ctx: SavedReportContext): Promise<SavedReport[]>;Defined in: server/insights/services/saved-report-service.ts:113
Reports visible to ctx.userId in ctx.organizationId, name-ordered.
Parameters
| Parameter | Type |
|---|---|
filter | ListSavedReportsFilter |
ctx | SavedReportContext |
Returns
Promise<SavedReport[]>
rename()
rename(input: {
id: string;
name: string;
}, ctx: SavedReportContext): Promise<SavedReport>;Defined in: server/insights/services/saved-report-service.ts:186
Rename a report the caller owns.
Parameters
| Parameter | Type |
|---|---|
input | { id: string; name: string; } |
input.id | string |
input.name | string |
ctx | SavedReportContext |
Returns
Promise<SavedReport>
setVisibility()
setVisibility(input: {
id: string;
visibility: "public" | "private";
}, ctx: SavedReportContext): Promise<SavedReport>;Defined in: server/insights/services/saved-report-service.ts:230
Share (public) or unshare (private) a report the caller owns.
Parameters
| Parameter | Type |
|---|---|
input | { id: string; visibility: "public" | "private"; } |
input.id | string |
input.visibility | "public" | "private" |
ctx | SavedReportContext |
Returns
Promise<SavedReport>
updateDefinition()
updateDefinition(input: {
definition: {
aggregations?: {
alias: string;
field?: string;
fn: "min" | "max" | "count" | "avg" | "sum";
}[];
columns: string[];
filter?: FilterNode;
groupBy?: {
bucket?: "year" | "month" | "day" | "week" | "quarter";
field: string;
}[];
schemaVersion: 1;
sort: {
by: string;
direction: "asc" | "desc";
}[];
subject: string;
timezone: string;
totals?: boolean;
};
description?: string | null;
id: string;
name?: string;
}, ctx: SavedReportContext): Promise<SavedReport>;Defined in: server/insights/services/saved-report-service.ts:207
Replace a report's definition (optionally its name/description) after
re-validating against the current model. The new definition's subject
must still match the row's stored subjectKey — that column is what
list({ subjectKey }) filters on, and the repository never rewrites it,
so allowing a subject switch would silently desynchronize the two.
Parameters
| Parameter | Type | Description |
|---|---|---|
input | { definition: { aggregations?: { alias: string; field?: string; fn: "min" | "max" | "count" | "avg" | "sum"; }[]; columns: string[]; filter?: FilterNode; groupBy?: { bucket?: "year" | "month" | "day" | "week" | "quarter"; field: string; }[]; schemaVersion: 1; sort: { by: string; direction: "asc" | "desc"; }[]; subject: string; timezone: string; totals?: boolean; }; description?: string | null; id: string; name?: string; } | - |
input.definition | { aggregations?: { alias: string; field?: string; fn: "min" | "max" | "count" | "avg" | "sum"; }[]; columns: string[]; filter?: FilterNode; groupBy?: { bucket?: "year" | "month" | "day" | "week" | "quarter"; field: string; }[]; schemaVersion: 1; sort: { by: string; direction: "asc" | "desc"; }[]; subject: string; timezone: string; totals?: boolean; } | - |
input.definition.aggregations? | { alias: string; field?: string; fn: "min" | "max" | "count" | "avg" | "sum"; }[] | - |
input.definition.columns | string[] | The SELECT list — and the ONLY source of output columns, for grouped runs as much as detail runs: the compiler derives its projection from columns alone and never from groupBy/aggregations. So an empty columns has no compilable projection at all (not merely a semantically odd one), which makes it a STRUCTURAL failure and puts the check here rather than in validateDefinition. Keeping it in the schema also means every surface that parses a definition — run input, saved-report create/update, normalizeSavedReportDefinition on read, canned reports in the registry — fails closed without having to remember a second validation pass. |
input.definition.filter? | FilterNode | - |
input.definition.groupBy? | { bucket?: "year" | "month" | "day" | "week" | "quarter"; field: string; }[] | - |
input.definition.schemaVersion | 1 | - |
input.definition.sort | { by: string; direction: "asc" | "desc"; }[] | - |
input.definition.subject | string | - |
input.definition.timezone | string | - |
input.definition.totals? | boolean | - |
input.description? | string | null | - |
input.id | string | - |
input.name? | string | - |
ctx | SavedReportContext | - |
Returns
Promise<SavedReport>