SDK Reference

Reports

company/report/definition.ts (ReportDefinition) and company/report/report.ts (RunReport, ReportRow, and the supporting filter/groupBy/metric types). This is an ad-hoc/saved aggregation-query builderover a fixed allowlist of resource types — not a set of canned reports. There is no persisted "Report" or result entity: running a ReportDefinition re-executes its saved filter/groupBy/metrics/sort shape against live data every time via RunReport, rather than returning a cached snapshot.

Reportable resources (REPORT_RESOURCES)

report.ts defines REPORT_RESOURCES, a const allowlist mirroring the backend registry (per the code comment — the backend is the source of truth and rejects anything not listed there). ReportResourceName = keyof typeof REPORT_RESOURCES:

ResourcedimensionFieldsdateFieldsmetricFields
checkoutsid, companyID, memberID, soldByMemberID, payingMemberID, status, createdAt, updatedAt, resolvedAt, canceledAtcreatedAt, updatedAt, resolvedAt, canceledAtamount, subtotalAmount
invoicesid, checkoutID, companyID, status, currency, billingReason, periodStart, periodEnd, dueDate, paidAt, createdAt, updatedAtperiodStart, periodEnd, dueDate, paidAt, createdAt, updatedAtamountDue, amountPaid, amountRemaining
paymentsid, companyID, checkoutID, invoiceID, status, currency, paymentMethodType, cardBrand, paidAt, refundedAt, createdAt, updatedAtpaidAt, refundedAt, createdAt, updatedAtamount, amountRefunded
account_credit_transactionsid, companyID, memberID, source, checkoutID, invoiceID, createdByUserID, createdAtcreatedAtamount, balanceAfter
payroll_run_linesid, companyID, payrollRunID, staffID, rateID, createdAtcreatedAtregularUnits, overtimeUnits, regularAmount, overtimeAmount, bonusAmount, totalAmount

ReportResourceConfig splits each resource's fields into three roles: dimensionFields (safe to filter/group-by as-is), dateFields (a subset of dimension fields that are also safe to bucket via ReportGroupBy.bucket), and metricFields (safe to aggregate via ReportMetric). invoices and payments correspond to the Payment domain class — "the class is named Paymentfor backward compatibility, it represents what the API calls an invoice"; payments here is the Stripe payment/charge record, distinct from the invoices resource. checkouts corresponds to Checkout; payroll_run_lines to PayrollRunLine; account_credit_transactions to AccountCreditTransaction.

Not reportable:Any resource type not in this table — e.g. Member, Visit, Credit, Booking, Class/ClassInstance, Product, Subscription, CompanyEvent, StaffTimeBlock — has no entry in REPORT_RESOURCES and cannot be targeted by RunReport/ReportDefinition. The code doesn't explain why these five were chosen; treat the table above as the complete, current list rather than inferring a pattern.

Shared query shape

