@happyvertical/smrt-reports 0.43.0 → 0.43.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/dist/index.js CHANGED
@@ -8,6 +8,7 @@ import { DataQueryValidationError, ObjectRegistry, SmrtCollection, SmrtObject, c
8
8
  import { toSnakeCase } from "@happyvertical/smrt-core/utils";
9
9
  import { getTenantId, withSystemContext } from "@happyvertical/smrt-tenancy";
10
10
  import { tableExists, validateColumnName } from "@happyvertical/sql";
11
+ import { createHash } from "node:crypto";
11
12
  //#region src/lifecycle.ts
12
13
  function registeredReport(reportCtor) {
13
14
  return ObjectRegistry.getClassByConstructor(reportCtor) ?? ObjectRegistry.getClass(reportCtor.name);
@@ -131,7 +132,7 @@ async function getReportLifecycle(reportCtor, options) {
131
132
  }
132
133
  };
133
134
  }
134
- function actionContext(reportCtor, phase, mode, refreshAction) {
135
+ function actionContext$1(reportCtor, phase, mode, refreshAction) {
135
136
  return {
136
137
  phase,
137
138
  reportClassName: canonicalClassName(reportCtor),
@@ -145,7 +146,7 @@ async function actionMode(reportCtor, requested) {
145
146
  return (await buildReportDefinition(reportCtor)).refresh?.mode ?? "rebuild";
146
147
  }
147
148
  async function previewReportRefresh(reportCtor, options) {
148
- const action = actionContext(reportCtor, "preview", await actionMode(reportCtor, options.mode), options.refreshAction);
149
+ const action = actionContext$1(reportCtor, "preview", await actionMode(reportCtor, options.mode), options.refreshAction);
149
150
  await options.host.authorize(action);
150
151
  await options.host.audit(action);
151
152
  return {
@@ -157,7 +158,7 @@ async function previewReportRefresh(reportCtor, options) {
157
158
  }
158
159
  async function applyReportRefresh(reportCtor, options) {
159
160
  const mode = await actionMode(reportCtor, options.mode);
160
- const action = actionContext(reportCtor, "apply", mode, options.refreshAction);
161
+ const action = actionContext$1(reportCtor, "apply", mode, options.refreshAction);
161
162
  await options.host.authorize(action);
162
163
  await options.host.audit(action);
163
164
  const tenantId = lifecycleTenantId(reportCtor);
@@ -988,6 +989,526 @@ var SmrtReportCollection = class extends SmrtCollection {
988
989
  }
989
990
  };
990
991
  //#endregion
991
- export { REPORT_LOCKS_TABLE, REPORT_REFRESH_TASKS_TABLE, REPORT_RUNS_TABLE, REPORT_RUNTIME_TABLES, REPORT_SCHEDULER_TABLES, REPORT_SCHEDULES_TABLE, REPORT_WATERMARKS_TABLE, ReportScheduleRunner, SmrtReport, SmrtReportCollection, SmrtReportLock, SmrtReportLockCollection, SmrtReportRefreshTask, SmrtReportRun, SmrtReportRunCollection, SmrtReportSchedule, SmrtReportScheduleCollection, SmrtReportWatermark, SmrtReportWatermarkCollection, aggregate, applyReportRefresh, assertReportTablesReady, avg, bucketExpr, buildAggregate, buildReportAdapterDescriptor, buildReportDefinition, buildReportDrilldownQuery, compileReportDefinition, compileReportSpec, count, day, enqueueReportRefresh, ensureReportRefreshSchedules, getReportGroupingColumns, getReportLifecycle, getRuntimeReportOptions, groupBy, hour, max, min, minute, month, previewReportRefresh, quarter, queryReportMaterializedRows, refreshReport, registerReportRefreshInterceptor, report, reportMaterializedRowKey, reportRefreshOutcome, reportRowIdentity, scopeKeyForTenant, splitReportFilterScopes, sum, week, year };
992
+ //#region src/views.ts
993
+ var INHERITED_REPORT_SCOPE = [
994
+ "principal",
995
+ "tenant",
996
+ "report-definition",
997
+ "field-policy"
998
+ ];
999
+ var EXPORT_FORMATS = /* @__PURE__ */ new Set(["csv", "json"]);
1000
+ var PINNED_COLUMN_SIDES = /* @__PURE__ */ new Set(["start", "end"]);
1001
+ var DISPLAY_DENSITIES = /* @__PURE__ */ new Set(["compact", "comfortable"]);
1002
+ var RFC_3339_INSTANT = /^(\d{4})-(\d{2})-(\d{2})T([01]\d|2[0-3]):([0-5]\d):([0-5]\d)(?:\.\d{1,9})?(?:Z|[+-](?:[01]\d|2[0-3]):[0-5]\d)$/;
1003
+ var DEFAULT_EXPORT_LIMITS = {
1004
+ maxRows: 1e5,
1005
+ maxBytes: 25e6,
1006
+ deadlineMs: 6e4,
1007
+ foregroundRowLimit: 1e3
1008
+ };
1009
+ var MAX_EXPORT_LIMITS = {
1010
+ maxRows: 1e6,
1011
+ maxBytes: 1e8,
1012
+ deadlineMs: 3e5,
1013
+ foregroundRowLimit: 1e5
1014
+ };
1015
+ var ReportSurfaceValidationError = class extends Error {
1016
+ constructor(message) {
1017
+ super(message);
1018
+ this.name = "ReportSurfaceValidationError";
1019
+ }
1020
+ };
1021
+ function fail(message) {
1022
+ throw new ReportSurfaceValidationError(message);
1023
+ }
1024
+ function plainObject(value, label) {
1025
+ if (!value || typeof value !== "object" || Array.isArray(value)) return fail(`${label} must be a plain object`);
1026
+ const prototype = Object.getPrototypeOf(value);
1027
+ if (prototype !== Object.prototype && prototype !== null) return fail(`${label} must be a plain object`);
1028
+ return value;
1029
+ }
1030
+ function exactKeys(value, allowed, label) {
1031
+ for (const key of Object.keys(value)) if (!allowed.includes(key)) fail(`${label} contains an unsupported field: ${key}`);
1032
+ }
1033
+ function requiredString(value, label, maxLength = 256) {
1034
+ if (typeof value !== "string") return fail(`${label} must be a string`);
1035
+ const normalized = value.trim();
1036
+ if (!normalized) return fail(`${label} cannot be empty`);
1037
+ if (normalized.length > maxLength) return fail(`${label} cannot exceed ${String(maxLength)} characters`);
1038
+ return normalized;
1039
+ }
1040
+ function optionalBoolean(value, label) {
1041
+ if (value === void 0) return void 0;
1042
+ if (typeof value !== "boolean") return fail(`${label} must be a boolean`);
1043
+ return value;
1044
+ }
1045
+ function boundedInteger(value, label, minimum, maximum) {
1046
+ if (!Number.isInteger(value) || value < minimum) return fail(`${label} must be an integer of at least ${String(minimum)}`);
1047
+ if (value > maximum) return fail(`${label} cannot exceed ${String(maximum)}`);
1048
+ return value;
1049
+ }
1050
+ function normalizeIso(value, label) {
1051
+ const text = requiredString(value, label, 64);
1052
+ const match = RFC_3339_INSTANT.exec(text);
1053
+ if (!match) return fail(`${label} must be an RFC 3339 instant`);
1054
+ const [year, month, day] = match.slice(1, 4).map(Number);
1055
+ const calendar = /* @__PURE__ */ new Date(0);
1056
+ calendar.setUTCFullYear(year, month - 1, day);
1057
+ calendar.setUTCHours(0, 0, 0, 0);
1058
+ if (calendar.getUTCFullYear() !== year || calendar.getUTCMonth() !== month - 1 || calendar.getUTCDate() !== day) return fail(`${label} must be an RFC 3339 instant`);
1059
+ const parsed = new Date(text);
1060
+ if (Number.isNaN(parsed.getTime())) return fail(`${label} must be an RFC 3339 instant`);
1061
+ return parsed.toISOString();
1062
+ }
1063
+ function normalizeSnapshotBinding(value) {
1064
+ const input = plainObject(value, "Report export snapshot binding");
1065
+ exactKeys(input, ["id"], "Report export snapshot binding");
1066
+ return { id: requiredString(input.id, "Report export snapshot binding id") };
1067
+ }
1068
+ function stable(value) {
1069
+ if (value === null || typeof value === "boolean" || typeof value === "number") return JSON.stringify(value);
1070
+ if (typeof value === "string") return JSON.stringify(value);
1071
+ if (Array.isArray(value)) return `[${value.map(stable).join(",")}]`;
1072
+ const object = plainObject(value, "Report surface value");
1073
+ return "{" + Object.keys(object).sort().map((key) => `${JSON.stringify(key)}:${stable(object[key])}`).join(",") + "}";
1074
+ }
1075
+ function reportDefinitionFingerprint(descriptor) {
1076
+ const definition = {
1077
+ resourceId: descriptor.resourceId,
1078
+ reportClassName: descriptor.reportClassName,
1079
+ sourceClassName: descriptor.sourceClassName,
1080
+ tenantScoped: descriptor.tenantScoped,
1081
+ tenantField: descriptor.tenantField ?? null,
1082
+ identityField: descriptor.identityField,
1083
+ columns: descriptor.columns.map((column) => ({
1084
+ id: column.id,
1085
+ fieldName: column.fieldName,
1086
+ kind: column.kind,
1087
+ filterScope: column.filterScope,
1088
+ type: column.type,
1089
+ projectable: column.projectable === true,
1090
+ sortable: column.sortable === true,
1091
+ facetable: column.facetable === true,
1092
+ filterOperators: column.filterOperators ?? [],
1093
+ capabilities: [...column.capabilities].sort(),
1094
+ sensitivity: column.sensitivity ?? null,
1095
+ sourceColumn: column.sourceColumn ?? null,
1096
+ bucket: column.bucket ?? null,
1097
+ aggregate: column.aggregate ?? null,
1098
+ distinct: column.distinct ?? null
1099
+ })),
1100
+ schema: descriptor.schema
1101
+ };
1102
+ return "rv1_" + createHash("sha256").update(stable(definition), "utf8").digest("base64url");
1103
+ }
1104
+ function columnMap(descriptor) {
1105
+ return new Map(descriptor.columns.map((column) => [column.id, column]));
1106
+ }
1107
+ function requireColumnCapability(descriptor, id, capability, label) {
1108
+ const columnId = requiredString(id, label);
1109
+ if (!columnMap(descriptor).get(columnId)?.capabilities.includes(capability)) return fail(`${label} is not available under the current report policy`);
1110
+ return columnId;
1111
+ }
1112
+ function normalizeColumns(descriptor, value) {
1113
+ if (value === void 0) return descriptor.columns.filter((column) => column.capabilities.includes("project")).map((column) => ({ id: column.id }));
1114
+ if (!Array.isArray(value)) return fail("Saved report view columns must be an array");
1115
+ if (value.length > descriptor.columns.length) return fail("Saved report view columns cannot repeat or exceed exposed columns");
1116
+ const ids = /* @__PURE__ */ new Set();
1117
+ return value.map((entry, index) => {
1118
+ const input = plainObject(entry, `Saved report view columns[${String(index)}]`);
1119
+ const id = requireColumnCapability(descriptor, input.id, "project", `Saved report view columns[${String(index)}].id`);
1120
+ if (ids.has(id)) return fail("Saved report view columns cannot repeat ids");
1121
+ ids.add(id);
1122
+ const width = input.width === void 0 ? void 0 : boundedInteger(input.width, `Saved report view columns[${String(index)}].width`, 40, 2e3);
1123
+ const pinned = input.pinned === void 0 ? void 0 : requiredString(input.pinned, `Saved report view columns[${String(index)}].pinned`, 16);
1124
+ if (pinned !== void 0 && !PINNED_COLUMN_SIDES.has(pinned)) return fail(`Saved report view columns[${String(index)}].pinned is invalid`);
1125
+ return {
1126
+ id,
1127
+ ...width === void 0 ? {} : { width },
1128
+ ...pinned === void 0 ? {} : { pinned }
1129
+ };
1130
+ });
1131
+ }
1132
+ function normalizeGrouping(descriptor, value) {
1133
+ if (value === void 0) return void 0;
1134
+ const input = plainObject(value, "Saved report view grouping");
1135
+ if (!Array.isArray(input.fields)) return fail("Saved report view grouping.fields must be an array");
1136
+ const fields = input.fields.map((field, index) => requireColumnCapability(descriptor, field, "group", `Saved report view grouping.fields[${String(index)}]`));
1137
+ if (new Set(fields).size !== fields.length) return fail("Saved report view grouping.fields cannot repeat ids");
1138
+ const expanded = optionalBoolean(input.expanded, "Saved report view grouping.expanded");
1139
+ return {
1140
+ fields,
1141
+ ...expanded === void 0 ? {} : { expanded }
1142
+ };
1143
+ }
1144
+ function normalizeDisplay(value) {
1145
+ if (value === void 0) return void 0;
1146
+ const input = plainObject(value, "Saved report view display");
1147
+ const density = requiredString(input.density, "Saved report view display.density", 16);
1148
+ if (!DISPLAY_DENSITIES.has(density)) return fail("Saved report view display.density is invalid");
1149
+ const showTotals = optionalBoolean(input.showTotals, "Saved report view display.showTotals");
1150
+ return {
1151
+ density,
1152
+ ...showTotals === void 0 ? {} : { showTotals }
1153
+ };
1154
+ }
1155
+ function migrateReportSavedView(value) {
1156
+ const input = plainObject(value, "Saved report view");
1157
+ if (input.version === void 0 || input.version === 0) return {
1158
+ ...input,
1159
+ version: 1
1160
+ };
1161
+ if (input.version === 1) return input;
1162
+ return fail("Saved report view version is unsupported");
1163
+ }
1164
+ function normalizeReportSavedView(descriptor, value) {
1165
+ const input = migrateReportSavedView(value);
1166
+ const fingerprint = reportDefinitionFingerprint(descriptor);
1167
+ if (input.resourceId !== void 0 && input.resourceId !== descriptor.resourceId) return fail("Saved report view belongs to a different report resource");
1168
+ if (input.reportClassName !== void 0 && input.reportClassName !== descriptor.reportClassName) return fail("Saved report view belongs to a different report class");
1169
+ if (input.definitionFingerprint !== void 0 && input.definitionFingerprint !== fingerprint) return fail("Saved report view definition has changed");
1170
+ const query = normalizeDataQueryRequest(input.query, descriptor.schema);
1171
+ if (query.mode !== "rows") return fail("Saved report views must use a rows query");
1172
+ splitReportFilterScopes(descriptor, query.filter);
1173
+ const columns = normalizeColumns(descriptor, input.columns);
1174
+ const grouping = normalizeGrouping(descriptor, input.grouping);
1175
+ const display = normalizeDisplay(input.display);
1176
+ return {
1177
+ version: 1,
1178
+ id: requiredString(input.id, "Saved report view id"),
1179
+ title: requiredString(input.title, "Saved report view title", 240),
1180
+ resourceId: descriptor.resourceId,
1181
+ reportClassName: descriptor.reportClassName,
1182
+ definitionFingerprint: fingerprint,
1183
+ query,
1184
+ columns,
1185
+ ...grouping === void 0 ? {} : { grouping },
1186
+ ...display === void 0 ? {} : { display }
1187
+ };
1188
+ }
1189
+ function restoreReportSavedView(descriptor, savedView) {
1190
+ return normalizeReportSavedView(descriptor, savedView);
1191
+ }
1192
+ function createReportExportSnapshot(descriptor, input, result, binding) {
1193
+ const request = normalizeDataQueryRequest(input, descriptor.schema);
1194
+ if (request.mode !== "rows") return fail("Report export snapshots require a rows query");
1195
+ const queryFingerprint = createDataQueryFingerprint(request, descriptor.schema);
1196
+ if (result.queryFingerprint !== queryFingerprint) return fail("Report export result does not match the requested query fingerprint");
1197
+ if (result.identityField !== descriptor.identityField) return fail("Report export result has an unexpected row identity field");
1198
+ if (result.total.kind !== "exact") return fail("Report export snapshots require an exact row count");
1199
+ if (!Number.isSafeInteger(result.total.value) || result.total.value < 0) return fail("Report export snapshots require a safe non-negative row count");
1200
+ if (result.truncated) return fail("Report export snapshots cannot use a truncated query result");
1201
+ if (result.freshness.state === "unknown") return fail("Report export snapshots require lifecycle freshness");
1202
+ const lifecycleAsOf = result.reportLifecycle?.snapshot.asOf;
1203
+ const freshnessAsOf = result.freshness.asOf;
1204
+ const normalizedLifecycleAsOf = lifecycleAsOf ? normalizeIso(lifecycleAsOf, "Report export lifecycle snapshot asOf") : void 0;
1205
+ const normalizedFreshnessAsOf = freshnessAsOf ? normalizeIso(freshnessAsOf, "Report export freshness asOf") : void 0;
1206
+ if (normalizedLifecycleAsOf !== void 0 && normalizedFreshnessAsOf !== void 0 && normalizedLifecycleAsOf !== normalizedFreshnessAsOf) return fail("Report export lifecycle freshness does not match its snapshot");
1207
+ const normalizedAsOf = normalizedLifecycleAsOf ?? normalizedFreshnessAsOf;
1208
+ if (!normalizedAsOf) return fail("Report export snapshots require an as-of materialization instant");
1209
+ const refreshedAt = result.reportLifecycle?.snapshot.refreshedAt;
1210
+ return {
1211
+ version: 1,
1212
+ resourceId: descriptor.resourceId,
1213
+ reportClassName: descriptor.reportClassName,
1214
+ definitionFingerprint: reportDefinitionFingerprint(descriptor),
1215
+ binding: normalizeSnapshotBinding(binding),
1216
+ request,
1217
+ queryFingerprint,
1218
+ projection: [...request.projection ?? [descriptor.identityField]],
1219
+ sort: [...request.sort ?? descriptor.schema.defaultSort ?? [{
1220
+ field: descriptor.identityField,
1221
+ direction: "asc"
1222
+ }]],
1223
+ freshness: {
1224
+ state: result.freshness.state,
1225
+ asOf: normalizedAsOf
1226
+ },
1227
+ snapshot: {
1228
+ asOf: normalizedAsOf,
1229
+ ...refreshedAt ? { refreshedAt: normalizeIso(refreshedAt, "Report export snapshot refreshedAt") } : {},
1230
+ stale: result.freshness.state === "stale"
1231
+ },
1232
+ total: {
1233
+ kind: "exact",
1234
+ value: result.total.value
1235
+ },
1236
+ inherits: [...INHERITED_REPORT_SCOPE]
1237
+ };
1238
+ }
1239
+ function verifyReportExportSnapshot(descriptor, snapshot) {
1240
+ exactKeys(plainObject(snapshot, "Report export snapshot"), [
1241
+ "version",
1242
+ "resourceId",
1243
+ "reportClassName",
1244
+ "definitionFingerprint",
1245
+ "binding",
1246
+ "request",
1247
+ "queryFingerprint",
1248
+ "projection",
1249
+ "sort",
1250
+ "freshness",
1251
+ "snapshot",
1252
+ "total",
1253
+ "inherits"
1254
+ ], "Report export snapshot");
1255
+ if (snapshot.version !== 1) return fail("Report export snapshot version is unsupported");
1256
+ if (snapshot.resourceId !== descriptor.resourceId || snapshot.reportClassName !== descriptor.reportClassName) return fail("Report export snapshot belongs to a different report");
1257
+ if (snapshot.definitionFingerprint !== reportDefinitionFingerprint(descriptor)) return fail("Report export definition has changed");
1258
+ const binding = normalizeSnapshotBinding(snapshot.binding);
1259
+ const normalized = normalizeDataQueryRequest(snapshot.request, descriptor.schema);
1260
+ if (normalized.mode !== "rows") return fail("Report export snapshot query must remain a rows query");
1261
+ if (createDataQueryFingerprint(normalized, descriptor.schema) !== snapshot.queryFingerprint) return fail("Report export snapshot query fingerprint is invalid");
1262
+ const expectedProjection = normalized.projection ?? [descriptor.identityField];
1263
+ const expectedSort = normalized.sort ?? descriptor.schema.defaultSort ?? [{
1264
+ field: descriptor.identityField,
1265
+ direction: "asc"
1266
+ }];
1267
+ if (!Array.isArray(snapshot.projection) || snapshot.projection.some((field) => typeof field !== "string")) return fail("Report export snapshot projection is invalid");
1268
+ if (snapshot.projection.length !== expectedProjection.length || snapshot.projection.some((field, index) => field !== expectedProjection[index])) return fail("Report export snapshot projection or sort has changed");
1269
+ if (!Array.isArray(snapshot.sort)) return fail("Report export snapshot sort is invalid");
1270
+ if (stable(snapshot.sort.map((value) => {
1271
+ const sortInput = plainObject(value, "Report export snapshot sort");
1272
+ exactKeys(sortInput, ["field", "direction"], "Report export snapshot sort");
1273
+ return {
1274
+ field: requiredString(sortInput.field, "Report export snapshot sort field"),
1275
+ direction: requiredString(sortInput.direction, "Report export snapshot sort direction")
1276
+ };
1277
+ })) !== stable(expectedSort)) return fail("Report export snapshot projection or sort has changed");
1278
+ const freshness = plainObject(snapshot.freshness, "Report export snapshot freshness");
1279
+ exactKeys(freshness, ["state", "asOf"], "Report export snapshot freshness");
1280
+ const materialization = plainObject(snapshot.snapshot, "Report export snapshot materialization");
1281
+ exactKeys(materialization, [
1282
+ "asOf",
1283
+ "refreshedAt",
1284
+ "stale"
1285
+ ], "Report export snapshot materialization");
1286
+ const total = plainObject(snapshot.total, "Report export snapshot total");
1287
+ exactKeys(total, ["kind", "value"], "Report export snapshot total");
1288
+ const totalValue = total.value;
1289
+ const asOf = normalizeIso(materialization.asOf, "Report export snapshot asOf");
1290
+ if (freshness.state !== "fresh" && freshness.state !== "stale") return fail("Report export snapshot freshness state is invalid");
1291
+ const freshnessAsOf = normalizeIso(freshness.asOf, "Report export snapshot freshness asOf");
1292
+ if (freshnessAsOf !== asOf) return fail("Report export snapshot freshness does not match its as-of instant");
1293
+ const refreshedAt = materialization.refreshedAt ? normalizeIso(materialization.refreshedAt, "Report export snapshot refreshedAt") : void 0;
1294
+ if (materialization.stale !== (freshness.state === "stale")) return fail("Report export snapshot stale state is invalid");
1295
+ if (total.kind !== "exact" || typeof totalValue !== "number" || !Number.isSafeInteger(totalValue) || totalValue < 0) return fail("Report export snapshot row count is invalid");
1296
+ if (!Array.isArray(snapshot.inherits) || snapshot.inherits.some((value) => typeof value !== "string") || snapshot.inherits.length !== INHERITED_REPORT_SCOPE.length || snapshot.inherits.some((value, index) => value !== INHERITED_REPORT_SCOPE[index])) return fail("Report export snapshot inheritance contract is invalid");
1297
+ return {
1298
+ version: 1,
1299
+ resourceId: descriptor.resourceId,
1300
+ reportClassName: descriptor.reportClassName,
1301
+ definitionFingerprint: reportDefinitionFingerprint(descriptor),
1302
+ binding,
1303
+ request: normalized,
1304
+ queryFingerprint: createDataQueryFingerprint(normalized, descriptor.schema),
1305
+ projection: [...expectedProjection],
1306
+ sort: expectedSort.map(({ field, direction }) => ({
1307
+ field,
1308
+ direction
1309
+ })),
1310
+ freshness: {
1311
+ state: freshness.state,
1312
+ asOf: freshnessAsOf
1313
+ },
1314
+ snapshot: {
1315
+ asOf,
1316
+ stale: materialization.stale,
1317
+ ...refreshedAt === void 0 ? {} : { refreshedAt }
1318
+ },
1319
+ total: {
1320
+ kind: "exact",
1321
+ value: totalValue
1322
+ },
1323
+ inherits: [...INHERITED_REPORT_SCOPE]
1324
+ };
1325
+ }
1326
+ function normalizeExportLimits(value) {
1327
+ if (value === void 0) return { ...DEFAULT_EXPORT_LIMITS };
1328
+ const input = plainObject(value, "Report export limits");
1329
+ const maxRows = boundedInteger(input.maxRows ?? DEFAULT_EXPORT_LIMITS.maxRows, "Report export limits.maxRows", 1, MAX_EXPORT_LIMITS.maxRows);
1330
+ return {
1331
+ maxRows,
1332
+ maxBytes: boundedInteger(input.maxBytes ?? DEFAULT_EXPORT_LIMITS.maxBytes, "Report export limits.maxBytes", 1, MAX_EXPORT_LIMITS.maxBytes),
1333
+ deadlineMs: boundedInteger(input.deadlineMs ?? DEFAULT_EXPORT_LIMITS.deadlineMs, "Report export limits.deadlineMs", 1, MAX_EXPORT_LIMITS.deadlineMs),
1334
+ foregroundRowLimit: boundedInteger(input.foregroundRowLimit ?? Math.min(DEFAULT_EXPORT_LIMITS.foregroundRowLimit, maxRows), "Report export limits.foregroundRowLimit", 1, Math.min(maxRows, MAX_EXPORT_LIMITS.foregroundRowLimit))
1335
+ };
1336
+ }
1337
+ function hasSensitiveProjection(descriptor, snapshot) {
1338
+ const columns = columnMap(descriptor);
1339
+ return snapshot.projection.some((id) => {
1340
+ const sensitivity = columns.get(id)?.sensitivity;
1341
+ return sensitivity === "personal" || sensitivity === "sensitive" || sensitivity === "secret";
1342
+ });
1343
+ }
1344
+ function createReadPlan(descriptor, snapshot, limits) {
1345
+ return {
1346
+ snapshotId: snapshot.binding.id,
1347
+ queryFingerprint: snapshot.queryFingerprint,
1348
+ asOf: snapshot.snapshot.asOf,
1349
+ page: {
1350
+ kind: "offset",
1351
+ offset: 0,
1352
+ limit: Math.min(limits.maxRows, descriptor.schema.maxPageLimit ?? DEFAULT_EXPORT_LIMITS.foregroundRowLimit)
1353
+ }
1354
+ };
1355
+ }
1356
+ function createReportExportRequest(descriptor, snapshot, value) {
1357
+ const input = plainObject(value, "Report export request");
1358
+ const verified = verifyReportExportSnapshot(descriptor, snapshot);
1359
+ const format = requiredString(input.format, "Report export format", 16);
1360
+ if (!EXPORT_FORMATS.has(format)) return fail("Report export format is unsupported");
1361
+ const limits = normalizeExportLimits(input.limits);
1362
+ const rowCount = Math.min(verified.total.value, limits.maxRows);
1363
+ const confirmationRequired = hasSensitiveProjection(descriptor, verified);
1364
+ return {
1365
+ version: 1,
1366
+ format,
1367
+ execution: verified.total.value > limits.foregroundRowLimit ? "background" : "stream",
1368
+ snapshot: verified,
1369
+ limits,
1370
+ read: createReadPlan(descriptor, verified, limits),
1371
+ rowCount,
1372
+ truncated: verified.total.value > limits.maxRows,
1373
+ action: {
1374
+ id: "export",
1375
+ requiredPermission: "reports.export",
1376
+ auditRequired: true,
1377
+ confirmationRequired
1378
+ }
1379
+ };
1380
+ }
1381
+ function validateReportExportRequest(descriptor, request) {
1382
+ const expected = createReportExportRequest(descriptor, request.snapshot, {
1383
+ format: request.format,
1384
+ limits: request.limits
1385
+ });
1386
+ if (stable(request) !== stable(expected)) return fail("Report export request does not match current policy");
1387
+ return expected;
1388
+ }
1389
+ function createReportExportPageRequest(descriptor, request, offset) {
1390
+ const verified = validateReportExportRequest(descriptor, request);
1391
+ if (!Number.isSafeInteger(offset) || offset < 0 || offset >= verified.rowCount) return fail("Report export page offset is outside the bounded export");
1392
+ const limit = Math.min(verified.read.page.limit, verified.rowCount - offset);
1393
+ return normalizeDataQueryRequest({
1394
+ ...verified.snapshot.request,
1395
+ page: {
1396
+ kind: "offset",
1397
+ offset,
1398
+ limit
1399
+ }
1400
+ }, descriptor.schema);
1401
+ }
1402
+ function actionContext(phase, request) {
1403
+ return {
1404
+ phase,
1405
+ reportClassName: request.snapshot.reportClassName,
1406
+ resourceId: request.snapshot.resourceId,
1407
+ requiredPermission: request.action.requiredPermission,
1408
+ confirmationRequired: request.action.confirmationRequired,
1409
+ execution: request.execution,
1410
+ queryFingerprint: request.snapshot.queryFingerprint,
1411
+ definitionFingerprint: request.snapshot.definitionFingerprint,
1412
+ snapshotId: request.read.snapshotId,
1413
+ asOf: request.read.asOf,
1414
+ pageSize: request.read.page.limit
1415
+ };
1416
+ }
1417
+ function snapshotContext(request) {
1418
+ return {
1419
+ bindingId: request.snapshot.binding.id,
1420
+ resourceId: request.snapshot.resourceId,
1421
+ reportClassName: request.snapshot.reportClassName,
1422
+ definitionFingerprint: request.snapshot.definitionFingerprint,
1423
+ queryFingerprint: request.snapshot.queryFingerprint,
1424
+ asOf: request.snapshot.snapshot.asOf
1425
+ };
1426
+ }
1427
+ async function validateReportExportExecution(descriptor, request, host) {
1428
+ const verified = validateReportExportRequest(descriptor, request);
1429
+ await host.assertSnapshot(snapshotContext(verified));
1430
+ return verified;
1431
+ }
1432
+ async function previewReportExport(descriptor, request, host) {
1433
+ const initial = validateReportExportRequest(descriptor, request);
1434
+ const action = actionContext("preview", initial);
1435
+ await host.authorize(action);
1436
+ const verified = await validateReportExportExecution(descriptor, initial, host);
1437
+ await host.audit(action);
1438
+ return {
1439
+ phase: "preview",
1440
+ action,
1441
+ request: verified
1442
+ };
1443
+ }
1444
+ async function applyReportExport(descriptor, request, host, options = {}) {
1445
+ const initial = validateReportExportRequest(descriptor, request);
1446
+ if (initial.action.confirmationRequired && options.confirmed !== true) return fail("Sensitive report exports require explicit confirmation");
1447
+ const action = actionContext("apply", initial);
1448
+ await host.authorize(action);
1449
+ const verified = await validateReportExportExecution(descriptor, initial, host);
1450
+ await host.audit(action);
1451
+ if (verified.execution === "stream") return {
1452
+ phase: "apply",
1453
+ execution: "stream",
1454
+ status: "ready",
1455
+ request: verified
1456
+ };
1457
+ if (!host.enqueue) return fail("Background report exports require an application queue host");
1458
+ const queued = await host.enqueue({
1459
+ version: 1,
1460
+ execution: "background",
1461
+ request: verified,
1462
+ inherits: [...INHERITED_REPORT_SCOPE]
1463
+ });
1464
+ if (typeof queued.taskId !== "string" || queued.taskId.trim().length === 0) return fail("Background report export hosts must return a task id");
1465
+ return {
1466
+ phase: "apply",
1467
+ execution: "background",
1468
+ status: "queued",
1469
+ taskId: queued.taskId,
1470
+ request: verified
1471
+ };
1472
+ }
1473
+ function validateReportExportArtifact(descriptor, artifact, now = /* @__PURE__ */ new Date()) {
1474
+ exactKeys(plainObject(artifact, "Report export artifact"), [
1475
+ "id",
1476
+ "request",
1477
+ "progress",
1478
+ "expiresAt"
1479
+ ], "Report export artifact");
1480
+ const request = validateReportExportRequest(descriptor, artifact.request);
1481
+ const id = requiredString(artifact.id, "Report export artifact id");
1482
+ const expiresAt = normalizeIso(artifact.expiresAt, "Report export artifact expiresAt");
1483
+ if (new Date(expiresAt).getTime() <= now.getTime()) return fail("Report export artifact has expired");
1484
+ const progress = plainObject(artifact.progress, "Report export artifact progress");
1485
+ exactKeys(progress, [
1486
+ "state",
1487
+ "rowCount",
1488
+ "byteCount",
1489
+ "truncated"
1490
+ ], "Report export artifact progress");
1491
+ const state = progress.state;
1492
+ if (state !== "queued" && state !== "running" && state !== "completed" && state !== "failed") return fail("Report export artifact progress state is invalid");
1493
+ const rowCount = boundedInteger(progress.rowCount, "Report export artifact rowCount", 0, request.rowCount);
1494
+ const byteCount = boundedInteger(progress.byteCount, "Report export artifact byteCount", 0, request.limits.maxBytes);
1495
+ const truncated = progress.truncated;
1496
+ if (typeof truncated !== "boolean") return fail("Report export artifact truncated must be a boolean");
1497
+ if (state === "completed" && rowCount !== request.rowCount) return fail("Completed report export artifacts must contain the requested row count");
1498
+ if (state === "completed" && truncated !== request.truncated) return fail("Completed report export artifacts must match the requested truncation state");
1499
+ return {
1500
+ id,
1501
+ request,
1502
+ expiresAt,
1503
+ progress: {
1504
+ state,
1505
+ rowCount,
1506
+ byteCount,
1507
+ truncated
1508
+ }
1509
+ };
1510
+ }
1511
+ //#endregion
1512
+ export { REPORT_LOCKS_TABLE, REPORT_REFRESH_TASKS_TABLE, REPORT_RUNS_TABLE, REPORT_RUNTIME_TABLES, REPORT_SCHEDULER_TABLES, REPORT_SCHEDULES_TABLE, REPORT_WATERMARKS_TABLE, ReportScheduleRunner, ReportSurfaceValidationError, SmrtReport, SmrtReportCollection, SmrtReportLock, SmrtReportLockCollection, SmrtReportRefreshTask, SmrtReportRun, SmrtReportRunCollection, SmrtReportSchedule, SmrtReportScheduleCollection, SmrtReportWatermark, SmrtReportWatermarkCollection, aggregate, applyReportExport, applyReportRefresh, assertReportTablesReady, avg, bucketExpr, buildAggregate, buildReportAdapterDescriptor, buildReportDefinition, buildReportDrilldownQuery, compileReportDefinition, compileReportSpec, count, createReportExportPageRequest, createReportExportRequest, createReportExportSnapshot, day, enqueueReportRefresh, ensureReportRefreshSchedules, getReportGroupingColumns, getReportLifecycle, getRuntimeReportOptions, groupBy, hour, max, migrateReportSavedView, min, minute, month, normalizeReportSavedView, previewReportExport, previewReportRefresh, quarter, queryReportMaterializedRows, refreshReport, registerReportRefreshInterceptor, report, reportDefinitionFingerprint, reportMaterializedRowKey, reportRefreshOutcome, reportRowIdentity, restoreReportSavedView, scopeKeyForTenant, splitReportFilterScopes, sum, validateReportExportArtifact, validateReportExportExecution, validateReportExportRequest, verifyReportExportSnapshot, week, year };
992
1513
 
993
1514
  //# sourceMappingURL=index.js.map