Kaizen
Browse modulesInsightsinsights/serverClasses

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:

  1. Validate on write. create/updateDefinition run BOTH reportDefinitionSchema (shape) and validateDefinition (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.
  2. 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.
  3. 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

ParameterType
depsSavedReportServiceDeps

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

ParameterType
idstring
ctxSavedReportContext

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

ParameterType
idstring
ctxSavedReportContext

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

ParameterTypeDescription
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.columnsstring[]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.schemaVersion1-
input.definition.sort{ by: string; direction: "asc" | "desc"; }[]-
input.definition.subjectstring-
input.definition.timezonestring-
input.definition.totals?boolean-
input.description?string | null-
input.namestring-
input.subjectKeystring-
ctxSavedReportContext-

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

ParameterType
idstring
ctxSavedReportContext

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

ParameterType
idstring
ctxSavedReportContext

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

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

ParameterType
input{ id: string; name: string; }
input.idstring
input.namestring
ctxSavedReportContext

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

ParameterType
input{ id: string; visibility: "public" | "private"; }
input.idstring
input.visibility"public" | "private"
ctxSavedReportContext

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

ParameterTypeDescription
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.columnsstring[]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.schemaVersion1-
input.definition.sort{ by: string; direction: "asc" | "desc"; }[]-
input.definition.subjectstring-
input.definition.timezonestring-
input.definition.totals?boolean-
input.description?string | null-
input.idstring-
input.name?string-
ctxSavedReportContext-

Returns

Promise<SavedReport>

On this page