Four types in report.ts compose the filter/groupBy/metric/sort request, generic over a field-name union Field extends string = string (narrowed by ReportResourceConfig's three arrays when a caller wants autocomplete, same pattern as QueryRequest<T>):

ts
const ReportAggFnList = ["count", "sum", "avg", "min", "max"] as const;
type ReportAggFn = (typeof ReportAggFnList)[number];

const ReportBucketList = ["day", "week", "month"] as const;
type ReportBucket = (typeof ReportBucketList)[number];

interface ReportGroupBy<Field extends string = string> {
  field: Field;
  bucket?: ReportBucket; // buckets a date field by day/week/month; omit to group as-is on a dimension field
}

interface ReportMetric<Field extends string = string> {
  field: Field | "*";  // "*" is only meaningful with agg: "count"
  agg: ReportAggFn;
}

interface ReportSort {
  on: "group" | "metric";
  index: number;        // index into groupBy (on: "group") or metrics (on: "metric") — referenced by position, not name
  order?: "asc" | "desc";
}

RunReportRequest / RunSavedReportRequest / ReportRow

ts
interface RunReportRequest<Field extends string = string> {
  resource: ReportResourceName;
  filter?: QueryRequest<Field>["filter"];
  logic?: "and" | "or";
  groupBy?: ReportGroupBy<Field>[];
  metrics: ReportMetric<Field>[];
  sort?: ReportSort;
  limit?: number;
  offset?: number;
  includeDeleted?: boolean;
}

interface RunSavedReportRequest {
  reportDefinitionID: string;
  limit?: number;
  offset?: number;
  includeDeleted?: boolean;
}

RunSavedReportRequest runs a previously saved ReportDefinition instead of sending the full ad-hoc shape; its limit/offset/includeDeletedoverride the saved definition's values when provided (the definition itself carries no limit/offset/includeDeleted — those three are per-run-only, never persisted).

ts
type ReportRow = Record<string, string | number | null>;

Each row is dynamically keyed, not a fixed interface — column names are constructed positionally from the request's own groupBy/metrics arrays (per the doc comment on ReportRow):

ColumnShape
Group-by columnsg{n}_{field}, or g{n}_{field}_{bucket} when the group-by entry has a bucket — n is the entry's index in the groupBy array sent
Metric columnsm{n}_{agg}_{field}, or m{n}_{agg}_all for a field: "*" count — n is the entry's index in the metrics array sent

E.g. { groupBy: [{ field: "status" }], metrics: [{ field: "amount", agg: "sum" }, { field: "*", agg: "count" }] } produces rows shaped like { g0_status: "resolved", m0_sum_amount: 4820, m1_count_all: 12 }.

RunReport(company, request)

ts
async function RunReport<Field extends string = string>(
  company: Company,
  request: RunReportRequest<Field> | RunSavedReportRequest,
): Promise<SimpleResult<ReportRow[]>>

POST /company/{companyID}/report — server-side filter/groupBy/metric aggregation over an allowlisted resource (see REPORT_RESOURCES above for what each resource supports). Accepts either shape in the union: a RunReportRequest (ad-hoc — specifies resource plus the full filter/groupBy/metrics/sort shape) or a RunSavedReportRequest ({ reportDefinitionID, limit?, offset?, includeDeleted? } — re-runs a saved ReportDefinition by id). The backend distinguishes the two by the presence of reportDefinitionID vs. resource/metrics.

Note:Prefer calling this through Company/ReportDefinition instance methods per the SDK-wide convention rather than importing RunReport directly, except where no saved definition exists yet (the ad-hoc case has no instance to call it from).

ReportDefinition

definition.ts. A saved query config — the filter/groupBy/metrics/sort shape plus name/resource, persisted so it can be re-run via Run() without resending the full shape each time. DataStructure<ReportDefinitionData>, endpoint /company/{companyID}/report/definitions.

Data fields

ts
interface ReportDefinitionShape {
  filter?: QueryRequest["filter"];
  logic?: "and" | "or";
  groupBy?: ReportGroupBy[];
  metrics: ReportMetric[];
  sort?: ReportSort;
}

interface LazyReportDefinitionData {
  id: string;
  companyID: string;
  name: string;
  resource: ReportResourceName;
  definition: ReportDefinitionShape;
  createdByUserID?: string | null;
}

interface ReportDefinitionData extends LazyReportDefinitionData {
  createdAt: Date;
  updatedAt: Date;
}

ReportDefinitionShape matches RunReportRequest minus resource (which lives alongside definition on the row, not nested inside it) and minus limit/offset/includeDeleted (per-run-only, as noted above).

Gotcha:definition is a JSON column — per the code comment, mysql2 auto-parses it back into an object on read, so LazyReportDefinitionData and the full ReportDefinitionData carry the exact same definitionshape; there's no separate lazy/full split for this field the way most other resources trim detail fields out of their lazy shape.

CreateReportDefinitionData is the create payload: ReportDefinitionShape & { name: string; resource: ReportResourceName } — i.e. name/resourceflattened alongside the shape's own fields, not wrapped in a nested definition key.

Constructing

ts
new ReportDefinition(id: string, company: Company);
new ReportDefinition(data: LazyReportDefinitionData | ReportDefinitionData, company: Company, lazy?: boolean);

Static methods

ts
ReportDefinition.create(company: Company, definitions: CreateReportDefinitionData[]): Promise<EventData<string[]>>

POST /company/{companyID}/report/definitions with the array body.

ts
ReportDefinition.query(company: Company, data: QueryRequest): Promise<SimpleResult<ReportDefinition[]>>

POST /company/{companyID}/report/definitions/query — standard query shape; wraps each row in new ReportDefinition(d, company, lazy) (lazy defaults to true when data.lazy is undefined).

Instance methods

ts
definition.save(): Promise<SimpleResult<EventData>>
Gotcha:PATCH /company/{companyID}/report/definitions with [{ id, name, resource, ...definition }] — flattens the nested definition object back out to top-level fields before sending. Per the code comment, the backend rebuilds the definition JSON column from top-level filter/logic/groupBy/metrics/sort fields on PATCH and does not accept a nested definition object back — this is the inverse of what fetch()/query()return, so don't assume save() can just re-send the cached row shape unmodified.
ts
definition.delete(): Promise<SimpleResult<EventData>>

DELETE /company/{companyID}/report/definitions?ids={id}.

ts
definition.Run(opts?: { limit?: number; offset?: number; includeDeleted?: boolean }): Promise<SimpleResult<ReportRow[]>>

Executes the saved definition by delegating to RunReport(this.company, { reportDefinitionID: this.id, ...opts }) — i.e. it always sends a RunSavedReportRequest, never re-sends the definition's own filter/groupBy/metrics/sort shape (the backend re-derives that server-side from the saved row). opts.limit/opts.offset/opts.includeDeletedoverride the saved definition's values for this run only; omitting optsentirely runs with the backend's defaults for those three. Returns fresh ReportRow[] on every call — nothing is cached or persisted between runs.

Examples

Running a saved ReportDefinition

ts
import { ReportDefinition } from "zensile-sdk/company/report/definition";

// Load (or construct) the saved definition, then execute it
const definition = new ReportDefinition(reportDefinitionID, company);
const result = await definition.Run({ limit: 50 });

if (result.status === 200) {
  for (const row of result.data ?? []) {
    console.log(row); // e.g. { g0_status: "resolved", m0_sum_amount: 4820 }
  }
}

Ad-hoc RunReport call

ts
import { RunReport } from "zensile-sdk/company/report/report";

const result = await RunReport(company, {
  resource: "checkouts",
  filter: [{ field: "status", condition: "=", value: "resolved" }],
  groupBy: [{ field: "createdAt", bucket: "month" }],
  metrics: [
    { field: "amount", agg: "sum" },
    { field: "*", agg: "count" },
  ],
  sort: { on: "metric", index: 0, order: "desc" },
  limit: 12,
});

if (result.status === 200) {
  // rows shaped like { g0_createdAt_month: "2026-07", m0_sum_amount: 12300, m1_count_all: 41 }
  console.log(result.data);
}

Saving the same shape as a reusable ReportDefinition instead of re-sending it ad hoc every time:

ts
import { ReportDefinition } from "zensile-sdk/company/report/definition";

const created = await ReportDefinition.create(company, [
  {
    name: "Monthly checkout revenue",
    resource: "checkouts",
    filter: [{ field: "status", condition: "=", value: "resolved" }],
    groupBy: [{ field: "createdAt", bucket: "month" }],
    metrics: [{ field: "amount", agg: "sum" }],
  },
]);