@happyvertical/smrt-reports 0.42.6 → 0.42.7
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/AGENTS.md +33 -0
- package/README.md +120 -0
- package/dist/index.d.ts +483 -4
- package/dist/index.js +908 -5
- package/dist/index.js.map +1 -1
- package/dist/manifest.json +1 -1
- package/dist/refresh.js +2 -2
- package/dist/scheduler.js +1 -1
- package/dist/smrt-knowledge.json +7 -5
- package/package.json +6 -5
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../src/report.ts"],"sourcesContent":["import {\n ObjectRegistry,\n SmrtCollection,\n SmrtObject,\n type SmrtObjectOptions,\n} from '@happyvertical/smrt-core';\nimport { toSnakeCase } from '@happyvertical/smrt-core/utils';\nimport { getTenantId } from '@happyvertical/smrt-tenancy';\nimport { validateColumnName } from '@happyvertical/sql';\nimport { buildReportDefinition } from './compiler.js';\nimport { refreshReport } from './refresh.js';\nimport type { ReportRefreshOptions, ReportRefreshResult } from './types.js';\n\ntype RegistryField = {\n columnName?: string;\n _meta?: {\n columnName?: string;\n __tenancy?: { isTenantIdField?: boolean };\n };\n};\n\nfunction registryColumnName(fieldName: string, field?: RegistryField): string {\n return validateColumnName(\n field?.columnName ?? field?._meta?.columnName ?? toSnakeCase(fieldName),\n );\n}\n\nfunction findFieldColumn(\n fields: Map<string, RegistryField>,\n fieldName: string,\n): string {\n const direct = fields.get(fieldName);\n if (direct) return registryColumnName(fieldName, direct);\n const requestedColumn = toSnakeCase(fieldName);\n for (const [name, field] of fields.entries()) {\n if (toSnakeCase(name) === requestedColumn) {\n return registryColumnName(name, field);\n }\n }\n return validateColumnName(requestedColumn);\n}\n\nfunction findTenantColumn(\n fields: Map<string, RegistryField>,\n configuredField?: string,\n): string | null {\n if (configuredField) {\n return registryColumnName(configuredField, fields.get(configuredField));\n }\n for (const [fieldName, field] of fields.entries()) {\n if (fieldName === 'tenantId' || field?._meta?.__tenancy?.isTenantIdField) {\n return registryColumnName(fieldName, field);\n }\n }\n return null;\n}\n\nexport class SmrtReport extends SmrtObject {\n static readonly _isReportBase = true as const;\n\n refreshedAt: Date | null = null;\n\n constructor(options: SmrtObjectOptions = {}) {\n super(options);\n if (options.refreshedAt !== undefined) {\n this.refreshedAt =\n options.refreshedAt instanceof Date\n ? options.refreshedAt\n : options.refreshedAt\n ? new Date(options.refreshedAt)\n : null;\n }\n }\n\n isStale(ttlMs?: number): boolean {\n if (!this.refreshedAt) return true;\n if (ttlMs === undefined) return false;\n return Date.now() - this.refreshedAt.getTime() > ttlMs;\n }\n\n async refresh(\n options: Omit<ReportRefreshOptions, 'db'> = {},\n ): Promise<ReportRefreshResult> {\n const result = await refreshReport(this.constructor as typeof SmrtReport, {\n ...options,\n db: this.db,\n });\n this.refreshedAt = result.refreshedAt;\n return result;\n }\n}\n\nexport class SmrtReportCollection<\n ModelType extends SmrtReport,\n> extends SmrtCollection<ModelType> {\n private async refreshIfStale(): Promise<void> {\n const reportCtor = this.getItemClass();\n const definition = await buildReportDefinition(reportCtor);\n const refresh = definition.refresh;\n if (!refresh?.ttl || refresh.manual) return;\n\n const registered =\n ObjectRegistry.getClassByConstructor(reportCtor) ??\n ObjectRegistry.getClass(reportCtor.name);\n const reportClass =\n registered?.qualifiedName ?? registered?.name ?? reportCtor.name;\n const tableName = ObjectRegistry.getTableName(reportClass);\n if (!tableName) return;\n\n const fields = (await ObjectRegistry.getAllFields(reportClass)) as Map<\n string,\n RegistryField\n >;\n const safeTableName = validateColumnName(tableName);\n const refreshedAtColumn = findFieldColumn(fields, 'refreshedAt');\n const tenantColumn = findTenantColumn(\n fields,\n registered?.tenantScopedConfig?.field,\n );\n const tenantId = getTenantId() ?? null;\n const result = tenantColumn\n ? await this.db.query(\n `SELECT MAX(${refreshedAtColumn}) AS refreshed_at FROM ${safeTableName} WHERE ${tenantColumn} ${tenantId ? '= ?' : 'IS NULL'}`,\n ...(tenantId ? [tenantId] : []),\n )\n : await this.db.query(\n `SELECT MAX(${refreshedAtColumn}) AS refreshed_at FROM ${safeTableName}`,\n );\n const refreshedAt = result.rows[0]?.refreshed_at\n ? new Date(result.rows[0].refreshed_at as string)\n : null;\n const stale =\n !refreshedAt || Date.now() - refreshedAt.getTime() > refresh.ttl;\n if (!stale) return;\n\n await refreshReport(reportCtor, {\n db: this.db,\n mode: refresh.mode ?? 'rebuild',\n trigger: 'ttl',\n tenantId,\n });\n }\n\n async refresh(\n options: Omit<ReportRefreshOptions, 'db'> = {},\n ): Promise<ReportRefreshResult> {\n return refreshReport(this.getItemClass(), {\n ...options,\n db: this.db,\n });\n }\n\n override async list(\n options: Parameters<SmrtCollection<ModelType>['list']>[0] = {},\n ): Promise<ModelType[]> {\n await this.refreshIfStale();\n return super.list(options);\n }\n\n override async get(\n filter: Parameters<SmrtCollection<ModelType>['get']>[0],\n options: Parameters<SmrtCollection<ModelType>['get']>[1] = {},\n ): Promise<ModelType | null> {\n await this.refreshIfStale();\n return super.get(filter, options);\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":["suffix","children","filter"],"sources":["../src/lifecycle.ts","../src/adapter.ts","../src/report.ts"],"sourcesContent":["import { ObjectRegistry, type SmrtObject } from '@happyvertical/smrt-core';\nimport { getTenantId, withSystemContext } from '@happyvertical/smrt-tenancy';\nimport {\n type DatabaseInterface,\n tableExists,\n validateColumnName,\n} from '@happyvertical/sql';\nimport type { ReportRefreshActionDescriptor } from './adapter.js';\nimport { buildReportDefinition } from './compiler.js';\nimport { enqueueReportRefresh } from './scheduler.js';\nimport {\n assertReportTablesReady,\n REPORT_LOCKS_TABLE,\n REPORT_RUNS_TABLE,\n scopeKeyForTenant,\n} from './state.js';\nimport type {\n ReportRefreshMode,\n ReportRefreshResult,\n ReportRefreshTrigger,\n} from './types.js';\n\ntype ReportCtor = new (...args: any[]) => SmrtObject;\n\nfunction registeredReport(reportCtor: ReportCtor) {\n return (\n ObjectRegistry.getClassByConstructor(reportCtor) ??\n ObjectRegistry.getClass(reportCtor.name)\n );\n}\n\nexport type ReportLifecycleState =\n | 'current'\n | 'stale'\n | 'refreshing'\n | 'lock-skipped'\n | 'failed';\n\nexport interface ReportLifecycleRun {\n id: string;\n status: 'running' | 'success' | 'failed' | 'skipped';\n mode: ReportRefreshMode;\n trigger: ReportRefreshTrigger;\n startedAt?: string;\n completedAt?: string;\n rowCount: number;\n changedGroupCount: number;\n /** A failed run is safe to retry through the separately permissioned action. */\n mayRetry: boolean;\n}\n\n/**\n * Transport-neutral report lifecycle state. It deliberately never includes a\n * lock owner, a raw failure message, or tenant fanout identifiers.\n */\nexport interface ReportLifecycleSnapshot {\n version: 1;\n state: ReportLifecycleState;\n asOf?: string;\n refreshedAt?: string;\n /** Whether existing materialized rows can remain visible while work changes state. */\n hasUsableRows: boolean;\n mode: ReportRefreshMode;\n run?: ReportLifecycleRun;\n /** A stable, redacted failure signal; raw executor errors stay server-side. */\n failure?: {\n code: 'refresh_failed';\n retryable: true;\n };\n lock: {\n held: boolean;\n expiresAt?: string;\n };\n}\n\nexport interface ReportLifecycleOptions {\n db: DatabaseInterface;\n /** Test seam; production callers use the current clock. */\n now?: Date;\n}\n\nexport interface ReportRefreshJobHandle {\n jobId: string;\n status: 'pending' | 'running' | 'completed' | 'failed' | 'cancelled';\n attempts: number;\n maxAttempts: number;\n /** Bounded polling guidance; this API does not create a second queue. */\n pollAfterMs: number;\n}\n\nexport interface ReportRefreshActionContext {\n phase: 'preview' | 'apply';\n reportClassName: string;\n /** The action always applies to the caller's already-bound tenant scope. */\n tenantScope: 'ambient';\n mode: ReportRefreshMode;\n requiredPermission: string;\n}\n\n/** Authority and audit stay with the application action host, not reports. */\nexport interface ReportRefreshActionHost {\n authorize(context: ReportRefreshActionContext): Promise<void> | void;\n audit(context: ReportRefreshActionContext): Promise<void> | void;\n}\n\nexport interface PreviewReportRefreshOptions extends ReportLifecycleOptions {\n host: ReportRefreshActionHost;\n mode?: ReportRefreshMode;\n /**\n * Pass `descriptor.refresh.action` when the adapter declares a custom\n * refresh permission, so the displayed and enforced action stay aligned.\n */\n refreshAction?: Pick<ReportRefreshActionDescriptor, 'requiredPermission'>;\n}\n\nexport interface ReportRefreshPreview {\n phase: 'preview';\n lifecycle: ReportLifecycleSnapshot;\n action: ReportRefreshActionContext;\n execution: 'background';\n}\n\nexport interface ApplyReportRefreshOptions extends PreviewReportRefreshOptions {\n queue?: string;\n priority?: number;\n timeout?: number;\n maxAttempts?: number;\n tenantJobCap?: number;\n}\n\nexport interface AppliedReportRefresh {\n phase: 'apply';\n job: ReportRefreshJobHandle;\n}\n\nexport interface ReportRefreshOutcome {\n state: 'current' | 'lock-skipped' | 'partial';\n rowCount: number;\n changedGroupCount: number;\n completedScopes: number;\n lockSkippedScopes: number;\n mode: ReportRefreshMode;\n refreshedAt: string;\n runId?: string;\n}\n\nfunction canonicalClassName(reportCtor: ReportCtor): string {\n const registered = registeredReport(reportCtor);\n return registered?.qualifiedName ?? registered?.name ?? reportCtor.name;\n}\n\nfunction reportTableName(reportCtor: ReportCtor): string {\n const reportClassName = canonicalClassName(reportCtor);\n const tableName = ObjectRegistry.getTableName(reportClassName);\n if (!tableName) {\n throw new Error(`No report table registered for ${reportCtor.name}`);\n }\n return validateColumnName(tableName);\n}\n\nasync function reportColumnName(\n reportClassName: string,\n fieldName: string,\n): Promise<string> {\n const fields = await ObjectRegistry.getAllFields(reportClassName);\n const direct = fields.get(fieldName) as\n | { columnName?: string; _meta?: { columnName?: string } }\n | undefined;\n return validateColumnName(\n direct?.columnName ??\n direct?._meta?.columnName ??\n fieldName.replace(/([A-Z])/g, '_$1').toLowerCase(),\n );\n}\n\nfunction toIso(value: unknown): string | undefined {\n if (!value) return undefined;\n const date = value instanceof Date ? value : new Date(String(value));\n return Number.isNaN(date.getTime()) ? undefined : date.toISOString();\n}\n\nfunction latestIso(\n first: string | undefined,\n second: string | undefined,\n): string | undefined {\n if (!first) return second;\n if (!second) return first;\n return new Date(first).getTime() >= new Date(second).getTime()\n ? first\n : second;\n}\n\nfunction lifecycleTenantId(reportCtor: ReportCtor): string | null {\n return registeredReport(reportCtor)?.tenantScopedConfig\n ? (getTenantId() ?? null)\n : null;\n}\n\nfunction numberValue(value: unknown): number {\n const numeric = Number(value ?? 0);\n return Number.isFinite(numeric) ? numeric : 0;\n}\n\nfunction rowWhere(\n reportClassName: string,\n scopeKey: string,\n tenantId: string | null,\n): { sql: string; values: unknown[] } {\n if (tenantId) {\n return {\n sql: 'report_class = ? AND scope_key = ? AND tenant_id = ?',\n values: [reportClassName, scopeKey, tenantId],\n };\n }\n return {\n sql: 'report_class = ? AND scope_key = ? AND tenant_id IS NULL',\n values: [reportClassName, scopeKey],\n };\n}\n\nfunction runFromRow(\n row: Record<string, unknown> | undefined,\n): ReportLifecycleRun | undefined {\n if (!row || typeof row.id !== 'string') return undefined;\n const status = row.status;\n if (\n status !== 'running' &&\n status !== 'success' &&\n status !== 'failed' &&\n status !== 'skipped'\n ) {\n return undefined;\n }\n const mode: ReportRefreshMode =\n row.mode === 'incremental' ? 'incremental' : 'rebuild';\n const trigger: ReportRefreshTrigger =\n row.trigger === 'schedule' ||\n row.trigger === 'change' ||\n row.trigger === 'ttl' ||\n row.trigger === 'job'\n ? row.trigger\n : 'manual';\n return {\n id: row.id,\n status,\n mode,\n trigger,\n ...(toIso(row.started_at) ? { startedAt: toIso(row.started_at) } : {}),\n ...(toIso(row.completed_at)\n ? { completedAt: toIso(row.completed_at) }\n : {}),\n rowCount: numberValue(row.row_count),\n changedGroupCount: numberValue(row.changed_group_count),\n mayRetry: status === 'failed',\n };\n}\n\nfunction jobHandle(job: {\n id?: unknown;\n status: ReportRefreshJobHandle['status'];\n attempts: number;\n maxAttempts: number;\n}): ReportRefreshJobHandle {\n if (typeof job.id !== 'string' || job.id.length === 0) {\n throw new Error('Queued report refresh is missing its job id');\n }\n return {\n jobId: job.id,\n status: job.status,\n attempts: job.attempts,\n maxAttempts: job.maxAttempts,\n pollAfterMs: 1_000,\n };\n}\n\n/**\n * Read only the ambient tenant's report state. The caller cannot supply a\n * tenant selector, which prevents lifecycle lookup from becoming a tenant\n * enumeration surface.\n */\nexport async function getReportLifecycle(\n reportCtor: ReportCtor,\n options: ReportLifecycleOptions,\n): Promise<ReportLifecycleSnapshot> {\n await assertReportTablesReady(options.db, [\n REPORT_RUNS_TABLE,\n REPORT_LOCKS_TABLE,\n ]);\n\n const definition = await buildReportDefinition(reportCtor);\n const reportClassName = canonicalClassName(reportCtor);\n const tenantId = lifecycleTenantId(reportCtor);\n const scopeKey = scopeKeyForTenant(tenantId);\n const where = rowWhere(reportClassName, scopeKey, tenantId);\n const now = options.now ?? new Date();\n\n const [runResult, lockResult] = await Promise.all([\n options.db.query(\n `SELECT id, mode, trigger, status, started_at, completed_at, row_count, changed_group_count\n FROM ${REPORT_RUNS_TABLE}\n WHERE ${where.sql}\n ORDER BY started_at DESC, created_at DESC, id DESC\n LIMIT 1`,\n ...where.values,\n ),\n options.db.query(\n `SELECT expires_at\n FROM ${REPORT_LOCKS_TABLE}\n WHERE ${where.sql} AND expires_at > ?\n LIMIT 1`,\n ...where.values,\n now.toISOString(),\n ),\n ]);\n\n const tableName = reportTableName(reportCtor);\n if (!(await tableExists(options.db, tableName))) {\n throw new Error(\n `Report table '${tableName}' does not exist for ${reportClassName}`,\n );\n }\n const refreshedAtColumn = await reportColumnName(\n reportClassName,\n 'refreshedAt',\n );\n const registered = registeredReport(reportCtor);\n const configuredTenantField = registered?.tenantScopedConfig?.field;\n const tenantColumn = configuredTenantField\n ? await reportColumnName(reportClassName, configuredTenantField)\n : undefined;\n const materialized = tenantColumn\n ? await options.db.query(\n `SELECT MAX(${refreshedAtColumn}) AS refreshed_at FROM ${tableName} WHERE ${tenantColumn} ${tenantId ? '= ?' : 'IS NULL'}`,\n ...(tenantId ? [tenantId] : []),\n )\n : await options.db.query(\n `SELECT MAX(${refreshedAtColumn}) AS refreshed_at FROM ${tableName}`,\n );\n\n const run = runFromRow(\n runResult.rows[0] as Record<string, unknown> | undefined,\n );\n const refreshedAt = toIso(materialized.rows[0]?.refreshed_at);\n const completedAt = run?.status === 'success' ? run.completedAt : undefined;\n const asOf = latestIso(refreshedAt, completedAt);\n const ttlMs = definition.refresh?.ttl;\n const isStale =\n !asOf ||\n (ttlMs !== undefined && now.getTime() - new Date(asOf).getTime() > ttlMs);\n const lockExpiresAt = toIso(lockResult.rows[0]?.expires_at);\n const lockHeld = Boolean(lockExpiresAt);\n const state: ReportLifecycleState =\n run?.status === 'skipped'\n ? 'lock-skipped'\n : lockHeld\n ? 'refreshing'\n : run?.status === 'failed'\n ? 'failed'\n : run?.status === 'running'\n ? 'stale'\n : isStale\n ? 'stale'\n : 'current';\n\n return {\n version: 1,\n state,\n ...(asOf ? { asOf } : {}),\n ...(refreshedAt ? { refreshedAt } : {}),\n hasUsableRows: Boolean(refreshedAt),\n mode: run?.mode ?? definition.refresh?.mode ?? 'rebuild',\n ...(run ? { run } : {}),\n ...(run?.status === 'failed'\n ? { failure: { code: 'refresh_failed' as const, retryable: true } }\n : {}),\n lock: {\n held: lockHeld,\n ...(lockExpiresAt ? { expiresAt: lockExpiresAt } : {}),\n },\n };\n}\n\nfunction actionContext(\n reportCtor: ReportCtor,\n phase: ReportRefreshActionContext['phase'],\n mode: ReportRefreshMode,\n refreshAction:\n | Pick<ReportRefreshActionDescriptor, 'requiredPermission'>\n | undefined,\n): ReportRefreshActionContext {\n return {\n phase,\n reportClassName: canonicalClassName(reportCtor),\n tenantScope: 'ambient',\n mode,\n requiredPermission:\n refreshAction?.requiredPermission &&\n refreshAction.requiredPermission.trim().length > 0\n ? refreshAction.requiredPermission\n : 'reports.refresh',\n };\n}\n\nasync function actionMode(\n reportCtor: ReportCtor,\n requested: ReportRefreshMode | undefined,\n): Promise<ReportRefreshMode> {\n if (requested) return requested;\n const definition = await buildReportDefinition(reportCtor);\n return definition.refresh?.mode ?? 'rebuild';\n}\n\n/** Preview only declares a separately authorized and audited refresh request. */\nexport async function previewReportRefresh(\n reportCtor: ReportCtor,\n options: PreviewReportRefreshOptions,\n): Promise<ReportRefreshPreview> {\n const mode = await actionMode(reportCtor, options.mode);\n const action = actionContext(\n reportCtor,\n 'preview',\n mode,\n options.refreshAction,\n );\n await options.host.authorize(action);\n await options.host.audit(action);\n return {\n phase: 'preview',\n lifecycle: await getReportLifecycle(reportCtor, options),\n action,\n execution: 'background',\n };\n}\n\n/** Queue a manual refresh only after the action host authorizes and audits it. */\nexport async function applyReportRefresh(\n reportCtor: ReportCtor,\n options: ApplyReportRefreshOptions,\n): Promise<AppliedReportRefresh> {\n const mode = await actionMode(reportCtor, options.mode);\n const action = actionContext(\n reportCtor,\n 'apply',\n mode,\n options.refreshAction,\n );\n await options.host.authorize(action);\n await options.host.audit(action);\n const tenantId = lifecycleTenantId(reportCtor);\n const enqueue = () =>\n enqueueReportRefresh({\n db: options.db,\n reportClass: canonicalClassName(reportCtor),\n mode,\n trigger: 'manual',\n tenantId,\n queue: options.queue,\n priority: options.priority,\n timeout: options.timeout,\n maxAttempts: options.maxAttempts,\n tenantJobCap: options.tenantJobCap,\n });\n const job =\n tenantId === null && getTenantId()\n ? await withSystemContext(enqueue)\n : await enqueue();\n return { phase: 'apply', job: jobHandle(job) };\n}\n\n/** Normalize executor results without exposing the tenant identifiers in a fanout. */\nexport function reportRefreshOutcome(\n result: ReportRefreshResult,\n): ReportRefreshOutcome {\n const scopes = result.tenantResults ?? [result];\n const lockSkippedScopes = scopes.filter((scope) => scope.skipped).length;\n const completedScopes = scopes.length - lockSkippedScopes;\n return {\n state:\n scopes.length > 1 && lockSkippedScopes > 0\n ? 'partial'\n : lockSkippedScopes > 0\n ? 'lock-skipped'\n : 'current',\n rowCount: result.rowCount,\n changedGroupCount: result.changedGroupCount ?? 0,\n completedScopes,\n lockSkippedScopes,\n mode: result.mode,\n refreshedAt: result.refreshedAt.toISOString(),\n ...(result.runId ? { runId: result.runId } : {}),\n };\n}\n","import {\n createDataQueryFingerprint,\n DataQueryValidationError,\n normalizeDataQueryRequest,\n normalizeDataQueryResult,\n ObjectRegistry,\n type SmrtObject,\n} from '@happyvertical/smrt-core';\nimport { toSnakeCase } from '@happyvertical/smrt-core/utils';\nimport type {\n DataQueryCondition,\n DataQueryFieldDescriptor,\n DataQueryFilter,\n DataQueryFilterOperator,\n DataQueryFreshness,\n DataQueryRequest,\n DataQueryResult,\n DataQuerySchema,\n} from '@happyvertical/smrt-types';\nimport { buildReportDefinition } from './compiler.js';\nimport {\n getReportLifecycle,\n type ReportLifecycleOptions,\n type ReportLifecycleSnapshot,\n} from './lifecycle.js';\nimport type {\n ReportAggregateFn,\n ReportDefinition,\n ReportFieldDefinition,\n ReportRefreshConfig,\n ReportTimeBucketUnit,\n} from './types.js';\n\n/** Presentation and policy metadata understood by the report adapter. */\nexport type ReportColumnSensitivity =\n | 'public'\n | 'personal'\n | 'sensitive'\n | 'secret';\n\nexport type ReportColumnCapability =\n | 'read'\n | 'project'\n | 'filter'\n | 'sort'\n | 'facet'\n | 'group'\n | 'aggregate';\n\nexport type ReportColumnKind = 'identity' | 'group' | 'bucket' | 'aggregate';\n\n/**\n * Source-query clause a field belongs to. The materialized read executor\n * applies both kinds through its tenant-scoped collection, but consumers use\n * this declaration to keep dimensions/periods (WHERE) distinct from measures\n * (HAVING) when constructing drilldowns, saved views, or a live query.\n */\nexport type ReportFilterScope = 'where' | 'having';\n\nexport interface ReportColumnDescriptor extends DataQueryFieldDescriptor {\n /** Stable output column id (never a property path). */\n id: string;\n fieldName: string;\n label: string;\n kind: ReportColumnKind;\n filterScope: ReportFilterScope;\n /** Capabilities available from this adapter revision. */\n capabilities: ReportColumnCapability[];\n format?: string;\n sensitivity?: ReportColumnSensitivity;\n sourceColumn?: string;\n bucket?: ReportTimeBucketUnit;\n aggregate?: ReportAggregateFn;\n distinct?: boolean;\n}\n\nexport interface ReportRefreshDescriptor {\n mode: 'rebuild' | 'incremental';\n /**\n * Whether the report configuration lets a registered SmrtReportCollection\n * synchronously refresh a stale read. Generic collection reads stay\n * read-only and report unknown freshness until the lifecycle adapter runs.\n */\n mayRefreshOnRead: boolean;\n ttlMs?: number;\n triggers: Array<'manual' | 'schedule' | 'change' | 'ttl' | 'job'>;\n /** Declared only; the lifecycle adapter owns authorization, audit, and run state. */\n action: ReportRefreshActionDescriptor;\n}\n\n/** A report-wide lifecycle action, never a mutation performed by this adapter. */\nexport interface ReportRefreshActionDescriptor {\n id: 'refresh';\n label: 'Refresh report';\n scope: 'surface';\n phases: Array<'preview' | 'apply'>;\n requiresPermission: true;\n requiredPermission: string;\n auditRequired: true;\n}\n\n/**\n * The report-owned, transport-neutral descriptor. Consumers can map `schema`\n * to the canonical data-query runtime and `columns` to their presentation\n * contract without importing smrt-ui or a report domain class.\n */\nexport interface ReportAdapterDescriptor {\n version: 1;\n resourceId: string;\n reportClassName: string;\n sourceClassName: string;\n tenantScoped: boolean;\n tenantField?: string;\n identityField: 'id';\n columns: ReportColumnDescriptor[];\n schema: DataQuerySchema;\n queryExecution: ReportQueryExecutionDescriptor;\n dataTable: ReportDataTableDescriptor;\n drilldown: ReportDrilldownDescriptor;\n refresh: ReportRefreshDescriptor;\n}\n\n/**\n * Delivery choices for the same bounded report query. They never add\n * authority: a host owns visible state and background-job execution.\n */\nexport type ReportQueryExecutionMode = 'visible' | 'background' | 'silent';\n\nexport interface ReportQueryExecutionDescriptor {\n modes: ReportQueryExecutionMode[];\n /** The caller may apply the returned rows to an already-authorized surface. */\n visible: { delivery: 'result' };\n /** A host queues the authority-free task and returns no materialized rows yet. */\n background: { delivery: 'queued'; requiresHost: true };\n /** The caller receives rows but the adapter makes no visible-surface change. */\n silent: { delivery: 'result'; mutatesVisibleSurface: false };\n}\n\n/**\n * The exact bounded request a background host may persist. Tenant, principal,\n * collection, database, and display state deliberately remain with that host.\n */\nexport interface ReportBackgroundQueryTask {\n version: 1;\n execution: 'background';\n resourceId: string;\n reportClassName: string;\n request: DataQueryRequest;\n inherits: Array<\n 'principal' | 'tenant' | 'report-definition' | 'field-policy'\n >;\n}\n\n/** Queuing a background query returns a handle, never rows from another scope. */\nexport interface ReportBackgroundQueryResult {\n version: 1;\n execution: 'background';\n status: 'queued';\n taskId: string;\n queryFingerprint: string;\n}\n\n/** One row value that can safely constrain a source-record drilldown. */\nexport interface ReportDrilldownFieldDescriptor {\n /** Stable report column id; never a display label. */\n id: string;\n /** Declared source field/column chosen by report metadata, not caller input. */\n sourceColumn: string;\n kind: 'group' | 'bucket';\n /** A bucket stays declarative so the source adapter preserves report timezone semantics. */\n bucket?: ReportTimeBucketUnit;\n}\n\n/**\n * Declarative source-query handoff for a report row. No client can supply a\n * principal, tenant, report definition, or arbitrary source field here.\n */\nexport interface ReportDrilldownDescriptor {\n id: 'drilldown';\n sourceClassName: string;\n fields: ReportDrilldownFieldDescriptor[];\n inherits: Array<\n 'principal' | 'tenant' | 'report-definition' | 'field-policy'\n >;\n}\n\nexport interface ReportDrilldownConstraint {\n id: string;\n sourceColumn: string;\n kind: 'group' | 'bucket';\n value: unknown;\n bucket?: ReportTimeBucketUnit;\n}\n\n/** A server adapter uses this authority-free handoff to execute a source drilldown. */\nexport interface ReportDrilldownQuery {\n version: 1;\n resourceId: string;\n reportClassName: string;\n sourceClassName: string;\n constraints: ReportDrilldownConstraint[];\n inherits: Array<\n 'principal' | 'tenant' | 'report-definition' | 'field-policy'\n >;\n}\n\n/** Structural DataTable view hints; deliberately not a smrt-ui dependency. */\nexport interface ReportDataTableHeaderPathSegment {\n /** Stable group identity within a header level. */\n id: string;\n /** Human-readable grouped-header label. */\n label: string;\n}\n\n/** Responsive metadata that maps directly to a consumer's table contract. */\nexport interface ReportDataTableColumnResponsive {\n /** Higher values make a column more important during responsive collapse. */\n priority?: number;\n /** Keep key dimensions reachable during responsive collapse. */\n keepVisible?: boolean;\n}\n\n/**\n * A rendering instruction for a raw materialized value. Formatting is never\n * applied to a query row, so sorting, export, and agent consumers retain it.\n */\nexport type ReportDataTableValueFormat =\n | 'text'\n | 'date'\n | 'datetime'\n | 'percentage'\n | 'count'\n | 'money'\n | 'number';\n\nexport type ReportDataTableColumnRole = 'data' | 'status' | 'action';\n\nexport interface ReportDataTableColumn {\n id: string;\n label: string;\n accessor: string;\n sortable: boolean;\n searchable: false;\n filterable: boolean;\n /** Group ancestry for consumers with multi-level table headers. */\n headerPath: ReportDataTableHeaderPathSegment[];\n /** Consumer-side display instruction; materialized rows remain raw. */\n valueFormat: ReportDataTableValueFormat;\n /** The default alignment for the formatted display value. */\n align: 'left' | 'right';\n /** Generic semantic role for status/action columns introduced by later slices. */\n role: ReportDataTableColumnRole;\n responsive: ReportDataTableColumnResponsive;\n}\n\nexport type ReportDataTableStructuralRowKind =\n | 'summary'\n | 'subtotal'\n | 'aggregate'\n | 'footer';\n\n/** Input supplied by a report consumer that computes a summary or subtotal. */\nexport interface ReportDataTableStructuralRowInput {\n id: string;\n kind: ReportDataTableStructuralRowKind;\n label: string;\n /** Raw values keyed by the adapter's stable column id. */\n values?: Readonly<Record<string, unknown>>;\n /** Column that renders this row's accessible row header. */\n labelColumnId?: string;\n}\n\n/**\n * Structural rows are intentionally separate from materialized data rows.\n * Consumers pass them to their DataTable's structural-row slot/prop, never its\n * selectable data collection.\n */\nexport interface ReportDataTableStructuralRow\n extends ReportDataTableStructuralRowInput {\n selection: 'excluded';\n actions: 'excluded';\n}\n\n/** Per-column presentation overrides owned by the consuming report surface. */\nexport interface ReportDataTableColumnOverride {\n label?: string;\n headerPath?: readonly ReportDataTableHeaderPathSegment[];\n valueFormat?: ReportDataTableValueFormat;\n align?: 'left' | 'right';\n role?: ReportDataTableColumnRole;\n responsive?: ReportDataTableColumnResponsive;\n}\n\nexport interface ReportDataTablePresentationOptions {\n /** Overrides are keyed by stable adapter column id, not display labels. */\n columns?: Readonly<Record<string, ReportDataTableColumnOverride>>;\n /** Summary/subtotal rows remain structural rather than materialized data rows. */\n structuralRows?: readonly ReportDataTableStructuralRowInput[];\n}\n\nexport interface ReportDataTableDescriptor {\n rowKey: 'id';\n manualPagination: true;\n manualSorting: true;\n enableFiltering: true;\n enableSearch: false;\n columns: ReportDataTableColumn[];\n structuralRows: ReportDataTableStructuralRow[];\n}\n\nexport interface ReportAdapterOptions {\n /** Scope used only to make a stable resource id; it is not authority. */\n tenantScope?: 'current' | 'global' | 'tenant';\n /** Permission that a lifecycle action host must require before refresh. */\n refreshPermission?: string;\n /** Consumer-owned presentational preferences; they never affect query rows. */\n dataTable?: ReportDataTablePresentationOptions;\n}\n\ninterface ReportQueryCommonOptions {\n /** Injected collection seam for tests and application-owned adapters. */\n collection?: {\n list(\n options: Record<string, unknown>,\n ): Promise<Array<Record<string, unknown>>>;\n count(options?: Record<string, unknown>): Promise<number>;\n facets?(options: Record<string, unknown>): Promise<\n Array<{\n field: string;\n values: Array<{\n value: string | number | boolean | null;\n count: number;\n }>;\n }>\n >;\n };\n db?: import('@happyvertical/sql').DatabaseInterface;\n /**\n * Opt in to a tenant-safe lifecycle snapshot alongside this materialized\n * read. It never makes a generic read mutate.\n */\n lifecycle?: Omit<ReportLifecycleOptions, 'db'>;\n}\n\n/** Visible is the default; silent has identical data authority but no UI intent. */\nexport interface ReportQueryOptions extends ReportQueryCommonOptions {\n execution?: 'visible' | 'silent';\n}\n\n/** Background execution is possible only through an application-owned queue host. */\nexport interface ReportBackgroundQueryOptions extends ReportQueryCommonOptions {\n execution: 'background';\n enqueueBackgroundQuery(\n task: ReportBackgroundQueryTask,\n ): Promise<{ taskId: string }>;\n}\n\n/** Lifecycle context attached only when a caller explicitly opts in. */\nexport interface ReportMaterializedReadLifecycle {\n snapshot: ReportLifecycleSnapshot;\n /** Whether this collection read appears to have completed a TTL refresh. */\n read: 'current' | 'stale' | 'refresh-triggered';\n}\n\nexport interface ReportDataQueryResult extends DataQueryResult {\n /** Delivery intent only; the adapter never mutates a visible surface. */\n execution: 'visible' | 'silent';\n reportLifecycle?: ReportMaterializedReadLifecycle;\n}\n\ntype RegistryField = {\n type?: string;\n transient?: boolean;\n description?: string;\n sensitive?: boolean;\n readPermission?: string;\n format?: unknown;\n sensitivity?: unknown;\n _meta?: Record<string, unknown>;\n};\nconst SENSITIVITIES = new Set<ReportColumnSensitivity>([\n 'public',\n 'personal',\n 'sensitive',\n 'secret',\n]);\n\nfunction registeredClass(\n ctor: new (...args: any[]) => SmrtObject,\n): ReturnType<typeof ObjectRegistry.getClassByConstructor> {\n return (\n ObjectRegistry.getClassByConstructor(ctor) ??\n ObjectRegistry.getClass(ctor.name)\n );\n}\n\nfunction fieldPolicy(field: RegistryField | undefined): {\n sensitive: boolean;\n readPermission?: string;\n} {\n const meta = field?._meta ?? {};\n const sensitivity = [field?.sensitivity, meta.sensitivity].find(\n (value): value is string => typeof value === 'string',\n );\n return {\n sensitive:\n field?.sensitive === true ||\n meta.sensitive === true ||\n sensitivity === 'sensitive' ||\n sensitivity === 'secret',\n readPermission:\n typeof field?.readPermission === 'string'\n ? field.readPermission\n : typeof meta.readPermission === 'string'\n ? meta.readPermission\n : undefined,\n };\n}\n\nfunction descriptorMetadata(field: RegistryField | undefined): {\n format?: string;\n sensitivity?: ReportColumnSensitivity;\n} {\n const metadata = field?._meta ?? {};\n const format = [field?.format, metadata.format].find(\n (value): value is string => typeof value === 'string' && value.length > 0,\n );\n const rawSensitivity = [field?.sensitivity, metadata.sensitivity].find(\n (value): value is string =>\n typeof value === 'string' &&\n SENSITIVITIES.has(value as ReportColumnSensitivity),\n );\n return {\n ...(format ? { format } : {}),\n ...(rawSensitivity\n ? { sensitivity: rawSensitivity as ReportColumnSensitivity }\n : {}),\n };\n}\n\nfunction labelFor(fieldName: string): string {\n return fieldName\n .replace(/([a-z\\d])([A-Z])/g, '$1 $2')\n .replace(/[_-]+/g, ' ')\n .replace(/^./, (value) => value.toUpperCase());\n}\n\nfunction queryType(type: string | undefined): DataQueryFieldDescriptor['type'] {\n switch (type) {\n case 'integer':\n case 'decimal':\n return 'number';\n case 'boolean':\n return 'boolean';\n case 'datetime':\n return 'datetime';\n case 'json':\n return 'json';\n default:\n return 'string';\n }\n}\n\nfunction configuredTenantField(configured?: string): string | undefined {\n return configured ? toSnakeCase(configured) : undefined;\n}\n\nfunction isColumnBackedField(field: RegistryField | undefined): boolean {\n if (!field) return false;\n if (field.transient === true || field._meta?.transient === true) return false;\n return !['meta', 'oneToMany', 'manyToMany'].includes(field.type ?? '');\n}\n\nfunction reportColumn(\n field: ReportFieldDefinition,\n registryField: RegistryField | undefined,\n): ReportColumnDescriptor | undefined {\n const policy = fieldPolicy(registryField);\n // A descriptor is an exposure boundary. Without a principal/permission\n // context, sensitive, permission-gated, and non-column fields fail closed.\n if (\n policy.sensitive ||\n policy.readPermission ||\n !isColumnBackedField(registryField)\n ) {\n return undefined;\n }\n\n const id = field.columnName ?? toSnakeCase(field.fieldName);\n const report = field.report;\n if (!report) return undefined;\n const type = queryType(field.type ?? registryField?.type);\n const filterOperators = filterOperatorsFor(type);\n const base: ReportColumnDescriptor = {\n id,\n fieldName: field.fieldName,\n label: registryField?.description || labelFor(field.fieldName),\n kind: report.kind,\n filterScope: report.kind === 'aggregate' ? 'having' : 'where',\n type,\n projectable: true,\n sortable: true,\n facetable: report.kind !== 'aggregate',\n ...(filterOperators ? { filterOperators } : {}),\n capabilities: [\n 'project',\n 'read',\n ...(filterOperators ? (['filter'] as const) : []),\n 'sort',\n ...(report.kind !== 'aggregate' ? (['facet'] as const) : []),\n ...(report.kind === 'aggregate'\n ? (['aggregate'] as const)\n : report.kind === 'group'\n ? (['group'] as const)\n : []),\n ],\n ...(report.kind === 'group'\n ? { sourceColumn: toSnakeCase(report.sourceColumn ?? field.fieldName) }\n : {}),\n ...(report.kind === 'bucket'\n ? { bucket: report.unit, sourceColumn: toSnakeCase(report.sourceColumn) }\n : {}),\n ...(report.kind === 'aggregate'\n ? {\n aggregate: report.fn,\n ...(report.column\n ? { sourceColumn: toSnakeCase(report.column) }\n : {}),\n ...(report.distinct === undefined\n ? {}\n : { distinct: report.distinct }),\n }\n : {}),\n ...descriptorMetadata(registryField),\n };\n return base;\n}\n\nfunction identityColumn(): ReportColumnDescriptor {\n return {\n id: 'id',\n fieldName: 'id',\n label: 'ID',\n kind: 'identity',\n filterScope: 'where',\n type: 'string',\n projectable: true,\n sortable: true,\n facetable: false,\n filterOperators: filterOperatorsFor('string'),\n capabilities: ['project', 'read', 'filter', 'sort'],\n };\n}\n\nfunction toQueryField(\n column: ReportColumnDescriptor,\n): DataQueryFieldDescriptor {\n return {\n id: column.id,\n type: column.type,\n ...(column.projectable === undefined\n ? {}\n : { projectable: column.projectable }),\n sortable: column.sortable === true,\n facetable: column.facetable === true,\n ...(column.filterOperators === undefined\n ? {}\n : { filterOperators: [...column.filterOperators] }),\n };\n}\n\nfunction filterOperatorsFor(\n type: DataQueryFieldDescriptor['type'],\n): DataQueryFilterOperator[] | undefined {\n switch (type) {\n case 'string':\n return ['eq', 'ne', 'gt', 'gte', 'lt', 'lte', 'in', 'notIn', 'like'];\n case 'number':\n case 'datetime':\n return ['eq', 'ne', 'gt', 'gte', 'lt', 'lte', 'in', 'notIn'];\n case 'boolean':\n return ['eq', 'ne', 'in', 'notIn'];\n case 'json':\n return undefined;\n }\n}\n\nfunction refreshDescriptor(\n refresh?: ReportRefreshConfig,\n refreshPermission = 'reports.refresh',\n): ReportRefreshDescriptor {\n const config = refresh ?? {};\n const triggers = new Set<ReportRefreshDescriptor['triggers'][number]>([\n 'manual',\n ]);\n if (!config.manual) {\n const hasSchedule = Boolean(config.schedule || config.fullRebuildSchedule);\n const hasChangeTrigger = Boolean(config.onChange?.length);\n if (hasSchedule) triggers.add('schedule');\n if (hasChangeTrigger) triggers.add('change');\n if (config.ttl !== undefined && config.ttl > 0) triggers.add('ttl');\n // Scheduler and on-change refreshes enqueue a durable job in either mode;\n // incremental mode alone is not itself a trigger.\n if (hasSchedule || hasChangeTrigger) triggers.add('job');\n }\n return {\n mode: config.mode ?? 'rebuild',\n mayRefreshOnRead:\n config.ttl !== undefined && config.ttl > 0 && !config.manual,\n ...(config.ttl === undefined ? {} : { ttlMs: config.ttl }),\n triggers: [...triggers],\n action: {\n id: 'refresh',\n label: 'Refresh report',\n scope: 'surface',\n phases: ['preview', 'apply'],\n requiresPermission: true,\n requiredPermission:\n refreshPermission.trim().length > 0\n ? refreshPermission\n : 'reports.refresh',\n auditRequired: true,\n },\n };\n}\n\nfunction resourceId(\n definition: ReportDefinition,\n scope: ReportAdapterOptions['tenantScope'],\n): string {\n return `${definition.reportClassName}#${scope ?? 'current'}`;\n}\n\nconst VALUE_FORMAT_ALIASES: Readonly<\n Record<string, ReportDataTableValueFormat>\n> = {\n text: 'text',\n date: 'date',\n datetime: 'datetime',\n 'date-time': 'datetime',\n percentage: 'percentage',\n percent: 'percentage',\n count: 'count',\n money: 'money',\n currency: 'money',\n number: 'number',\n decimal: 'number',\n integer: 'number',\n};\n\nconst VALUE_FORMATS = new Set<ReportDataTableValueFormat>(\n Object.values(VALUE_FORMAT_ALIASES),\n);\nconst COLUMN_ROLES = new Set<ReportDataTableColumnRole>([\n 'data',\n 'status',\n 'action',\n]);\nconst STRUCTURAL_ROW_KINDS = new Set<ReportDataTableStructuralRowKind>([\n 'summary',\n 'subtotal',\n 'aggregate',\n 'footer',\n]);\n\nfunction valueFormat(\n column: ReportColumnDescriptor,\n): ReportDataTableValueFormat {\n const configured = column.format?.trim().toLowerCase();\n if (configured && Object.hasOwn(VALUE_FORMAT_ALIASES, configured)) {\n return VALUE_FORMAT_ALIASES[configured];\n }\n if (column.kind === 'bucket') {\n return column.bucket === 'minute' || column.bucket === 'hour'\n ? 'datetime'\n : 'date';\n }\n if (column.kind === 'aggregate' && column.aggregate === 'count') {\n return 'count';\n }\n if (column.type === 'datetime') return 'datetime';\n if (column.type === 'number') return 'number';\n return 'text';\n}\n\nfunction headerPath(\n column: ReportColumnDescriptor,\n): ReportDataTableHeaderPathSegment[] {\n if (column.kind === 'aggregate') {\n return [\n { id: 'measures', label: 'Measures' },\n {\n id: `aggregate:${column.aggregate ?? 'value'}`,\n label: labelFor(column.aggregate ?? 'value'),\n },\n ];\n }\n if (column.kind === 'bucket') {\n return [\n { id: 'dimensions', label: 'Dimensions' },\n { id: 'time', label: 'Time' },\n ];\n }\n if (column.kind === 'group') {\n return [\n { id: 'dimensions', label: 'Dimensions' },\n { id: 'groups', label: 'Groups' },\n ];\n }\n return [{ id: 'dimensions', label: 'Dimensions' }];\n}\n\nfunction responsive(\n column: ReportColumnDescriptor,\n): ReportDataTableColumnResponsive {\n if (column.kind === 'group' || column.kind === 'bucket') {\n return { priority: 100, keepVisible: true };\n }\n if (column.kind === 'identity') return { priority: 80 };\n return { priority: 20 };\n}\n\nfunction assertHeaderPath(\n columnId: string,\n path: readonly ReportDataTableHeaderPathSegment[],\n): ReportDataTableHeaderPathSegment[] {\n if (!Array.isArray(path)) {\n throw new TypeError(\n `Report DataTable headerPath for ${columnId} must be an array`,\n );\n }\n return path.map((segment) => {\n if (\n !segment ||\n typeof segment.id !== 'string' ||\n segment.id.trim().length === 0 ||\n typeof segment.label !== 'string' ||\n segment.label.trim().length === 0\n ) {\n throw new TypeError(\n `Report DataTable headerPath entries for ${columnId} require non-empty id and label`,\n );\n }\n return { id: segment.id, label: segment.label };\n });\n}\n\nfunction dataTableColumn(\n column: ReportColumnDescriptor,\n override?: ReportDataTableColumnOverride,\n): ReportDataTableColumn {\n const format = override?.valueFormat ?? valueFormat(column);\n const path = override?.headerPath ?? headerPath(column);\n if (!VALUE_FORMATS.has(format)) {\n throw new TypeError(\n `Report DataTable valueFormat for ${column.id} is not supported: ${String(format)}`,\n );\n }\n if (override?.role !== undefined && !COLUMN_ROLES.has(override.role)) {\n throw new TypeError(\n `Report DataTable role for ${column.id} is not supported: ${String(override.role)}`,\n );\n }\n if (\n override?.label !== undefined &&\n (typeof override.label !== 'string' || override.label.trim().length === 0)\n ) {\n throw new TypeError(\n `Report DataTable label for ${column.id} must not be empty`,\n );\n }\n if (\n override?.align !== undefined &&\n override.align !== 'left' &&\n override.align !== 'right'\n ) {\n throw new TypeError(\n `Report DataTable align for ${column.id} is not supported: ${String(override.align)}`,\n );\n }\n if (\n override?.responsive?.priority !== undefined &&\n (!Number.isFinite(override.responsive.priority) ||\n override.responsive.priority < 0)\n ) {\n throw new TypeError(\n `Report DataTable responsive priority for ${column.id} must be a non-negative finite number`,\n );\n }\n if (\n override?.responsive?.keepVisible !== undefined &&\n typeof override.responsive.keepVisible !== 'boolean'\n ) {\n throw new TypeError(\n `Report DataTable keepVisible for ${column.id} must be a boolean`,\n );\n }\n return {\n id: column.id,\n label: override?.label ?? column.label,\n accessor: column.id,\n sortable: column.sortable === true,\n searchable: false,\n filterable: column.filterOperators !== undefined,\n headerPath: assertHeaderPath(column.id, path),\n valueFormat: format,\n align:\n override?.align ??\n (['count', 'money', 'number', 'percentage'].includes(format)\n ? 'right'\n : 'left'),\n role: override?.role ?? 'data',\n responsive: { ...responsive(column), ...override?.responsive },\n };\n}\n\nfunction structuralRows(\n columnIds: ReadonlySet<string>,\n rows: readonly ReportDataTableStructuralRowInput[] = [],\n): ReportDataTableStructuralRow[] {\n const ids = new Set<string>();\n return rows.map((row) => {\n if (\n !row ||\n typeof row.id !== 'string' ||\n row.id.trim().length === 0 ||\n typeof row.label !== 'string' ||\n row.label.trim().length === 0\n ) {\n throw new TypeError(\n 'Report structural rows require non-empty id and label values',\n );\n }\n if (ids.has(row.id)) {\n throw new TypeError(`Report structural row id must be unique: ${row.id}`);\n }\n if (!STRUCTURAL_ROW_KINDS.has(row.kind)) {\n throw new TypeError(\n `Report structural row kind is not supported: ${String(row.kind)}`,\n );\n }\n if (row.labelColumnId !== undefined) {\n if (\n typeof row.labelColumnId !== 'string' ||\n row.labelColumnId.trim().length === 0 ||\n !columnIds.has(row.labelColumnId)\n ) {\n throw new TypeError(\n `Report structural row labelColumnId must name an adapter column: ${String(row.labelColumnId)}`,\n );\n }\n }\n if (\n row.values !== undefined &&\n (typeof row.values !== 'object' ||\n row.values === null ||\n Array.isArray(row.values))\n ) {\n throw new TypeError('Report structural row values must be an object');\n }\n if (\n row.values &&\n Object.keys(row.values).some((columnId) => !columnIds.has(columnId))\n ) {\n throw new TypeError(\n 'Report structural row values must use adapter column ids',\n );\n }\n ids.add(row.id);\n return {\n id: row.id,\n kind: row.kind,\n label: row.label,\n ...(row.values !== undefined\n ? {\n values: jsonSafeValue(row.values) as Record<string, unknown>,\n }\n : {}),\n ...(row.labelColumnId !== undefined\n ? { labelColumnId: row.labelColumnId }\n : {}),\n selection: 'excluded' as const,\n actions: 'excluded' as const,\n };\n });\n}\n\nfunction drilldownDescriptor(\n columns: readonly ReportColumnDescriptor[],\n sourceClassName: string,\n): ReportDrilldownDescriptor {\n return {\n id: 'drilldown',\n sourceClassName,\n fields: columns\n .filter(\n (\n column,\n ): column is ReportColumnDescriptor & {\n kind: 'group' | 'bucket';\n sourceColumn: string;\n } =>\n (column.kind === 'group' || column.kind === 'bucket') &&\n typeof column.sourceColumn === 'string',\n )\n .map((column) => ({\n id: column.id,\n sourceColumn: column.sourceColumn,\n kind: column.kind,\n ...(column.kind === 'bucket' && column.bucket\n ? { bucket: column.bucket }\n : {}),\n })),\n inherits: ['principal', 'tenant', 'report-definition', 'field-policy'],\n };\n}\n\nexport async function buildReportAdapterDescriptor(\n reportCtor: new (...args: any[]) => SmrtObject,\n options: ReportAdapterOptions = {},\n): Promise<ReportAdapterDescriptor> {\n const definition = await buildReportDefinition(reportCtor);\n const registered = registeredClass(reportCtor);\n const fields = (await ObjectRegistry.getAllFields(\n definition.reportClassName,\n )) as Map<string, RegistryField>;\n // A tenant-looking field is not an isolation boundary. Only registered\n // tenant scoping installs the collection interceptor used by the executor.\n const tenantField = configuredTenantField(\n registered?.tenantScopedConfig?.field,\n );\n const columns = [\n identityColumn(),\n ...definition.fields\n .map((field) => reportColumn(field, fields.get(field.fieldName)))\n .filter((field): field is ReportColumnDescriptor => Boolean(field)),\n ].sort((left, right) => left.id.localeCompare(right.id));\n const schema: DataQuerySchema = {\n version: 1,\n identityField: 'id',\n fields: columns.map(toQueryField),\n defaultSort: [{ field: 'id', direction: 'asc' }],\n supports: { cursorPagination: false, consistency: false, facets: true },\n };\n const dataTable: ReportDataTableDescriptor = {\n rowKey: 'id',\n manualPagination: true,\n manualSorting: true,\n enableFiltering: true,\n enableSearch: false,\n columns: columns.map((column) =>\n dataTableColumn(column, options.dataTable?.columns?.[column.id]),\n ),\n structuralRows: structuralRows(\n new Set(columns.map((column) => column.id)),\n options.dataTable?.structuralRows,\n ),\n };\n return {\n version: 1,\n resourceId: resourceId(definition, options.tenantScope),\n reportClassName: definition.reportClassName,\n sourceClassName: definition.sourceClassName,\n tenantScoped: Boolean(registered?.tenantScopedConfig),\n ...(tenantField ? { tenantField } : {}),\n identityField: 'id',\n columns,\n schema,\n queryExecution: {\n modes: ['visible', 'background', 'silent'],\n visible: { delivery: 'result' },\n background: { delivery: 'queued', requiresHost: true },\n silent: { delivery: 'result', mutatesVisibleSurface: false },\n },\n dataTable,\n drilldown: drilldownDescriptor(columns, definition.sourceClassName),\n refresh: refreshDescriptor(definition.refresh, options.refreshPermission),\n };\n}\n\nfunction publicFieldMap(\n descriptor: ReportAdapterDescriptor,\n): Map<string, string> {\n return new Map(\n descriptor.columns.map((column) => [column.id, column.fieldName]),\n );\n}\n\n/**\n * Bind a materialized report row to its declared source dimensions. This is a\n * query handoff, not an executor: an authenticated source adapter must apply\n * the inherited principal, tenant, definition, and field policy before it\n * reads source records.\n */\nexport async function buildReportDrilldownQuery(\n reportCtor: new (...args: any[]) => SmrtObject,\n row: Readonly<Record<string, unknown>>,\n options: ReportAdapterOptions = {},\n): Promise<ReportDrilldownQuery> {\n const descriptor = await buildReportAdapterDescriptor(reportCtor, options);\n const constraints = descriptor.drilldown.fields.map((field) => {\n if (!Object.hasOwn(row, field.id)) {\n throw new DataQueryValidationError(\n `Report drilldown row is missing grouping field: ${field.id}`,\n 'DATA_QUERY_RESULT_INVALID',\n );\n }\n return {\n id: field.id,\n sourceColumn: field.sourceColumn,\n kind: field.kind,\n value: jsonSafeValue(row[field.id]),\n ...(field.bucket ? { bucket: field.bucket } : {}),\n };\n });\n return {\n version: 1,\n resourceId: descriptor.resourceId,\n reportClassName: descriptor.reportClassName,\n sourceClassName: descriptor.drilldown.sourceClassName,\n constraints,\n inherits: [...descriptor.drilldown.inherits],\n };\n}\n\ntype MaterializedWhere = Array<Array<Record<string, unknown>>>;\n\n/** Keep OR expansion bounded before it reaches the database collection. */\nconst MAX_REPORT_FILTER_OR_GROUPS = 128;\n\nfunction reportFilterError(message: string): never {\n throw new DataQueryValidationError(message, 'DATA_QUERY_UNSUPPORTED');\n}\n\nfunction crossProductWhere(\n left: MaterializedWhere,\n right: MaterializedWhere,\n): MaterializedWhere {\n if (left.length * right.length > MAX_REPORT_FILTER_OR_GROUPS) {\n return reportFilterError(\n `Report filter expands beyond ${MAX_REPORT_FILTER_OR_GROUPS} OR groups`,\n );\n }\n return left.flatMap((leftGroup) =>\n right.map((rightGroup) => [...leftGroup, ...rightGroup]),\n );\n}\n\nfunction inverseOperator(\n operator: DataQueryFilterOperator,\n): DataQueryFilterOperator {\n switch (operator) {\n case 'eq':\n return 'ne';\n case 'ne':\n return 'eq';\n case 'gt':\n return 'lte';\n case 'gte':\n return 'lt';\n case 'lt':\n return 'gte';\n case 'lte':\n return 'gt';\n case 'in':\n return 'notIn';\n case 'notIn':\n return 'in';\n case 'like':\n return reportFilterError(\n 'Report filters do not support negating a LIKE predicate',\n );\n }\n}\n\nfunction collectionCondition(\n field: string,\n operator: DataQueryFilterOperator,\n value: DataQueryCondition['value'],\n): MaterializedWhere {\n const keyFor = (suffix: string) => (suffix ? `${field} ${suffix}` : field);\n const scalar = (key: string, scalarValue: unknown): MaterializedWhere => [\n [{ [key]: scalarValue }],\n ];\n\n if (operator === 'in') {\n const values = value as Array<string | number | boolean | null>;\n const nonNull = values.filter((entry) => entry !== null);\n if (nonNull.length === 0) return scalar(field, null);\n if (nonNull.length === values.length) return scalar(keyFor('in'), nonNull);\n // SQL IN does not match NULL. Model the user-visible union explicitly.\n return [[{ [field]: null }], [{ [keyFor('in')]: nonNull }]];\n }\n\n if (operator === 'notIn') {\n const values = value as Array<string | number | boolean | null>;\n // `buildWhere()` does not have a `NOT IN` primitive. A bounded AND of\n // inequality predicates has the same null-safe semantics and remains fully\n // validated by the collection.\n return [values.map((entry) => ({ [keyFor('!=')]: entry }))];\n }\n\n const suffix: Record<\n Exclude<DataQueryFilterOperator, 'in' | 'notIn'>,\n string\n > = {\n eq: '',\n ne: '!=',\n gt: '>',\n gte: '>=',\n lt: '<',\n lte: '<=',\n like: 'like',\n };\n return scalar(keyFor(suffix[operator]), value);\n}\n\nfunction materializedWhereForFilter(\n filter: DataQueryFilter,\n fields: Map<string, string>,\n negate = false,\n): MaterializedWhere {\n if (filter.kind === 'condition') {\n const field = fields.get(filter.field);\n if (!field) {\n return reportFilterError(\n `Report filter field is not declared: ${filter.field}`,\n );\n }\n return collectionCondition(\n field,\n negate ? inverseOperator(filter.operator) : filter.operator,\n filter.value,\n );\n }\n\n if (filter.kind === 'not') {\n return materializedWhereForFilter(filter.filter, fields, !negate);\n }\n\n const combineWithAnd =\n (filter.kind === 'all' && !negate) || (filter.kind === 'any' && negate);\n if (combineWithAnd) {\n return filter.filters.reduce<MaterializedWhere>(\n (combined, child) =>\n crossProductWhere(\n combined,\n materializedWhereForFilter(child, fields, negate),\n ),\n [[]],\n );\n }\n\n const groups = filter.filters.flatMap((child) =>\n materializedWhereForFilter(child, fields, negate),\n );\n if (groups.length > MAX_REPORT_FILTER_OR_GROUPS) {\n return reportFilterError(\n `Report filter expands beyond ${MAX_REPORT_FILTER_OR_GROUPS} OR groups`,\n );\n }\n return groups;\n}\n\n/**\n * Converts a normalized report filter to collection-owned, parameterized\n * predicates. The returned DNF form gives each OR branch a complete AND\n * clause, so tenant interception can add its tenant predicate to every branch.\n */\nfunction materializedWhere(\n filter: DataQueryFilter | undefined,\n fields: Map<string, string>,\n): MaterializedWhere | undefined {\n return filter ? materializedWhereForFilter(filter, fields) : undefined;\n}\n\nfunction filterScopeForColumn(\n column: ReportColumnDescriptor,\n): ReportFilterScope {\n return column.filterScope;\n}\n\n/**\n * Split the declared filter language by report semantics for consumers that\n * construct a live source query. Mixed AND expressions are represented as two\n * independent clauses; mixed OR/NOT expressions are rejected because moving\n * either half across WHERE/HAVING would change their meaning.\n */\nexport function splitReportFilterScopes(\n descriptor: ReportAdapterDescriptor,\n filter: DataQueryFilter | undefined,\n): { where?: DataQueryFilter; having?: DataQueryFilter } {\n if (!filter) return {};\n const columns = new Map(\n descriptor.columns.map((column) => [column.id, column]),\n );\n const combineAll = (\n filters: DataQueryFilter[],\n ): DataQueryFilter | undefined => {\n const flattened = filters.flatMap((candidate) =>\n candidate.kind === 'all' ? candidate.filters : [candidate],\n );\n return flattened.length === 0\n ? undefined\n : flattened.length === 1\n ? flattened[0]\n : { kind: 'all', filters: flattened };\n };\n const split = (\n candidate: DataQueryFilter,\n ): { where?: DataQueryFilter; having?: DataQueryFilter } => {\n if (candidate.kind === 'condition') {\n const column = columns.get(candidate.field);\n if (!column) {\n reportFilterError(\n `Report filter field is not declared: ${candidate.field}`,\n );\n }\n return filterScopeForColumn(column) === 'where'\n ? { where: candidate }\n : { having: candidate };\n }\n\n if (candidate.kind === 'all') {\n const children = candidate.filters.map(split);\n const where = combineAll(\n children.flatMap((child) => (child.where ? [child.where] : [])),\n );\n const having = combineAll(\n children.flatMap((child) => (child.having ? [child.having] : [])),\n );\n return {\n ...(where ? { where } : {}),\n ...(having ? { having } : {}),\n };\n }\n\n const children =\n candidate.kind === 'not'\n ? [split(candidate.filter)]\n : candidate.filters.map(split);\n const scopes = new Set(\n children.flatMap((child) => [\n ...(child.where ? (['where'] as const) : []),\n ...(child.having ? (['having'] as const) : []),\n ]),\n );\n if (scopes.size !== 1) {\n reportFilterError(\n 'Report WHERE and HAVING filters cannot be mixed inside one OR or NOT expression',\n );\n }\n const scope = scopes.has('where') ? 'where' : 'having';\n const filters = children.flatMap((child) => {\n const filter = scope === 'where' ? child.where : child.having;\n return filter ? [filter] : [];\n });\n if (candidate.kind === 'not') {\n const [filter] = filters;\n if (!filter) {\n reportFilterError('Report NOT expressions must contain one filter');\n }\n return scope === 'where'\n ? { where: { kind: 'not', filter } }\n : { having: { kind: 'not', filter } };\n }\n return scope === 'where'\n ? { where: { kind: 'any', filters } }\n : { having: { kind: 'any', filters } };\n };\n\n return split(filter);\n}\n\nfunction jsonSafeValue(\n value: unknown,\n ancestors = new WeakSet<object>(),\n): unknown {\n if (value instanceof Date) return value.toISOString();\n if (typeof value === 'bigint') {\n const numeric = Number(value);\n if (!Number.isSafeInteger(numeric)) {\n throw new RangeError(\n 'Materialized bigint values must be safely representable as numbers',\n );\n }\n return numeric;\n }\n if (value && typeof value === 'object') {\n if (ancestors.has(value)) {\n throw new TypeError('Values must not contain circular references');\n }\n ancestors.add(value);\n try {\n if (Array.isArray(value)) {\n return value.map((entry) => jsonSafeValue(entry, ancestors));\n }\n return Object.fromEntries(\n Object.entries(value).map(([key, entry]) => [\n key,\n jsonSafeValue(entry, ancestors),\n ]),\n );\n } finally {\n ancestors.delete(value);\n }\n }\n return value;\n}\n\nfunction mapMaterializedRow(\n row: Record<string, unknown>,\n projection: readonly string[],\n fieldMap: Map<string, string>,\n): Record<string, unknown> {\n const mapped: Record<string, unknown> = {};\n for (const id of projection) {\n const sourceField = fieldMap.get(id) ?? id;\n if (sourceField in row) mapped[id] = jsonSafeValue(row[sourceField]);\n }\n return mapped;\n}\n\nfunction queryFreshness(\n lifecycle: ReportLifecycleSnapshot,\n): DataQueryFreshness {\n switch (lifecycle.state) {\n case 'current':\n return {\n state: 'fresh',\n ...(lifecycle.asOf ? { asOf: lifecycle.asOf } : {}),\n };\n case 'stale':\n case 'lock-skipped':\n case 'failed':\n return {\n state: 'stale',\n ...(lifecycle.asOf ? { asOf: lifecycle.asOf } : {}),\n };\n case 'refreshing':\n return lifecycle.hasUsableRows\n ? {\n state: 'stale',\n ...(lifecycle.asOf ? { asOf: lifecycle.asOf } : {}),\n }\n : { state: 'unknown' };\n }\n}\n\n/**\n * Execute bounded materialized report reads through the canonical query\n * envelope. Every predicate is an adapter-declared field/operator pair; the\n * collection converts it to parameterized SQL and applies tenant interceptors\n * to list, count, and facet reads alike.\n */\nexport function queryReportMaterializedRows(\n reportCtor: new (...args: any[]) => SmrtObject,\n input: unknown,\n options: ReportBackgroundQueryOptions,\n): Promise<ReportBackgroundQueryResult>;\nexport function queryReportMaterializedRows(\n reportCtor: new (...args: any[]) => SmrtObject,\n input: unknown,\n options?: ReportQueryOptions,\n): Promise<ReportDataQueryResult>;\nexport async function queryReportMaterializedRows(\n reportCtor: new (...args: any[]) => SmrtObject,\n input: unknown,\n options: ReportQueryOptions | ReportBackgroundQueryOptions = {},\n): Promise<ReportDataQueryResult | ReportBackgroundQueryResult> {\n const descriptor = await buildReportAdapterDescriptor(reportCtor);\n const request = normalizeDataQueryRequest(input, descriptor.schema);\n // Validate the semantic split even though materialized-row reads execute the\n // complete predicate against one persisted table. It prevents an adapter\n // consumer from silently treating a mixed source WHERE/HAVING expression as\n // either side of the aggregate boundary.\n splitReportFilterScopes(descriptor, request.filter);\n const queryFingerprint = createDataQueryFingerprint(\n request,\n descriptor.schema,\n );\n if (options.execution === 'background') {\n const queued = await options.enqueueBackgroundQuery({\n version: 1,\n execution: 'background',\n resourceId: descriptor.resourceId,\n reportClassName: descriptor.reportClassName,\n request,\n inherits: ['principal', 'tenant', 'report-definition', 'field-policy'],\n });\n if (typeof queued.taskId !== 'string' || queued.taskId.length === 0) {\n throw new Error('Background report query hosts must return a task id');\n }\n return {\n version: 1,\n execution: 'background',\n status: 'queued',\n taskId: queued.taskId,\n queryFingerprint,\n };\n }\n const execution = options.execution ?? 'visible';\n const lifecycleDb = options.db;\n if (options.lifecycle && !lifecycleDb) {\n throw new Error('Report lifecycle disclosure requires a database handle');\n }\n const lifecycleOptions =\n options.lifecycle && lifecycleDb\n ? { db: lifecycleDb, ...options.lifecycle }\n : undefined;\n const lifecycleBefore = lifecycleOptions\n ? await getReportLifecycle(reportCtor, lifecycleOptions)\n : undefined;\n const fieldMap = publicFieldMap(descriptor);\n const where = materializedWhere(request.filter, fieldMap);\n const collection: NonNullable<ReportQueryOptions['collection']> =\n options.collection ??\n ((await ObjectRegistry.getCollection(descriptor.reportClassName, {\n db: options.db,\n })) as unknown as NonNullable<ReportQueryOptions['collection']>);\n let rows: Record<string, unknown>[] = [];\n let page: DataQueryResult['page'];\n let total: number;\n let facets: DataQueryResult['facets'];\n if (request.mode === 'rows') {\n const projection = request.projection ?? [descriptor.identityField];\n const select = projection.map((id) => fieldMap.get(id) ?? id);\n const offset = request.page?.kind === 'offset' ? request.page.offset : 0;\n const limit = request.page?.limit ?? 50;\n const orderBy = request.sort?.map(\n ({ field, direction }) =>\n `${fieldMap.get(field) ?? field} ${direction.toUpperCase()}`,\n );\n const materialized = await collection.list({\n select,\n offset,\n limit,\n orderBy: orderBy?.length === 1 ? orderBy[0] : orderBy,\n ...(where === undefined ? {} : { where }),\n });\n rows = materialized.map((row) =>\n mapMaterializedRow(row, projection, fieldMap),\n );\n rows.forEach((row) => {\n if (typeof row.id !== 'string' || row.id.length === 0) {\n throw new Error(\n 'Materialized report rows require a non-empty string id',\n );\n }\n });\n // SmrtReportCollection performs an eligible TTL refresh from list(). Count\n // must follow that operation so total/hasMore describe the same snapshot.\n total = await collection.count(where === undefined ? undefined : { where });\n page = {\n kind: 'offset',\n offset,\n limit,\n hasMore: offset + rows.length < total,\n };\n } else {\n // Count-only reads still enter SmrtReportCollection's list lifecycle to\n // refresh an eligible stale report before calculating its exact total.\n await collection.list({\n select: ['id'],\n offset: 0,\n limit: 1,\n orderBy: 'id ASC',\n ...(where === undefined ? {} : { where }),\n });\n total = await collection.count(where === undefined ? undefined : { where });\n if (request.mode === 'facets') {\n if (!collection.facets) {\n throw new Error(\n 'Report materialized facet reads require a collection facets() implementation',\n );\n }\n const requested = request.facets ?? [];\n const sourceFacets = await collection.facets({\n fields: requested.map((facet) => ({\n field: fieldMap.get(facet.field) ?? facet.field,\n limit: facet.limit,\n })),\n ...(where === undefined ? {} : { where }),\n });\n const byField = new Map(\n sourceFacets.map((facet) => [facet.field, facet]),\n );\n facets = requested.map((facet) => {\n const sourceField = fieldMap.get(facet.field) ?? facet.field;\n const values = byField.get(sourceField)?.values ?? [];\n return {\n field: facet.field,\n values: values.map((value) => ({\n value: value.value,\n count: value.count,\n })),\n // The collection deliberately bounds this database grouping query;\n // an exactly-full page may have more values, so report conservatively.\n truncated: values.length >= facet.limit,\n };\n });\n }\n }\n const result = {\n version: 1 as const,\n requestId: request.requestId,\n queryFingerprint,\n identityField: descriptor.identityField,\n rows,\n ...(page ? { page } : {}),\n total: { kind: 'exact' as const, value: total },\n ...(facets === undefined ? {} : { facets }),\n freshness: { state: 'unknown' as const },\n warnings: [],\n truncated: false,\n };\n const normalized = normalizeDataQueryResult(\n result,\n request,\n descriptor.schema,\n );\n if (!lifecycleBefore || !lifecycleOptions) {\n return { ...normalized, execution };\n }\n\n const lifecycleAfter = await getReportLifecycle(reportCtor, lifecycleOptions);\n return {\n ...normalized,\n execution,\n freshness: queryFreshness(lifecycleAfter),\n reportLifecycle: {\n snapshot: lifecycleAfter,\n read:\n lifecycleBefore.state !== 'current' &&\n lifecycleAfter.state === 'current'\n ? 'refresh-triggered'\n : lifecycleAfter.state === 'current'\n ? 'current'\n : 'stale',\n },\n };\n}\n\n/** Read the already-materialized primary key; never use a display/page index. */\nexport function reportMaterializedRowKey(row: Record<string, unknown>): string {\n const id = row.id;\n if (typeof id !== 'string' || id.length === 0) {\n throw new Error('Materialized report rows require a non-empty string id');\n }\n return id;\n}\n","import {\n ObjectRegistry,\n SmrtCollection,\n SmrtObject,\n type SmrtObjectOptions,\n} from '@happyvertical/smrt-core';\nimport { toSnakeCase } from '@happyvertical/smrt-core/utils';\nimport { getTenantId } from '@happyvertical/smrt-tenancy';\nimport { validateColumnName } from '@happyvertical/sql';\nimport { buildReportDefinition } from './compiler.js';\nimport { refreshReport } from './refresh.js';\nimport type { ReportRefreshOptions, ReportRefreshResult } from './types.js';\n\ntype RegistryField = {\n columnName?: string;\n _meta?: {\n columnName?: string;\n __tenancy?: { isTenantIdField?: boolean };\n };\n};\n\nfunction registryColumnName(fieldName: string, field?: RegistryField): string {\n return validateColumnName(\n field?.columnName ?? field?._meta?.columnName ?? toSnakeCase(fieldName),\n );\n}\n\nfunction findFieldColumn(\n fields: Map<string, RegistryField>,\n fieldName: string,\n): string {\n const direct = fields.get(fieldName);\n if (direct) return registryColumnName(fieldName, direct);\n const requestedColumn = toSnakeCase(fieldName);\n for (const [name, field] of fields.entries()) {\n if (toSnakeCase(name) === requestedColumn) {\n return registryColumnName(name, field);\n }\n }\n return validateColumnName(requestedColumn);\n}\n\nfunction findTenantColumn(\n fields: Map<string, RegistryField>,\n configuredField?: string,\n): string | null {\n if (configuredField) {\n return registryColumnName(configuredField, fields.get(configuredField));\n }\n for (const [fieldName, field] of fields.entries()) {\n if (fieldName === 'tenantId' || field?._meta?.__tenancy?.isTenantIdField) {\n return registryColumnName(fieldName, field);\n }\n }\n return null;\n}\n\nexport class SmrtReport extends SmrtObject {\n static readonly _isReportBase = true as const;\n\n refreshedAt: Date | null = null;\n\n constructor(options: SmrtObjectOptions = {}) {\n super(options);\n if (options.refreshedAt !== undefined) {\n this.refreshedAt =\n options.refreshedAt instanceof Date\n ? options.refreshedAt\n : options.refreshedAt\n ? new Date(options.refreshedAt)\n : null;\n }\n }\n\n isStale(ttlMs?: number): boolean {\n if (!this.refreshedAt) return true;\n if (ttlMs === undefined) return false;\n return Date.now() - this.refreshedAt.getTime() > ttlMs;\n }\n\n async refresh(\n options: Omit<ReportRefreshOptions, 'db'> = {},\n ): Promise<ReportRefreshResult> {\n const result = await refreshReport(this.constructor as typeof SmrtReport, {\n ...options,\n db: this.db,\n });\n this.refreshedAt = result.refreshedAt;\n return result;\n }\n}\n\nexport class SmrtReportCollection<\n ModelType extends SmrtReport,\n> extends SmrtCollection<ModelType> {\n private async refreshIfStale(): Promise<void> {\n const reportCtor = this.getItemClass();\n const definition = await buildReportDefinition(reportCtor);\n const refresh = definition.refresh;\n if (!refresh?.ttl || refresh.manual) return;\n\n const registered =\n ObjectRegistry.getClassByConstructor(reportCtor) ??\n ObjectRegistry.getClass(reportCtor.name);\n const reportClass =\n registered?.qualifiedName ?? registered?.name ?? reportCtor.name;\n const tableName = ObjectRegistry.getTableName(reportClass);\n if (!tableName) return;\n\n const fields = (await ObjectRegistry.getAllFields(reportClass)) as Map<\n string,\n RegistryField\n >;\n const safeTableName = validateColumnName(tableName);\n const refreshedAtColumn = findFieldColumn(fields, 'refreshedAt');\n const tenantColumn = findTenantColumn(\n fields,\n registered?.tenantScopedConfig?.field,\n );\n const tenantId = getTenantId() ?? null;\n const result = tenantColumn\n ? await this.db.query(\n `SELECT MAX(${refreshedAtColumn}) AS refreshed_at FROM ${safeTableName} WHERE ${tenantColumn} ${tenantId ? '= ?' : 'IS NULL'}`,\n ...(tenantId ? [tenantId] : []),\n )\n : await this.db.query(\n `SELECT MAX(${refreshedAtColumn}) AS refreshed_at FROM ${safeTableName}`,\n );\n const refreshedAt = result.rows[0]?.refreshed_at\n ? new Date(result.rows[0].refreshed_at as string)\n : null;\n const stale =\n !refreshedAt || Date.now() - refreshedAt.getTime() > refresh.ttl;\n if (!stale) return;\n\n await refreshReport(reportCtor, {\n db: this.db,\n mode: refresh.mode ?? 'rebuild',\n trigger: 'ttl',\n tenantId,\n });\n }\n\n async refresh(\n options: Omit<ReportRefreshOptions, 'db'> = {},\n ): Promise<ReportRefreshResult> {\n return refreshReport(this.getItemClass(), {\n ...options,\n db: this.db,\n });\n }\n\n override async list(\n options: Parameters<SmrtCollection<ModelType>['list']>[0] = {},\n ): Promise<ModelType[]> {\n await this.refreshIfStale();\n return super.list(options);\n }\n\n override async get(\n filter: Parameters<SmrtCollection<ModelType>['get']>[0],\n options: Parameters<SmrtCollection<ModelType>['get']>[1] = {},\n ): Promise<ModelType | null> {\n await this.refreshIfStale();\n return super.get(filter, options);\n }\n}\n"],"mappings":"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"}
|
package/dist/manifest.json
CHANGED
package/dist/refresh.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { buildReportDefinition, compileReportDefinition, getReportGroupingColumns } from "./compiler.js";
|
|
2
2
|
import { REPORT_LOCKS_TABLE, REPORT_RUNS_TABLE, REPORT_WATERMARKS_TABLE, assertReportTablesReady, scopeKeyForTenant } from "./state.js";
|
|
3
|
-
import { buildAggregate, buildWhere, tableExists, validateColumnName } from "@happyvertical/sql";
|
|
4
3
|
import { ObjectRegistry } from "@happyvertical/smrt-core";
|
|
5
4
|
import { toSnakeCase } from "@happyvertical/smrt-core/utils";
|
|
6
|
-
import { createHash, randomUUID } from "node:crypto";
|
|
7
5
|
import { getTenantId, withTenant } from "@happyvertical/smrt-tenancy";
|
|
6
|
+
import { buildAggregate, buildWhere, tableExists, validateColumnName } from "@happyvertical/sql";
|
|
7
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
8
8
|
//#region src/refresh.ts
|
|
9
9
|
function isSqlAdapterType(value) {
|
|
10
10
|
return value === "sqlite" || value === "postgres" || value === "duckdb" || value === "json";
|
package/dist/scheduler.js
CHANGED
|
@@ -2,8 +2,8 @@ import { buildReportDefinition } from "./compiler.js";
|
|
|
2
2
|
import { REPORT_SCHEDULER_TABLES, REPORT_SCHEDULES_TABLE, assertReportTablesReady, scopeKeyForTenant } from "./state.js";
|
|
3
3
|
import { refreshReport } from "./refresh.js";
|
|
4
4
|
import { GlobalInterceptors, ObjectRegistry, SmrtObject, field, smrt } from "@happyvertical/smrt-core";
|
|
5
|
-
import { createHash } from "node:crypto";
|
|
6
5
|
import { TenantScoped, getTenantId, tenantId } from "@happyvertical/smrt-tenancy";
|
|
6
|
+
import { createHash } from "node:crypto";
|
|
7
7
|
import { EventEmitter } from "node:events";
|
|
8
8
|
import { SmrtJobCollection, backgroundEligible, getNextCronDate, validateCronExpression } from "@happyvertical/smrt-jobs";
|
|
9
9
|
//#region src/scheduler.ts
|
package/dist/smrt-knowledge.json
CHANGED
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
"sensitiveFieldsExcluded": true,
|
|
4
4
|
"generatedAt": "1970-01-01T00:00:00.000Z",
|
|
5
5
|
"packageName": "@happyvertical/smrt-reports",
|
|
6
|
-
"packageVersion": "0.42.
|
|
6
|
+
"packageVersion": "0.42.7",
|
|
7
7
|
"sourceManifestPath": "dist/manifest.json",
|
|
8
8
|
"agentDocPath": "AGENTS.md",
|
|
9
9
|
"sourceHashes": {
|
|
10
|
-
"manifest": "
|
|
11
|
-
"packageJson": "
|
|
12
|
-
"agents": "
|
|
10
|
+
"manifest": "9588cba9a20a530e1d71213fbca472b2c57a87760f67470143768675dd5f4741",
|
|
11
|
+
"packageJson": "af05dfa424420ace395635656f6fed0b192689d9c33200439c74077a8f496137",
|
|
12
|
+
"agents": "3c4350e771c33aa34107f6b27ad9c8d1a90f11f290f839a07e7bb3fad7b6ae01"
|
|
13
13
|
},
|
|
14
14
|
"exports": [
|
|
15
15
|
".",
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"@happyvertical/smrt-core": "workspace:*",
|
|
27
27
|
"@happyvertical/smrt-jobs": "workspace:*",
|
|
28
|
+
"@happyvertical/smrt-types": "workspace:*",
|
|
28
29
|
"@happyvertical/smrt-tenancy": "workspace:*",
|
|
29
30
|
"@happyvertical/sql": "catalog:",
|
|
30
31
|
"@happyvertical/smrt-vitest": "workspace:*",
|
|
@@ -37,6 +38,7 @@
|
|
|
37
38
|
"@happyvertical/smrt-core",
|
|
38
39
|
"@happyvertical/smrt-jobs",
|
|
39
40
|
"@happyvertical/smrt-tenancy",
|
|
41
|
+
"@happyvertical/smrt-types",
|
|
40
42
|
"@happyvertical/smrt-vitest"
|
|
41
43
|
],
|
|
42
44
|
"sdkDependencies": [
|
|
@@ -812,5 +814,5 @@
|
|
|
812
814
|
"polymorphicAssociations": 0,
|
|
813
815
|
"uuidColumns": 14
|
|
814
816
|
},
|
|
815
|
-
"agentDoc": "# @happyvertical/smrt-reports\n\nMaterialized aggregate report models for SMRT.\n\n## Key Pieces\n\n| Module | Purpose |\n| --- | --- |\n| `SmrtReport` | Abstract report row base with `refreshedAt`, `isStale()`, and manual `refresh()` |\n| decorators | `@report`, grouping decorators, time buckets, and aggregate measure decorators |\n| compiler | Pure `ReportDefinition -> AggregateSpec` compiler and ObjectRegistry adapter |\n| aggregate | Compatibility re-export of the SDK aggregate query builder |\n| refresh | Rebuild and incremental refresh engine with run tracking, watermarks, locks, and tenant scoping |\n| state | Internal `_smrt_report_*` system models for runs, watermarks, locks, schedules, and refresh tasks |\n| scheduler | Cron schedule runner, durable refresh job enqueueing, and `onChange` interceptor registration |\n\n## Conventions\n\n- Report cache tables are normal `@smrt()` tables. Runtime refresh must not create schema.\n- Store report metadata under field `_meta.__report`; scanner and runtime decorators must stay aligned.\n- Keep SQL generation portable. Use the SDK `buildAggregate()`/`bucketExpr()` helpers for time buckets and `$N` placeholders.\n- Do not add a local aggregate SQL builder here; the implementation lives in `@happyvertical/sql`.\n- Refresh runtime tables are schema-managed. Runtime refresh must fail clearly when `_smrt_report_runs`, `_smrt_report_watermarks`, or `_smrt_report_locks` have not been migrated.\n- Incremental refresh requires a source watermark column (default `updatedAt`) and soft-delete column (default `deletedAt`); it recomputes affected groups and deletes empty report groups instead of applying aggregate deltas.\n- Raw aggregate refreshes must explicitly filter `tenant_id`; the tenancy interceptor only protects normal collection reads.\n- Scheduled/on-change refreshes enqueue `SmrtReportRefreshTask.run()` through `@happyvertical/smrt-jobs`; do not add a separate report queue.\n"
|
|
817
|
+
"agentDoc": "# @happyvertical/smrt-reports\n\nMaterialized aggregate report models for SMRT.\n\n## Key Pieces\n\n| Module | Purpose |\n| --- | --- |\n| `SmrtReport` | Abstract report row base with `refreshedAt`, `isStale()`, and manual `refresh()` |\n| decorators | `@report`, grouping decorators, time buckets, and aggregate measure decorators |\n| compiler | Pure `ReportDefinition -> AggregateSpec` compiler and ObjectRegistry adapter |\n| aggregate | Compatibility re-export of the SDK aggregate query builder |\n| refresh | Rebuild and incremental refresh engine with run tracking, watermarks, locks, and tenant scoping |\n| state | Internal `_smrt_report_*` system models for runs, watermarks, locks, schedules, and refresh tasks |\n| scheduler | Cron schedule runner, durable refresh job enqueueing, and `onChange` interceptor registration |\n| adapter | Transport-neutral report descriptor, canonical materialized-row reads, and stable `id` row identity |\n| lifecycle | Tenant-safe freshness, run, lock, failure, and manual refresh preview/apply surfaces |\n\n## Adapter contract\n\n- `buildReportAdapterDescriptor()` returns deterministic, serializable metadata\n for a report surface: a stable resource id, typed persisted report columns,\n the canonical `DataQuerySchema`, and UI-neutral DataTable hints. It must not\n import `smrt-ui` or expose a report-domain class to the consumer.\n- `queryReportMaterializedRows()` owns only the bounded read slice for rows that\n are already materialized. It supports projection, offset/limit paging,\n validated filters, deterministic multi-sort with an `id` tie-breaker, exact\n totals, and dimension facets. At source-query compilation,\n dimension and bucket filters compile to `WHERE`, aggregate-measure filters\n compile to `HAVING`, and mixed `OR`/`NOT` filter scopes fail closed.\n- `id` is the only row identity. It must be a non-empty persisted string and is\n never replaced by a display index or page position.\n- The descriptor is an exposure boundary. Sensitive/secret fields, fields with\n `readPermission`, and transient, system, or non-column fields fail closed and\n do not become public columns when no principal is available.\n- `tenantScoped`/`tenantField` reflect actual registered tenant metadata. A\n `tenantScope` option only contributes to the stable resource id; it is not\n authorization. The default query path resolves the registered collection via\n `ObjectRegistry`, so normal collection tenancy interceptors apply. An injected\n collection is application-owned and must preserve the same boundary.\n- `refresh` is a declaration, not execution. It describes configured mode,\n triggers, positive-TTL stale-read behavior, and a permissioned/audited action\n with preview/apply phases. The adapter remains read-only, while\n `getReportLifecycle()` provides an explicit, tenant-safe lifecycle snapshot\n and `previewReportRefresh()` / `applyReportRefresh()` delegate authorization,\n audit, and queueing through an application action host. Only a registered\n `SmrtReportCollection` may synchronously refresh stale reads, when its TTL is\n positive and the report is not manual.\n\n## Conventions\n\n- Report cache tables are normal `@smrt()` tables. Runtime refresh must not create schema.\n- Store report metadata under field `_meta.__report`; scanner and runtime decorators must stay aligned.\n- Keep SQL generation portable. Use the SDK `buildAggregate()`/`bucketExpr()` helpers for time buckets and `$N` placeholders.\n- Do not add a local aggregate SQL builder here; the implementation lives in `@happyvertical/sql`.\n- Refresh runtime tables are schema-managed. Runtime refresh must fail clearly when `_smrt_report_runs`, `_smrt_report_watermarks`, or `_smrt_report_locks` have not been migrated.\n- Incremental refresh requires a source watermark column (default `updatedAt`) and soft-delete column (default `deletedAt`); it recomputes affected groups and deletes empty report groups instead of applying aggregate deltas.\n- Raw aggregate refreshes must explicitly filter `tenant_id`; the tenancy interceptor only protects normal collection reads.\n- Scheduled/on-change refreshes enqueue `SmrtReportRefreshTask.run()` through `@happyvertical/smrt-jobs`; do not add a separate report queue.\n"
|
|
816
818
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@happyvertical/smrt-reports",
|
|
3
|
-
"version": "0.42.
|
|
3
|
+
"version": "0.42.7",
|
|
4
4
|
"description": "Materialized aggregate report models for SMRT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -44,16 +44,17 @@
|
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
46
|
"@happyvertical/sql": "^0.88.0",
|
|
47
|
-
"@happyvertical/smrt-core": "0.42.
|
|
48
|
-
"@happyvertical/smrt-
|
|
49
|
-
"@happyvertical/smrt-tenancy": "0.42.
|
|
47
|
+
"@happyvertical/smrt-core": "0.42.7",
|
|
48
|
+
"@happyvertical/smrt-types": "0.42.7",
|
|
49
|
+
"@happyvertical/smrt-tenancy": "0.42.7",
|
|
50
|
+
"@happyvertical/smrt-jobs": "0.42.7"
|
|
50
51
|
},
|
|
51
52
|
"devDependencies": {
|
|
52
53
|
"@types/node": "24.13.2",
|
|
53
54
|
"typescript": "5.9.3",
|
|
54
55
|
"vite": "8.1.4",
|
|
55
56
|
"vitest": "4.1.10",
|
|
56
|
-
"@happyvertical/smrt-vitest": "0.42.
|
|
57
|
+
"@happyvertical/smrt-vitest": "0.42.7"
|
|
57
58
|
},
|
|
58
59
|
"keywords": [
|
|
59
60
|
"ai",
|