@happyvertical/smrt-reports 0.55.6 → 0.55.8
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 +34 -0
- package/README.md +16 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +995 -4
- package/dist/index.js.map +1 -1
- package/dist/manifest.json +444 -14
- package/dist/recipes.d.ts +28 -0
- package/dist/recipes.d.ts.map +1 -0
- package/dist/refresh.d.ts +3 -0
- package/dist/refresh.d.ts.map +1 -1
- package/dist/refresh.js +1 -1
- package/dist/refresh.js.map +1 -1
- package/dist/runtime-compiler.d.ts +142 -0
- package/dist/runtime-compiler.d.ts.map +1 -0
- package/dist/runtime-report.d.ts +105 -0
- package/dist/runtime-report.d.ts.map +1 -0
- package/dist/runtime-spec.d.ts +126 -0
- package/dist/runtime-spec.d.ts.map +1 -0
- package/dist/smrt-knowledge.json +328 -19
- package/dist/state.d.ts.map +1 -1
- package/dist/state.js +26 -13
- package/dist/state.js.map +1 -1
- package/package.json +8 -7
package/dist/refresh.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"refresh.js","names":["runId"],"sources":["../src/refresh.ts"],"sourcesContent":["import { createHash, randomUUID } from 'node:crypto';\nimport { ObjectRegistry, type SmrtObject } from '@happyvertical/smrt-core';\nimport { toSnakeCase } from '@happyvertical/smrt-core/utils';\nimport { getTenantId, withTenant } from '@happyvertical/smrt-tenancy';\nimport {\n buildAggregate,\n buildWhere,\n type DatabaseInterface,\n type SqlAdapterType,\n tableExists,\n validateColumnName,\n} from '@happyvertical/sql';\nimport {\n buildReportDefinition,\n compileReportDefinition,\n getReportGroupingColumns,\n} from './compiler.js';\nimport {\n assertReportTablesReady,\n REPORT_LOCKS_TABLE,\n REPORT_RUNS_TABLE,\n REPORT_WATERMARKS_TABLE,\n scopeKeyForTenant,\n} from './state.js';\nimport type {\n AggregateSelectExpr,\n AggregateSpec,\n ReportDefinition,\n ReportRefreshMode,\n ReportRefreshOptions,\n ReportRefreshResult,\n ReportSource,\n} from './types.js';\n\ntype ReportCtor = new (...args: any[]) => SmrtObject;\n\ninterface RefreshScope {\n tenantId: string | null;\n scopeKey: string;\n sourceTenantColumn: string | null;\n reportTenantColumn: string | null;\n}\n\ninterface WatermarkConfig {\n watermarkColumn: string;\n softDeleteColumn: string;\n}\n\ninterface RefreshWorkResult {\n rowCount: number;\n changedGroupCount: number;\n watermarkBefore?: string | null;\n watermarkAfter?: string | null;\n mode: ReportRefreshMode;\n}\n\ninterface ReportLockHandle {\n ownerId: string;\n release(): Promise<void>;\n}\n\nfunction isSqlAdapterType(value: unknown): value is SqlAdapterType {\n return (\n value === 'sqlite' ||\n value === 'postgres' ||\n value === 'duckdb' ||\n value === 'json'\n );\n}\n\nfunction adapterTypeFromDb(\n db: DatabaseInterface,\n fallback?: SqlAdapterType,\n): SqlAdapterType {\n if (isSqlAdapterType(fallback)) return fallback;\n\n const url = String(db.url ?? '');\n if (url.startsWith('postgres://') || url.startsWith('postgresql://')) {\n return 'postgres';\n }\n if (url.endsWith('.duckdb')) {\n return 'duckdb';\n }\n if (url === ':memory:' || url.endsWith('.sqlite') || url.endsWith('.db')) {\n return 'sqlite';\n }\n throw new Error(\n 'Report refresh requires a database adapter type. Pass { adapterType }.',\n );\n}\n\nfunction stableReportId(values: unknown[]): string {\n const hash = createHash('sha256')\n .update(JSON.stringify(values))\n .digest('hex');\n const variant = ((Number.parseInt(hash[16], 16) & 0x3) | 0x8).toString(16);\n return [\n hash.slice(0, 8),\n hash.slice(8, 12),\n `4${hash.slice(13, 16)}`,\n `${variant}${hash.slice(17, 20)}`,\n hash.slice(20, 32),\n ].join('-');\n}\n\nfunction getReportTableName(reportCtor: ReportCtor): string {\n const registered =\n ObjectRegistry.getClassByConstructor(reportCtor) ??\n ObjectRegistry.getClass(reportCtor.name);\n const tableName = registered\n ? ObjectRegistry.getTableName(registered.qualifiedName ?? registered.name)\n : ObjectRegistry.getTableName(reportCtor.name);\n if (!tableName) {\n throw new Error(`No report table registered for ${reportCtor.name}`);\n }\n return tableName;\n}\n\nfunction getActualGroupingColumns(\n definition: Pick<ReportDefinition, 'fields'>,\n): string[] {\n return definition.fields\n .filter(\n (field) =>\n field.report?.kind === 'group' || field.report?.kind === 'bucket',\n )\n .map((field) => field.columnName ?? toSnakeCase(field.fieldName));\n}\n\nfunction materializeRows(\n rows: Record<string, any>[],\n definition: ReportDefinition,\n refreshedAt: Date,\n scope: RefreshScope,\n): Record<string, any>[] {\n const groupingColumns = getActualGroupingColumns(definition);\n const now = refreshedAt.toISOString();\n\n return rows.map((row, index) => {\n const groupingValues =\n groupingColumns.length === 0\n ? [definition.reportClassName, scope.scopeKey, index]\n : [\n definition.reportClassName,\n scope.scopeKey,\n ...groupingColumns.map((column) => row[column]),\n ];\n const id = stableReportId(groupingValues);\n const tenantFields = scope.reportTenantColumn\n ? { [scope.reportTenantColumn]: scope.tenantId }\n : {};\n\n return {\n id,\n slug: id,\n context: scope.scopeKey,\n ...tenantFields,\n ...row,\n refreshed_at: now,\n created_at: now,\n updated_at: now,\n };\n });\n}\n\nfunction sourceClassName(source: ReportSource): string {\n if (typeof source === 'string') return source;\n return source.name;\n}\n\nasync function fieldColumn(\n className: string,\n requested: string,\n): Promise<string | null> {\n const fields = await ObjectRegistry.getAllFields(className);\n const requestedColumn = toSnakeCase(requested);\n\n for (const [fieldName] of fields.entries()) {\n if (fieldName === requested || toSnakeCase(fieldName) === requestedColumn) {\n return requestedColumn;\n }\n }\n\n return null;\n}\n\nasync function tenantColumn(className: string): Promise<string | null> {\n const registered = ObjectRegistry.getClass(className);\n const configuredField = registered?.tenantScopedConfig?.field;\n if (configuredField) {\n return toSnakeCase(configuredField);\n }\n\n const fields = await ObjectRegistry.getAllFields(className);\n for (const [fieldName, field] of fields.entries()) {\n if (field?._meta?.__tenancy?.isTenantIdField) {\n return toSnakeCase(fieldName);\n }\n }\n\n const tenantIdField = await fieldColumn(className, 'tenantId');\n return tenantIdField;\n}\n\nasync function resolveScope(\n definition: ReportDefinition,\n options: ReportRefreshOptions,\n): Promise<RefreshScope> {\n const tenantId =\n options.tenantId !== undefined ? options.tenantId : (getTenantId() ?? null);\n const sourceTenantColumn = await tenantColumn(definition.sourceClassName);\n const reportTenantColumn = await tenantColumn(definition.reportClassName);\n\n if (tenantId && !sourceTenantColumn) {\n throw new Error(\n `Tenant-scoped refresh for ${definition.reportClassName} requires source ` +\n `${definition.sourceClassName} to expose a tenantId field.`,\n );\n }\n if (tenantId && !reportTenantColumn) {\n throw new Error(\n `Tenant-scoped refresh for ${definition.reportClassName} requires the report table to expose a tenantId field.`,\n );\n }\n\n return {\n tenantId,\n scopeKey: scopeKeyForTenant(tenantId),\n sourceTenantColumn,\n reportTenantColumn,\n };\n}\n\nfunction tenantWhere(scope: RefreshScope): Record<string, unknown> {\n if (!scope.sourceTenantColumn) return {};\n return { [scope.sourceTenantColumn]: scope.tenantId };\n}\n\nfunction andWhere(\n base: AggregateSpec['where'],\n extra: Record<string, unknown>,\n): AggregateSpec['where'] {\n if (Object.keys(extra).length === 0) return base;\n if (!base) return extra;\n if (Array.isArray(base)) {\n return base.map((andGroup) => [...andGroup, extra]);\n }\n return { ...base, ...extra };\n}\n\nfunction andHaving(\n base: AggregateSpec['having'],\n extra: Record<string, unknown>,\n): AggregateSpec['having'] {\n if (Object.keys(extra).length === 0) return base;\n if (!base) return extra;\n if (Array.isArray(base)) {\n return base.map((andGroup) => [...andGroup, extra]);\n }\n return { ...base, ...extra };\n}\n\nfunction withRefreshFilters(\n spec: AggregateSpec,\n scope: RefreshScope,\n extraWhere: Record<string, unknown> = {},\n): AggregateSpec {\n return {\n ...spec,\n where: andWhere(spec.where, {\n ...tenantWhere(scope),\n ...extraWhere,\n }),\n };\n}\n\nasync function resolveWatermarkConfig(\n definition: ReportDefinition,\n): Promise<WatermarkConfig> {\n const configured = definition.refresh ?? {};\n const watermarkField = configured.watermarkColumn ?? 'updatedAt';\n const softDeleteField = configured.softDeleteColumn ?? 'deletedAt';\n const watermarkColumn = await fieldColumn(\n definition.sourceClassName,\n watermarkField,\n );\n const softDeleteColumn = await fieldColumn(\n definition.sourceClassName,\n softDeleteField,\n );\n\n if (!watermarkColumn) {\n throw new Error(\n `Incremental report ${definition.reportClassName} requires source ` +\n `${definition.sourceClassName} to define watermark column '${watermarkField}'.`,\n );\n }\n if (!softDeleteColumn) {\n throw new Error(\n `Incremental report ${definition.reportClassName} requires source ` +\n `${definition.sourceClassName} to define soft-delete column '${softDeleteField}'.`,\n );\n }\n\n return { watermarkColumn, softDeleteColumn };\n}\n\nfunction compileChangedGroupSpec(\n definition: ReportDefinition,\n scope: RefreshScope,\n watermark: WatermarkConfig,\n watermarkBefore: string,\n): AggregateSpec {\n const spec = compileReportDefinition(definition);\n const select = spec.select.filter(\n (expr): expr is AggregateSelectExpr => !('fn' in expr),\n );\n\n return withRefreshFilters(\n {\n ...spec,\n // Ignore the report filter while finding changed groups so rows that\n // just stopped matching `report.where` still trigger a recompute/delete.\n where: undefined,\n select:\n select.length > 0\n ? select\n : [{ fn: 'count', as: '__smrt_changed_count' }],\n groupBy: select.length > 0 ? spec.groupBy : [],\n having: undefined,\n },\n scope,\n { [`${watermark.watermarkColumn} >`]: watermarkBefore },\n );\n}\n\nfunction compileAffectedGroupSpec(\n definition: ReportDefinition,\n scope: RefreshScope,\n watermark: WatermarkConfig,\n groupRow: Record<string, unknown>,\n): AggregateSpec {\n const groupingColumns = getActualGroupingColumns(definition);\n const groupHaving = Object.fromEntries(\n groupingColumns.map((column) => [column, groupRow[column]]),\n );\n const spec = withRefreshFilters(compileReportDefinition(definition), scope, {\n [watermark.softDeleteColumn]: null,\n });\n return {\n ...spec,\n having: andHaving(spec.having, groupHaving),\n };\n}\n\nfunction conflictColumns(\n definition: ReportDefinition,\n scope: RefreshScope,\n): string[] {\n const groupingColumns = getActualGroupingColumns(definition);\n if (groupingColumns.length === 0) return ['id'];\n return [\n ...(scope.reportTenantColumn ? [scope.reportTenantColumn] : []),\n ...groupingColumns,\n ];\n}\n\nfunction predicateSql(\n tableName: string,\n predicate: Record<string, unknown>,\n): { sql: string; values: unknown[] } {\n validateColumnName(tableName);\n const clauses: string[] = [];\n const values: unknown[] = [];\n\n for (const [column, value] of Object.entries(predicate)) {\n validateColumnName(column);\n if (value === null || value === undefined) {\n clauses.push(`${column} IS NULL`);\n continue;\n }\n clauses.push(`${column} = ?`);\n values.push(value);\n }\n\n return {\n sql: clauses.length > 0 ? `WHERE ${clauses.join(' AND ')}` : '',\n values,\n };\n}\n\nfunction dedupeGroupRows(\n definition: ReportDefinition,\n rows: Array<Record<string, unknown> | null>,\n): Record<string, unknown>[] {\n const groupingColumns = getActualGroupingColumns(definition);\n const groups = new Map<string, Record<string, unknown>>();\n\n for (const row of rows) {\n if (!row) continue;\n const groupRow =\n groupingColumns.length === 0\n ? {}\n : Object.fromEntries(\n groupingColumns.map((column) => [column, row[column] ?? null]),\n );\n const key =\n groupingColumns.length === 0\n ? '__smrt_global_group__'\n : JSON.stringify(groupingColumns.map((column) => groupRow[column]));\n if (!groups.has(key)) {\n groups.set(key, groupRow);\n }\n }\n\n return [...groups.values()];\n}\n\nasync function readMaterializedGroups(\n db: DatabaseInterface,\n tableName: string,\n definition: ReportDefinition,\n scope: RefreshScope,\n): Promise<Record<string, unknown>[]> {\n const groupingColumns = getActualGroupingColumns(definition);\n if (groupingColumns.length === 0) {\n return [{}];\n }\n\n const where = scope.reportTenantColumn\n ? predicateSql(tableName, { [scope.reportTenantColumn]: scope.tenantId })\n : { sql: '', values: [] as unknown[] };\n const columns = groupingColumns.map((column) => validateColumnName(column));\n const result = await db.query(\n `SELECT ${columns.join(', ')} FROM ${validateColumnName(tableName)} ${where.sql}`,\n ...where.values,\n );\n\n return result.rows\n .map((row) => parseMaybeJson<Record<string, unknown>>(row))\n .filter((row): row is Record<string, unknown> => Boolean(row));\n}\n\nasync function deleteMaterializedGroup(\n db: DatabaseInterface,\n tableName: string,\n definition: ReportDefinition,\n scope: RefreshScope,\n groupRow: Record<string, unknown>,\n): Promise<void> {\n const predicate: Record<string, unknown> = {};\n if (scope.reportTenantColumn) {\n predicate[scope.reportTenantColumn] = scope.tenantId;\n }\n for (const column of getActualGroupingColumns(definition)) {\n predicate[column] = groupRow[column];\n }\n\n const where = predicateSql(tableName, predicate);\n await db.query(\n `DELETE FROM ${validateColumnName(tableName)} ${where.sql}`,\n ...where.values,\n );\n}\n\nasync function replaceRows(\n db: DatabaseInterface,\n tableName: string,\n rows: Record<string, any>[],\n scope: RefreshScope,\n): Promise<void> {\n validateColumnName(tableName);\n const run = async (tx: DatabaseInterface) => {\n if (scope.reportTenantColumn) {\n const where = predicateSql(tableName, {\n [scope.reportTenantColumn]: scope.tenantId,\n });\n await tx.query(`DELETE FROM ${tableName} ${where.sql}`, ...where.values);\n } else {\n await tx.query(`DELETE FROM ${tableName}`);\n }\n if (rows.length > 0) {\n await tx.insert(tableName, rows);\n }\n };\n\n if (db.transaction) {\n await db.transaction(run);\n return;\n }\n\n await run(db);\n}\n\nfunction parseMaybeJson<T>(value: T | string | null | undefined): T | null {\n if (value == null) return null;\n if (typeof value !== 'string') return value;\n try {\n return JSON.parse(value) as T;\n } catch {\n return value as T;\n }\n}\n\nfunction watermarkValue(value: unknown): string | null {\n if (value === null || value === undefined) return null;\n if (value instanceof Date) return value.toISOString();\n return String(value);\n}\n\nasync function readWatermark(\n db: DatabaseInterface,\n definition: ReportDefinition,\n scope: RefreshScope,\n watermarkColumn: string,\n): Promise<string | null> {\n const result = await db.query(\n `SELECT watermark_value FROM ${REPORT_WATERMARKS_TABLE}\n WHERE report_class = ?\n AND scope_key = ?\n AND source_class = ?\n AND watermark_column = ?\n LIMIT 1`,\n definition.reportClassName,\n scope.scopeKey,\n definition.sourceClassName,\n watermarkColumn,\n );\n const row = result.rows[0] as { watermark_value?: unknown } | undefined;\n return watermarkValue(row?.watermark_value);\n}\n\nasync function writeWatermark(\n db: DatabaseInterface,\n definition: ReportDefinition,\n scope: RefreshScope,\n watermarkColumn: string,\n value: string | null,\n runId: string | undefined,\n): Promise<void> {\n if (value === null) return;\n const id = stableReportId([\n 'watermark',\n definition.reportClassName,\n scope.scopeKey,\n definition.sourceClassName,\n watermarkColumn,\n ]);\n const now = new Date().toISOString();\n await db.upsert(\n REPORT_WATERMARKS_TABLE,\n ['report_class', 'scope_key', 'source_class', 'watermark_column'],\n {\n id,\n slug: id,\n context: scope.scopeKey,\n tenant_id: scope.tenantId,\n scope_key: scope.scopeKey,\n report_class: definition.reportClassName,\n source_class: definition.sourceClassName,\n watermark_column: watermarkColumn,\n watermark_value: value,\n last_run_id: runId ?? null,\n created_at: now,\n updated_at: now,\n },\n );\n}\n\nasync function maxWatermark(\n db: DatabaseInterface,\n definition: ReportDefinition,\n scope: RefreshScope,\n watermarkColumn: string,\n adapterType: SqlAdapterType,\n): Promise<string | null> {\n const where = andWhere(undefined, tenantWhere(scope));\n const builtWhere = where\n ? buildWhere(where, 1, adapterType)\n : { sql: '', values: [] };\n const result = await db.query(\n `SELECT MAX(${validateColumnName(watermarkColumn)}) AS watermark FROM ${validateColumnName(definition.sourceTable)} ${builtWhere.sql}`,\n ...builtWhere.values,\n );\n const row = result.rows[0] as { watermark?: unknown } | undefined;\n return watermarkValue(row?.watermark);\n}\n\nasync function startRun(\n db: DatabaseInterface,\n definition: ReportDefinition,\n scope: RefreshScope,\n options: ReportRefreshOptions,\n mode: ReportRefreshMode,\n watermarkBefore?: string | null,\n): Promise<string | undefined> {\n if (options.trackRuns === false) return undefined;\n const id = randomUUID();\n const now = new Date().toISOString();\n await db.insert(REPORT_RUNS_TABLE, {\n id,\n slug: id,\n context: scope.scopeKey,\n tenant_id: scope.tenantId,\n scope_key: scope.scopeKey,\n report_class: definition.reportClassName,\n source_class: definition.sourceClassName,\n mode,\n trigger: options.trigger ?? 'manual',\n status: 'running',\n started_at: now,\n row_count: 0,\n changed_group_count: 0,\n watermark_before: watermarkBefore ?? null,\n watermark_after: null,\n error: null,\n metadata: {\n scheduleId: options.scheduleId,\n },\n created_at: now,\n updated_at: now,\n });\n return id;\n}\n\nasync function completeRun(\n db: DatabaseInterface,\n runId: string | undefined,\n status: 'success' | 'failed' | 'skipped',\n data: {\n rowCount?: number;\n changedGroupCount?: number;\n watermarkAfter?: string | null;\n error?: unknown;\n } = {},\n): Promise<void> {\n if (!runId) return;\n const error =\n data.error instanceof Error\n ? data.error.message\n : data.error\n ? String(data.error)\n : null;\n await db.update(\n REPORT_RUNS_TABLE,\n { id: runId },\n {\n status,\n completed_at: new Date().toISOString(),\n row_count: data.rowCount ?? 0,\n changed_group_count: data.changedGroupCount ?? 0,\n watermark_after: data.watermarkAfter ?? null,\n error,\n updated_at: new Date().toISOString(),\n },\n );\n}\n\nasync function acquireLock(\n db: DatabaseInterface,\n definition: ReportDefinition,\n scope: RefreshScope,\n ttlMs: number,\n): Promise<ReportLockHandle | null> {\n const now = new Date();\n const nowIso = now.toISOString();\n const ownerId = randomUUID();\n const expiresAt = new Date(now.getTime() + ttlMs);\n const expiresAtIso = expiresAt.toISOString();\n const id = stableReportId([\n 'lock',\n definition.reportClassName,\n scope.scopeKey,\n ]);\n await db.query(\n `INSERT INTO ${REPORT_LOCKS_TABLE} (\n id,\n slug,\n context,\n tenant_id,\n scope_key,\n report_class,\n owner_id,\n acquired_at,\n heartbeat_at,\n expires_at,\n created_at,\n updated_at\n )\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT(report_class, scope_key) DO UPDATE SET\n tenant_id = excluded.tenant_id,\n owner_id = excluded.owner_id,\n acquired_at = excluded.acquired_at,\n heartbeat_at = excluded.heartbeat_at,\n expires_at = excluded.expires_at,\n updated_at = excluded.updated_at\n WHERE ${REPORT_LOCKS_TABLE}.owner_id IS NULL\n OR ${REPORT_LOCKS_TABLE}.expires_at IS NULL\n OR ${REPORT_LOCKS_TABLE}.expires_at <= ?`,\n id,\n id,\n scope.scopeKey,\n scope.tenantId,\n scope.scopeKey,\n definition.reportClassName,\n ownerId,\n nowIso,\n nowIso,\n expiresAtIso,\n nowIso,\n nowIso,\n nowIso,\n );\n\n const claimed = await db.query(\n `SELECT owner_id FROM ${REPORT_LOCKS_TABLE}\n WHERE report_class = ? AND scope_key = ?\n LIMIT 1`,\n definition.reportClassName,\n scope.scopeKey,\n );\n const row = claimed.rows[0] as { owner_id?: unknown } | undefined;\n if (row?.owner_id !== ownerId) {\n return null;\n }\n\n return {\n ownerId,\n async release() {\n await db.query(\n `UPDATE ${REPORT_LOCKS_TABLE}\n SET owner_id = NULL,\n heartbeat_at = NULL,\n expires_at = NULL,\n updated_at = ?\n WHERE report_class = ?\n AND scope_key = ?\n AND owner_id = ?`,\n new Date().toISOString(),\n definition.reportClassName,\n scope.scopeKey,\n ownerId,\n );\n },\n };\n}\n\nasync function rebuildReport(\n db: DatabaseInterface,\n definition: ReportDefinition,\n reportTable: string,\n scope: RefreshScope,\n adapterType: SqlAdapterType,\n runId: string | undefined,\n watermark?: WatermarkConfig | null,\n): Promise<RefreshWorkResult> {\n const explicitWatermarkColumn =\n definition.refresh?.watermarkColumn &&\n (await fieldColumn(\n definition.sourceClassName,\n definition.refresh.watermarkColumn,\n ));\n const explicitSoftDeleteColumn =\n definition.refresh?.softDeleteColumn &&\n (await fieldColumn(\n definition.sourceClassName,\n definition.refresh.softDeleteColumn,\n ));\n const watermarkColumn =\n watermark?.watermarkColumn ?? explicitWatermarkColumn ?? null;\n const softDeleteColumn =\n watermark?.softDeleteColumn ?? explicitSoftDeleteColumn ?? null;\n const deletedFilter = softDeleteColumn ? { [softDeleteColumn]: null } : {};\n const spec = withRefreshFilters(\n compileReportDefinition(definition),\n scope,\n deletedFilter,\n );\n const aggregate = buildAggregate(spec, 1, adapterType);\n const result = await db.query(aggregate.sql, ...aggregate.values);\n const refreshedAt = new Date();\n const rows = materializeRows(result.rows, definition, refreshedAt, scope);\n\n await replaceRows(db, reportTable, rows, scope);\n\n const watermarkAfter = watermarkColumn\n ? await maxWatermark(db, definition, scope, watermarkColumn, adapterType)\n : null;\n if (watermarkColumn) {\n await writeWatermark(\n db,\n definition,\n scope,\n watermarkColumn,\n watermarkAfter,\n runId,\n );\n }\n\n return {\n rowCount: rows.length,\n changedGroupCount: rows.length,\n watermarkAfter,\n mode: 'rebuild',\n };\n}\n\nasync function incrementalReport(\n db: DatabaseInterface,\n definition: ReportDefinition,\n reportTable: string,\n scope: RefreshScope,\n adapterType: SqlAdapterType,\n runId: string | undefined,\n changedRows: Record<string, unknown>[] = [],\n): Promise<RefreshWorkResult> {\n const watermark = await resolveWatermarkConfig(definition);\n const watermarkBefore = await readWatermark(\n db,\n definition,\n scope,\n watermark.watermarkColumn,\n );\n\n if (!watermarkBefore) {\n const seeded = await rebuildReport(\n db,\n definition,\n reportTable,\n scope,\n adapterType,\n runId,\n watermark,\n );\n return {\n ...seeded,\n watermarkBefore,\n };\n }\n\n const changedSpec = compileChangedGroupSpec(\n definition,\n scope,\n watermark,\n watermarkBefore,\n );\n const changedAggregate = buildAggregate(changedSpec, 1, adapterType);\n const changed = await db.query(\n changedAggregate.sql,\n ...changedAggregate.values,\n );\n const changedGroups = changed.rows.map((row) =>\n parseMaybeJson<Record<string, unknown>>(row),\n );\n const materializedGroups =\n changedGroups.length > 0 || changedRows.length > 0\n ? await readMaterializedGroups(db, reportTable, definition, scope)\n : [];\n const affectedGroups = dedupeGroupRows(definition, [\n ...changedGroups,\n ...materializedGroups,\n ]);\n\n if (affectedGroups.length === 0) {\n return {\n rowCount: 0,\n changedGroupCount: 0,\n watermarkBefore,\n watermarkAfter: watermarkBefore,\n mode: 'incremental',\n };\n }\n\n let rowCount = 0;\n const conflicts = conflictColumns(definition, scope);\n\n for (const groupRow of affectedGroups) {\n const affectedSpec = compileAffectedGroupSpec(\n definition,\n scope,\n watermark,\n groupRow,\n );\n const aggregate = buildAggregate(affectedSpec, 1, adapterType);\n const result = await db.query(aggregate.sql, ...aggregate.values);\n const refreshedAt = new Date();\n const rows = materializeRows(result.rows, definition, refreshedAt, scope);\n\n if (rows.length === 0) {\n await deleteMaterializedGroup(\n db,\n reportTable,\n definition,\n scope,\n groupRow,\n );\n continue;\n }\n\n for (const row of rows) {\n await db.upsert(reportTable, conflicts, row);\n rowCount += 1;\n }\n }\n\n const watermarkAfter = await maxWatermark(\n db,\n definition,\n scope,\n watermark.watermarkColumn,\n adapterType,\n );\n await writeWatermark(\n db,\n definition,\n scope,\n watermark.watermarkColumn,\n watermarkAfter,\n runId,\n );\n\n return {\n rowCount,\n changedGroupCount: affectedGroups.length,\n watermarkBefore,\n watermarkAfter,\n mode: 'incremental',\n };\n}\n\nfunction requiredReportRuntimeTables(\n mode: ReportRefreshMode,\n options: ReportRefreshOptions,\n): string[] {\n const tables = new Set<string>();\n if (options.trackRuns !== false) tables.add(REPORT_RUNS_TABLE);\n if (mode === 'incremental') tables.add(REPORT_WATERMARKS_TABLE);\n if (options.lock !== false) tables.add(REPORT_LOCKS_TABLE);\n return [...tables];\n}\n\nasync function refreshReportOnce(\n reportCtor: ReportCtor,\n options: ReportRefreshOptions,\n): Promise<ReportRefreshResult> {\n if (!options.db) {\n throw new Error('refreshReport requires a database handle');\n }\n\n const definition = await buildReportDefinition(reportCtor);\n const reportTable = getReportTableName(reportCtor);\n const requestedMode = options.mode ?? definition.refresh?.mode ?? 'rebuild';\n const exists = await tableExists(options.db, reportTable);\n if (!exists) {\n throw new Error(\n `Report table '${reportTable}' does not exist for ${reportCtor.name}. ` +\n `Run smrt db:migrate before refreshing reports.`,\n );\n }\n\n const requiredTables = requiredReportRuntimeTables(requestedMode, options);\n if (requiredTables.length > 0) {\n await assertReportTablesReady(options.db, requiredTables);\n }\n\n const adapterType = adapterTypeFromDb(options.db, options.adapterType);\n const scope = await resolveScope(definition, options);\n const lock =\n options.lock === false\n ? null\n : await acquireLock(\n options.db,\n definition,\n scope,\n options.lockTtlMs ?? 15 * 60 * 1000,\n );\n\n if (options.lock !== false && !lock) {\n const runId = await startRun(\n options.db,\n definition,\n scope,\n options,\n requestedMode,\n );\n await completeRun(options.db, runId, 'skipped');\n return {\n rowCount: 0,\n refreshedAt: new Date(),\n mode: requestedMode,\n tenantId: scope.tenantId,\n runId,\n changedGroupCount: 0,\n skipped: true,\n };\n }\n\n let runId: string | undefined;\n try {\n runId = await startRun(\n options.db,\n definition,\n scope,\n options,\n requestedMode,\n );\n const work =\n requestedMode === 'incremental'\n ? await incrementalReport(\n options.db,\n definition,\n reportTable,\n scope,\n adapterType,\n runId,\n options.changedRows,\n )\n : await rebuildReport(\n options.db,\n definition,\n reportTable,\n scope,\n adapterType,\n runId,\n );\n\n await completeRun(options.db, runId, 'success', {\n rowCount: work.rowCount,\n changedGroupCount: work.changedGroupCount,\n watermarkAfter: work.watermarkAfter,\n });\n\n return {\n rowCount: work.rowCount,\n refreshedAt: new Date(),\n mode: work.mode,\n tenantId: scope.tenantId,\n runId,\n changedGroupCount: work.changedGroupCount,\n };\n } catch (error) {\n await completeRun(options.db, runId, 'failed', { error });\n throw error;\n } finally {\n await lock?.release();\n }\n}\n\nexport async function refreshReport(\n reportCtor: ReportCtor,\n options: ReportRefreshOptions = {},\n): Promise<ReportRefreshResult> {\n if (options.tenantIds && options.tenantIds.length > 0) {\n if (!options.db) {\n throw new Error('refreshReport requires a database handle');\n }\n const results: ReportRefreshResult[] = [];\n for (const tenantId of options.tenantIds) {\n const result = await withTenant({ tenantId }, () =>\n refreshReportOnce(reportCtor, {\n ...options,\n tenantId,\n tenantIds: undefined,\n }),\n );\n results.push(result);\n }\n return {\n rowCount: results.reduce((sum, result) => sum + result.rowCount, 0),\n refreshedAt: new Date(),\n mode: results[0]?.mode ?? options.mode ?? 'rebuild',\n tenantId: null,\n changedGroupCount: results.reduce(\n (sum, result) => sum + (result.changedGroupCount ?? 0),\n 0,\n ),\n tenantResults: results,\n };\n }\n\n return refreshReportOnce(reportCtor, options);\n}\n\nexport function reportRowIdentity(\n row: Record<string, any>,\n definition: ReportDefinition,\n): string {\n const groupingColumns = getReportGroupingColumns(definition);\n return stableReportId(\n groupingColumns.map((column) => row[toSnakeCase(column)]),\n );\n}\n\nexport function reportSourceClassName(source: ReportSource): string {\n return sourceClassName(source);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"refresh.js","names":["runId"],"sources":["../src/refresh.ts"],"sourcesContent":["import { createHash, randomUUID } from 'node:crypto';\nimport { ObjectRegistry, type SmrtObject } from '@happyvertical/smrt-core';\nimport { toSnakeCase } from '@happyvertical/smrt-core/utils';\nimport { getTenantId, withTenant } from '@happyvertical/smrt-tenancy';\nimport {\n buildAggregate,\n buildWhere,\n type DatabaseInterface,\n type SqlAdapterType,\n tableExists,\n validateColumnName,\n} from '@happyvertical/sql';\nimport {\n buildReportDefinition,\n compileReportDefinition,\n getReportGroupingColumns,\n} from './compiler.js';\nimport {\n assertReportTablesReady,\n REPORT_LOCKS_TABLE,\n REPORT_RUNS_TABLE,\n REPORT_WATERMARKS_TABLE,\n scopeKeyForTenant,\n} from './state.js';\nimport type {\n AggregateSelectExpr,\n AggregateSpec,\n ReportDefinition,\n ReportRefreshMode,\n ReportRefreshOptions,\n ReportRefreshResult,\n ReportSource,\n} from './types.js';\n\ntype ReportCtor = new (...args: any[]) => SmrtObject;\n\ninterface RefreshScope {\n tenantId: string | null;\n scopeKey: string;\n sourceTenantColumn: string | null;\n reportTenantColumn: string | null;\n}\n\ninterface WatermarkConfig {\n watermarkColumn: string;\n softDeleteColumn: string;\n}\n\ninterface RefreshWorkResult {\n rowCount: number;\n changedGroupCount: number;\n watermarkBefore?: string | null;\n watermarkAfter?: string | null;\n mode: ReportRefreshMode;\n}\n\ninterface ReportLockHandle {\n ownerId: string;\n release(): Promise<void>;\n}\n\nfunction isSqlAdapterType(value: unknown): value is SqlAdapterType {\n return (\n value === 'sqlite' ||\n value === 'postgres' ||\n value === 'duckdb' ||\n value === 'json'\n );\n}\n\nexport function adapterTypeFromDb(\n db: DatabaseInterface,\n fallback?: SqlAdapterType,\n): SqlAdapterType {\n if (isSqlAdapterType(fallback)) return fallback;\n\n const url = String(db.url ?? '');\n if (url.startsWith('postgres://') || url.startsWith('postgresql://')) {\n return 'postgres';\n }\n if (url.endsWith('.duckdb')) {\n return 'duckdb';\n }\n if (url === ':memory:' || url.endsWith('.sqlite') || url.endsWith('.db')) {\n return 'sqlite';\n }\n throw new Error(\n 'Report refresh requires a database adapter type. Pass { adapterType }.',\n );\n}\n\nfunction stableReportId(values: unknown[]): string {\n const hash = createHash('sha256')\n .update(JSON.stringify(values))\n .digest('hex');\n const variant = ((Number.parseInt(hash[16], 16) & 0x3) | 0x8).toString(16);\n return [\n hash.slice(0, 8),\n hash.slice(8, 12),\n `4${hash.slice(13, 16)}`,\n `${variant}${hash.slice(17, 20)}`,\n hash.slice(20, 32),\n ].join('-');\n}\n\nfunction getReportTableName(reportCtor: ReportCtor): string {\n const registered =\n ObjectRegistry.getClassByConstructor(reportCtor) ??\n ObjectRegistry.getClass(reportCtor.name);\n const tableName = registered\n ? ObjectRegistry.getTableName(registered.qualifiedName ?? registered.name)\n : ObjectRegistry.getTableName(reportCtor.name);\n if (!tableName) {\n throw new Error(`No report table registered for ${reportCtor.name}`);\n }\n return tableName;\n}\n\nfunction getActualGroupingColumns(\n definition: Pick<ReportDefinition, 'fields'>,\n): string[] {\n return definition.fields\n .filter(\n (field) =>\n field.report?.kind === 'group' || field.report?.kind === 'bucket',\n )\n .map((field) => field.columnName ?? toSnakeCase(field.fieldName));\n}\n\nfunction materializeRows(\n rows: Record<string, any>[],\n definition: ReportDefinition,\n refreshedAt: Date,\n scope: RefreshScope,\n): Record<string, any>[] {\n const groupingColumns = getActualGroupingColumns(definition);\n const now = refreshedAt.toISOString();\n\n return rows.map((row, index) => {\n const groupingValues =\n groupingColumns.length === 0\n ? [definition.reportClassName, scope.scopeKey, index]\n : [\n definition.reportClassName,\n scope.scopeKey,\n ...groupingColumns.map((column) => row[column]),\n ];\n const id = stableReportId(groupingValues);\n const tenantFields = scope.reportTenantColumn\n ? { [scope.reportTenantColumn]: scope.tenantId }\n : {};\n\n return {\n id,\n slug: id,\n context: scope.scopeKey,\n ...tenantFields,\n ...row,\n refreshed_at: now,\n created_at: now,\n updated_at: now,\n };\n });\n}\n\nfunction sourceClassName(source: ReportSource): string {\n if (typeof source === 'string') return source;\n return source.name;\n}\n\nasync function fieldColumn(\n className: string,\n requested: string,\n): Promise<string | null> {\n const fields = await ObjectRegistry.getAllFields(className);\n const requestedColumn = toSnakeCase(requested);\n\n for (const [fieldName] of fields.entries()) {\n if (fieldName === requested || toSnakeCase(fieldName) === requestedColumn) {\n return requestedColumn;\n }\n }\n\n return null;\n}\n\nexport async function tenantColumn(className: string): Promise<string | null> {\n const registered = ObjectRegistry.getClass(className);\n const configuredField = registered?.tenantScopedConfig?.field;\n if (configuredField) {\n return toSnakeCase(configuredField);\n }\n\n const fields = await ObjectRegistry.getAllFields(className);\n for (const [fieldName, field] of fields.entries()) {\n if (field?._meta?.__tenancy?.isTenantIdField) {\n return toSnakeCase(fieldName);\n }\n }\n\n const tenantIdField = await fieldColumn(className, 'tenantId');\n return tenantIdField;\n}\n\nasync function resolveScope(\n definition: ReportDefinition,\n options: ReportRefreshOptions,\n): Promise<RefreshScope> {\n const tenantId =\n options.tenantId !== undefined ? options.tenantId : (getTenantId() ?? null);\n const sourceTenantColumn = await tenantColumn(definition.sourceClassName);\n const reportTenantColumn = await tenantColumn(definition.reportClassName);\n\n if (tenantId && !sourceTenantColumn) {\n throw new Error(\n `Tenant-scoped refresh for ${definition.reportClassName} requires source ` +\n `${definition.sourceClassName} to expose a tenantId field.`,\n );\n }\n if (tenantId && !reportTenantColumn) {\n throw new Error(\n `Tenant-scoped refresh for ${definition.reportClassName} requires the report table to expose a tenantId field.`,\n );\n }\n\n return {\n tenantId,\n scopeKey: scopeKeyForTenant(tenantId),\n sourceTenantColumn,\n reportTenantColumn,\n };\n}\n\nfunction tenantWhere(scope: RefreshScope): Record<string, unknown> {\n if (!scope.sourceTenantColumn) return {};\n return { [scope.sourceTenantColumn]: scope.tenantId };\n}\n\nfunction andWhere(\n base: AggregateSpec['where'],\n extra: Record<string, unknown>,\n): AggregateSpec['where'] {\n if (Object.keys(extra).length === 0) return base;\n if (!base) return extra;\n if (Array.isArray(base)) {\n return base.map((andGroup) => [...andGroup, extra]);\n }\n return { ...base, ...extra };\n}\n\nfunction andHaving(\n base: AggregateSpec['having'],\n extra: Record<string, unknown>,\n): AggregateSpec['having'] {\n if (Object.keys(extra).length === 0) return base;\n if (!base) return extra;\n if (Array.isArray(base)) {\n return base.map((andGroup) => [...andGroup, extra]);\n }\n return { ...base, ...extra };\n}\n\nfunction withRefreshFilters(\n spec: AggregateSpec,\n scope: RefreshScope,\n extraWhere: Record<string, unknown> = {},\n): AggregateSpec {\n return {\n ...spec,\n where: andWhere(spec.where, {\n ...tenantWhere(scope),\n ...extraWhere,\n }),\n };\n}\n\nasync function resolveWatermarkConfig(\n definition: ReportDefinition,\n): Promise<WatermarkConfig> {\n const configured = definition.refresh ?? {};\n const watermarkField = configured.watermarkColumn ?? 'updatedAt';\n const softDeleteField = configured.softDeleteColumn ?? 'deletedAt';\n const watermarkColumn = await fieldColumn(\n definition.sourceClassName,\n watermarkField,\n );\n const softDeleteColumn = await fieldColumn(\n definition.sourceClassName,\n softDeleteField,\n );\n\n if (!watermarkColumn) {\n throw new Error(\n `Incremental report ${definition.reportClassName} requires source ` +\n `${definition.sourceClassName} to define watermark column '${watermarkField}'.`,\n );\n }\n if (!softDeleteColumn) {\n throw new Error(\n `Incremental report ${definition.reportClassName} requires source ` +\n `${definition.sourceClassName} to define soft-delete column '${softDeleteField}'.`,\n );\n }\n\n return { watermarkColumn, softDeleteColumn };\n}\n\nfunction compileChangedGroupSpec(\n definition: ReportDefinition,\n scope: RefreshScope,\n watermark: WatermarkConfig,\n watermarkBefore: string,\n): AggregateSpec {\n const spec = compileReportDefinition(definition);\n const select = spec.select.filter(\n (expr): expr is AggregateSelectExpr => !('fn' in expr),\n );\n\n return withRefreshFilters(\n {\n ...spec,\n // Ignore the report filter while finding changed groups so rows that\n // just stopped matching `report.where` still trigger a recompute/delete.\n where: undefined,\n select:\n select.length > 0\n ? select\n : [{ fn: 'count', as: '__smrt_changed_count' }],\n groupBy: select.length > 0 ? spec.groupBy : [],\n having: undefined,\n },\n scope,\n { [`${watermark.watermarkColumn} >`]: watermarkBefore },\n );\n}\n\nfunction compileAffectedGroupSpec(\n definition: ReportDefinition,\n scope: RefreshScope,\n watermark: WatermarkConfig,\n groupRow: Record<string, unknown>,\n): AggregateSpec {\n const groupingColumns = getActualGroupingColumns(definition);\n const groupHaving = Object.fromEntries(\n groupingColumns.map((column) => [column, groupRow[column]]),\n );\n const spec = withRefreshFilters(compileReportDefinition(definition), scope, {\n [watermark.softDeleteColumn]: null,\n });\n return {\n ...spec,\n having: andHaving(spec.having, groupHaving),\n };\n}\n\nfunction conflictColumns(\n definition: ReportDefinition,\n scope: RefreshScope,\n): string[] {\n const groupingColumns = getActualGroupingColumns(definition);\n if (groupingColumns.length === 0) return ['id'];\n return [\n ...(scope.reportTenantColumn ? [scope.reportTenantColumn] : []),\n ...groupingColumns,\n ];\n}\n\nfunction predicateSql(\n tableName: string,\n predicate: Record<string, unknown>,\n): { sql: string; values: unknown[] } {\n validateColumnName(tableName);\n const clauses: string[] = [];\n const values: unknown[] = [];\n\n for (const [column, value] of Object.entries(predicate)) {\n validateColumnName(column);\n if (value === null || value === undefined) {\n clauses.push(`${column} IS NULL`);\n continue;\n }\n clauses.push(`${column} = ?`);\n values.push(value);\n }\n\n return {\n sql: clauses.length > 0 ? `WHERE ${clauses.join(' AND ')}` : '',\n values,\n };\n}\n\nfunction dedupeGroupRows(\n definition: ReportDefinition,\n rows: Array<Record<string, unknown> | null>,\n): Record<string, unknown>[] {\n const groupingColumns = getActualGroupingColumns(definition);\n const groups = new Map<string, Record<string, unknown>>();\n\n for (const row of rows) {\n if (!row) continue;\n const groupRow =\n groupingColumns.length === 0\n ? {}\n : Object.fromEntries(\n groupingColumns.map((column) => [column, row[column] ?? null]),\n );\n const key =\n groupingColumns.length === 0\n ? '__smrt_global_group__'\n : JSON.stringify(groupingColumns.map((column) => groupRow[column]));\n if (!groups.has(key)) {\n groups.set(key, groupRow);\n }\n }\n\n return [...groups.values()];\n}\n\nasync function readMaterializedGroups(\n db: DatabaseInterface,\n tableName: string,\n definition: ReportDefinition,\n scope: RefreshScope,\n): Promise<Record<string, unknown>[]> {\n const groupingColumns = getActualGroupingColumns(definition);\n if (groupingColumns.length === 0) {\n return [{}];\n }\n\n const where = scope.reportTenantColumn\n ? predicateSql(tableName, { [scope.reportTenantColumn]: scope.tenantId })\n : { sql: '', values: [] as unknown[] };\n const columns = groupingColumns.map((column) => validateColumnName(column));\n const result = await db.query(\n `SELECT ${columns.join(', ')} FROM ${validateColumnName(tableName)} ${where.sql}`,\n ...where.values,\n );\n\n return result.rows\n .map((row) => parseMaybeJson<Record<string, unknown>>(row))\n .filter((row): row is Record<string, unknown> => Boolean(row));\n}\n\nasync function deleteMaterializedGroup(\n db: DatabaseInterface,\n tableName: string,\n definition: ReportDefinition,\n scope: RefreshScope,\n groupRow: Record<string, unknown>,\n): Promise<void> {\n const predicate: Record<string, unknown> = {};\n if (scope.reportTenantColumn) {\n predicate[scope.reportTenantColumn] = scope.tenantId;\n }\n for (const column of getActualGroupingColumns(definition)) {\n predicate[column] = groupRow[column];\n }\n\n const where = predicateSql(tableName, predicate);\n await db.query(\n `DELETE FROM ${validateColumnName(tableName)} ${where.sql}`,\n ...where.values,\n );\n}\n\nasync function replaceRows(\n db: DatabaseInterface,\n tableName: string,\n rows: Record<string, any>[],\n scope: RefreshScope,\n): Promise<void> {\n validateColumnName(tableName);\n const run = async (tx: DatabaseInterface) => {\n if (scope.reportTenantColumn) {\n const where = predicateSql(tableName, {\n [scope.reportTenantColumn]: scope.tenantId,\n });\n await tx.query(`DELETE FROM ${tableName} ${where.sql}`, ...where.values);\n } else {\n await tx.query(`DELETE FROM ${tableName}`);\n }\n if (rows.length > 0) {\n await tx.insert(tableName, rows);\n }\n };\n\n if (db.transaction) {\n await db.transaction(run);\n return;\n }\n\n await run(db);\n}\n\nfunction parseMaybeJson<T>(value: T | string | null | undefined): T | null {\n if (value == null) return null;\n if (typeof value !== 'string') return value;\n try {\n return JSON.parse(value) as T;\n } catch {\n return value as T;\n }\n}\n\nfunction watermarkValue(value: unknown): string | null {\n if (value === null || value === undefined) return null;\n if (value instanceof Date) return value.toISOString();\n return String(value);\n}\n\nasync function readWatermark(\n db: DatabaseInterface,\n definition: ReportDefinition,\n scope: RefreshScope,\n watermarkColumn: string,\n): Promise<string | null> {\n const result = await db.query(\n `SELECT watermark_value FROM ${REPORT_WATERMARKS_TABLE}\n WHERE report_class = ?\n AND scope_key = ?\n AND source_class = ?\n AND watermark_column = ?\n LIMIT 1`,\n definition.reportClassName,\n scope.scopeKey,\n definition.sourceClassName,\n watermarkColumn,\n );\n const row = result.rows[0] as { watermark_value?: unknown } | undefined;\n return watermarkValue(row?.watermark_value);\n}\n\nasync function writeWatermark(\n db: DatabaseInterface,\n definition: ReportDefinition,\n scope: RefreshScope,\n watermarkColumn: string,\n value: string | null,\n runId: string | undefined,\n): Promise<void> {\n if (value === null) return;\n const id = stableReportId([\n 'watermark',\n definition.reportClassName,\n scope.scopeKey,\n definition.sourceClassName,\n watermarkColumn,\n ]);\n const now = new Date().toISOString();\n await db.upsert(\n REPORT_WATERMARKS_TABLE,\n ['report_class', 'scope_key', 'source_class', 'watermark_column'],\n {\n id,\n slug: id,\n context: scope.scopeKey,\n tenant_id: scope.tenantId,\n scope_key: scope.scopeKey,\n report_class: definition.reportClassName,\n source_class: definition.sourceClassName,\n watermark_column: watermarkColumn,\n watermark_value: value,\n last_run_id: runId ?? null,\n created_at: now,\n updated_at: now,\n },\n );\n}\n\nasync function maxWatermark(\n db: DatabaseInterface,\n definition: ReportDefinition,\n scope: RefreshScope,\n watermarkColumn: string,\n adapterType: SqlAdapterType,\n): Promise<string | null> {\n const where = andWhere(undefined, tenantWhere(scope));\n const builtWhere = where\n ? buildWhere(where, 1, adapterType)\n : { sql: '', values: [] };\n const result = await db.query(\n `SELECT MAX(${validateColumnName(watermarkColumn)}) AS watermark FROM ${validateColumnName(definition.sourceTable)} ${builtWhere.sql}`,\n ...builtWhere.values,\n );\n const row = result.rows[0] as { watermark?: unknown } | undefined;\n return watermarkValue(row?.watermark);\n}\n\nasync function startRun(\n db: DatabaseInterface,\n definition: ReportDefinition,\n scope: RefreshScope,\n options: ReportRefreshOptions,\n mode: ReportRefreshMode,\n watermarkBefore?: string | null,\n): Promise<string | undefined> {\n if (options.trackRuns === false) return undefined;\n const id = randomUUID();\n const now = new Date().toISOString();\n await db.insert(REPORT_RUNS_TABLE, {\n id,\n slug: id,\n context: scope.scopeKey,\n tenant_id: scope.tenantId,\n scope_key: scope.scopeKey,\n report_class: definition.reportClassName,\n source_class: definition.sourceClassName,\n mode,\n trigger: options.trigger ?? 'manual',\n status: 'running',\n started_at: now,\n row_count: 0,\n changed_group_count: 0,\n watermark_before: watermarkBefore ?? null,\n watermark_after: null,\n error: null,\n metadata: {\n scheduleId: options.scheduleId,\n },\n created_at: now,\n updated_at: now,\n });\n return id;\n}\n\nasync function completeRun(\n db: DatabaseInterface,\n runId: string | undefined,\n status: 'success' | 'failed' | 'skipped',\n data: {\n rowCount?: number;\n changedGroupCount?: number;\n watermarkAfter?: string | null;\n error?: unknown;\n } = {},\n): Promise<void> {\n if (!runId) return;\n const error =\n data.error instanceof Error\n ? data.error.message\n : data.error\n ? String(data.error)\n : null;\n await db.update(\n REPORT_RUNS_TABLE,\n { id: runId },\n {\n status,\n completed_at: new Date().toISOString(),\n row_count: data.rowCount ?? 0,\n changed_group_count: data.changedGroupCount ?? 0,\n watermark_after: data.watermarkAfter ?? null,\n error,\n updated_at: new Date().toISOString(),\n },\n );\n}\n\nasync function acquireLock(\n db: DatabaseInterface,\n definition: ReportDefinition,\n scope: RefreshScope,\n ttlMs: number,\n): Promise<ReportLockHandle | null> {\n const now = new Date();\n const nowIso = now.toISOString();\n const ownerId = randomUUID();\n const expiresAt = new Date(now.getTime() + ttlMs);\n const expiresAtIso = expiresAt.toISOString();\n const id = stableReportId([\n 'lock',\n definition.reportClassName,\n scope.scopeKey,\n ]);\n await db.query(\n `INSERT INTO ${REPORT_LOCKS_TABLE} (\n id,\n slug,\n context,\n tenant_id,\n scope_key,\n report_class,\n owner_id,\n acquired_at,\n heartbeat_at,\n expires_at,\n created_at,\n updated_at\n )\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT(report_class, scope_key) DO UPDATE SET\n tenant_id = excluded.tenant_id,\n owner_id = excluded.owner_id,\n acquired_at = excluded.acquired_at,\n heartbeat_at = excluded.heartbeat_at,\n expires_at = excluded.expires_at,\n updated_at = excluded.updated_at\n WHERE ${REPORT_LOCKS_TABLE}.owner_id IS NULL\n OR ${REPORT_LOCKS_TABLE}.expires_at IS NULL\n OR ${REPORT_LOCKS_TABLE}.expires_at <= ?`,\n id,\n id,\n scope.scopeKey,\n scope.tenantId,\n scope.scopeKey,\n definition.reportClassName,\n ownerId,\n nowIso,\n nowIso,\n expiresAtIso,\n nowIso,\n nowIso,\n nowIso,\n );\n\n const claimed = await db.query(\n `SELECT owner_id FROM ${REPORT_LOCKS_TABLE}\n WHERE report_class = ? AND scope_key = ?\n LIMIT 1`,\n definition.reportClassName,\n scope.scopeKey,\n );\n const row = claimed.rows[0] as { owner_id?: unknown } | undefined;\n if (row?.owner_id !== ownerId) {\n return null;\n }\n\n return {\n ownerId,\n async release() {\n await db.query(\n `UPDATE ${REPORT_LOCKS_TABLE}\n SET owner_id = NULL,\n heartbeat_at = NULL,\n expires_at = NULL,\n updated_at = ?\n WHERE report_class = ?\n AND scope_key = ?\n AND owner_id = ?`,\n new Date().toISOString(),\n definition.reportClassName,\n scope.scopeKey,\n ownerId,\n );\n },\n };\n}\n\nasync function rebuildReport(\n db: DatabaseInterface,\n definition: ReportDefinition,\n reportTable: string,\n scope: RefreshScope,\n adapterType: SqlAdapterType,\n runId: string | undefined,\n watermark?: WatermarkConfig | null,\n): Promise<RefreshWorkResult> {\n const explicitWatermarkColumn =\n definition.refresh?.watermarkColumn &&\n (await fieldColumn(\n definition.sourceClassName,\n definition.refresh.watermarkColumn,\n ));\n const explicitSoftDeleteColumn =\n definition.refresh?.softDeleteColumn &&\n (await fieldColumn(\n definition.sourceClassName,\n definition.refresh.softDeleteColumn,\n ));\n const watermarkColumn =\n watermark?.watermarkColumn ?? explicitWatermarkColumn ?? null;\n const softDeleteColumn =\n watermark?.softDeleteColumn ?? explicitSoftDeleteColumn ?? null;\n const deletedFilter = softDeleteColumn ? { [softDeleteColumn]: null } : {};\n const spec = withRefreshFilters(\n compileReportDefinition(definition),\n scope,\n deletedFilter,\n );\n const aggregate = buildAggregate(spec, 1, adapterType);\n const result = await db.query(aggregate.sql, ...aggregate.values);\n const refreshedAt = new Date();\n const rows = materializeRows(result.rows, definition, refreshedAt, scope);\n\n await replaceRows(db, reportTable, rows, scope);\n\n const watermarkAfter = watermarkColumn\n ? await maxWatermark(db, definition, scope, watermarkColumn, adapterType)\n : null;\n if (watermarkColumn) {\n await writeWatermark(\n db,\n definition,\n scope,\n watermarkColumn,\n watermarkAfter,\n runId,\n );\n }\n\n return {\n rowCount: rows.length,\n changedGroupCount: rows.length,\n watermarkAfter,\n mode: 'rebuild',\n };\n}\n\nasync function incrementalReport(\n db: DatabaseInterface,\n definition: ReportDefinition,\n reportTable: string,\n scope: RefreshScope,\n adapterType: SqlAdapterType,\n runId: string | undefined,\n changedRows: Record<string, unknown>[] = [],\n): Promise<RefreshWorkResult> {\n const watermark = await resolveWatermarkConfig(definition);\n const watermarkBefore = await readWatermark(\n db,\n definition,\n scope,\n watermark.watermarkColumn,\n );\n\n if (!watermarkBefore) {\n const seeded = await rebuildReport(\n db,\n definition,\n reportTable,\n scope,\n adapterType,\n runId,\n watermark,\n );\n return {\n ...seeded,\n watermarkBefore,\n };\n }\n\n const changedSpec = compileChangedGroupSpec(\n definition,\n scope,\n watermark,\n watermarkBefore,\n );\n const changedAggregate = buildAggregate(changedSpec, 1, adapterType);\n const changed = await db.query(\n changedAggregate.sql,\n ...changedAggregate.values,\n );\n const changedGroups = changed.rows.map((row) =>\n parseMaybeJson<Record<string, unknown>>(row),\n );\n const materializedGroups =\n changedGroups.length > 0 || changedRows.length > 0\n ? await readMaterializedGroups(db, reportTable, definition, scope)\n : [];\n const affectedGroups = dedupeGroupRows(definition, [\n ...changedGroups,\n ...materializedGroups,\n ]);\n\n if (affectedGroups.length === 0) {\n return {\n rowCount: 0,\n changedGroupCount: 0,\n watermarkBefore,\n watermarkAfter: watermarkBefore,\n mode: 'incremental',\n };\n }\n\n let rowCount = 0;\n const conflicts = conflictColumns(definition, scope);\n\n for (const groupRow of affectedGroups) {\n const affectedSpec = compileAffectedGroupSpec(\n definition,\n scope,\n watermark,\n groupRow,\n );\n const aggregate = buildAggregate(affectedSpec, 1, adapterType);\n const result = await db.query(aggregate.sql, ...aggregate.values);\n const refreshedAt = new Date();\n const rows = materializeRows(result.rows, definition, refreshedAt, scope);\n\n if (rows.length === 0) {\n await deleteMaterializedGroup(\n db,\n reportTable,\n definition,\n scope,\n groupRow,\n );\n continue;\n }\n\n for (const row of rows) {\n await db.upsert(reportTable, conflicts, row);\n rowCount += 1;\n }\n }\n\n const watermarkAfter = await maxWatermark(\n db,\n definition,\n scope,\n watermark.watermarkColumn,\n adapterType,\n );\n await writeWatermark(\n db,\n definition,\n scope,\n watermark.watermarkColumn,\n watermarkAfter,\n runId,\n );\n\n return {\n rowCount,\n changedGroupCount: affectedGroups.length,\n watermarkBefore,\n watermarkAfter,\n mode: 'incremental',\n };\n}\n\nfunction requiredReportRuntimeTables(\n mode: ReportRefreshMode,\n options: ReportRefreshOptions,\n): string[] {\n const tables = new Set<string>();\n if (options.trackRuns !== false) tables.add(REPORT_RUNS_TABLE);\n if (mode === 'incremental') tables.add(REPORT_WATERMARKS_TABLE);\n if (options.lock !== false) tables.add(REPORT_LOCKS_TABLE);\n return [...tables];\n}\n\nasync function refreshReportOnce(\n reportCtor: ReportCtor,\n options: ReportRefreshOptions,\n): Promise<ReportRefreshResult> {\n if (!options.db) {\n throw new Error('refreshReport requires a database handle');\n }\n\n const definition = await buildReportDefinition(reportCtor);\n const reportTable = getReportTableName(reportCtor);\n const requestedMode = options.mode ?? definition.refresh?.mode ?? 'rebuild';\n const exists = await tableExists(options.db, reportTable);\n if (!exists) {\n throw new Error(\n `Report table '${reportTable}' does not exist for ${reportCtor.name}. ` +\n `Run smrt db:migrate before refreshing reports.`,\n );\n }\n\n const requiredTables = requiredReportRuntimeTables(requestedMode, options);\n if (requiredTables.length > 0) {\n await assertReportTablesReady(options.db, requiredTables);\n }\n\n const adapterType = adapterTypeFromDb(options.db, options.adapterType);\n const scope = await resolveScope(definition, options);\n const lock =\n options.lock === false\n ? null\n : await acquireLock(\n options.db,\n definition,\n scope,\n options.lockTtlMs ?? 15 * 60 * 1000,\n );\n\n if (options.lock !== false && !lock) {\n const runId = await startRun(\n options.db,\n definition,\n scope,\n options,\n requestedMode,\n );\n await completeRun(options.db, runId, 'skipped');\n return {\n rowCount: 0,\n refreshedAt: new Date(),\n mode: requestedMode,\n tenantId: scope.tenantId,\n runId,\n changedGroupCount: 0,\n skipped: true,\n };\n }\n\n let runId: string | undefined;\n try {\n runId = await startRun(\n options.db,\n definition,\n scope,\n options,\n requestedMode,\n );\n const work =\n requestedMode === 'incremental'\n ? await incrementalReport(\n options.db,\n definition,\n reportTable,\n scope,\n adapterType,\n runId,\n options.changedRows,\n )\n : await rebuildReport(\n options.db,\n definition,\n reportTable,\n scope,\n adapterType,\n runId,\n );\n\n await completeRun(options.db, runId, 'success', {\n rowCount: work.rowCount,\n changedGroupCount: work.changedGroupCount,\n watermarkAfter: work.watermarkAfter,\n });\n\n return {\n rowCount: work.rowCount,\n refreshedAt: new Date(),\n mode: work.mode,\n tenantId: scope.tenantId,\n runId,\n changedGroupCount: work.changedGroupCount,\n };\n } catch (error) {\n await completeRun(options.db, runId, 'failed', { error });\n throw error;\n } finally {\n await lock?.release();\n }\n}\n\nexport async function refreshReport(\n reportCtor: ReportCtor,\n options: ReportRefreshOptions = {},\n): Promise<ReportRefreshResult> {\n if (options.tenantIds && options.tenantIds.length > 0) {\n if (!options.db) {\n throw new Error('refreshReport requires a database handle');\n }\n const results: ReportRefreshResult[] = [];\n for (const tenantId of options.tenantIds) {\n const result = await withTenant({ tenantId }, () =>\n refreshReportOnce(reportCtor, {\n ...options,\n tenantId,\n tenantIds: undefined,\n }),\n );\n results.push(result);\n }\n return {\n rowCount: results.reduce((sum, result) => sum + result.rowCount, 0),\n refreshedAt: new Date(),\n mode: results[0]?.mode ?? options.mode ?? 'rebuild',\n tenantId: null,\n changedGroupCount: results.reduce(\n (sum, result) => sum + (result.changedGroupCount ?? 0),\n 0,\n ),\n tenantResults: results,\n };\n }\n\n return refreshReportOnce(reportCtor, options);\n}\n\nexport function reportRowIdentity(\n row: Record<string, any>,\n definition: ReportDefinition,\n): string {\n const groupingColumns = getReportGroupingColumns(definition);\n return stableReportId(\n groupingColumns.map((column) => row[toSnakeCase(column)]),\n );\n}\n\nexport function reportSourceClassName(source: ReportSource): string {\n return sourceClassName(source);\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime report compiler and runner (#3711).
|
|
3
|
+
*
|
|
4
|
+
* `compileRuntimeReportSpec()` turns a parsed `RuntimeReportSpec` into the same
|
|
5
|
+
* `AggregateSpec` the declared-report compiler targets, and
|
|
6
|
+
* `runRuntimeReport()` executes it through the SDK `buildAggregate()` builder.
|
|
7
|
+
* Nothing the spec says is ever interpolated into SQL text:
|
|
8
|
+
*
|
|
9
|
+
* - Source tables and columns come ONLY from the ObjectRegistry, resolved from
|
|
10
|
+
* a server-owned source allow-list (`context.sources`). A spec names a
|
|
11
|
+
* source id and registry field names; it can never name a table or column.
|
|
12
|
+
* - Fields are admitted by the same exposure policy the report adapter uses:
|
|
13
|
+
* sensitive/secret fields, `readPermission`-gated fields the live principal
|
|
14
|
+
* lacks, transient/non-column/json fields, `_`-prefixed internals and the
|
|
15
|
+
* tenant column are un-nameable. A hidden field is reported exactly like an
|
|
16
|
+
* absent one so the compiler is not an existence oracle.
|
|
17
|
+
* - Filter and having values travel as bound parameters through the SDK
|
|
18
|
+
* `buildWhere()` builder, coerced to the field's type first.
|
|
19
|
+
* - Output aliases are prefixed (`r_<alias>`) inside the SQL so they can never
|
|
20
|
+
* collide with, shadow, or probe a source column.
|
|
21
|
+
* - Tenant isolation is added explicitly (raw aggregates bypass the tenancy
|
|
22
|
+
* interceptors) and fails closed: a tenant-scoped source with no tenant in
|
|
23
|
+
* scope reads only NULL-tenant rows when tenancy is enabled. There is no
|
|
24
|
+
* cross-tenant option.
|
|
25
|
+
*/
|
|
26
|
+
import type { DatabaseInterface, SqlAdapterType } from '@happyvertical/sql';
|
|
27
|
+
import { type RuntimeReportChart, type RuntimeReportSpec } from './runtime-spec.js';
|
|
28
|
+
import type { AggregateSpec } from './types.js';
|
|
29
|
+
/** A server-owned report source. The spec refers to it only by `id`. */
|
|
30
|
+
export interface RuntimeReportSourceDefinition {
|
|
31
|
+
/** Opaque id the spec names; defaults to nothing, always explicit. */
|
|
32
|
+
id: string;
|
|
33
|
+
/** Registered `ObjectRegistry` class (qualified or bare name). */
|
|
34
|
+
className: string;
|
|
35
|
+
label?: string;
|
|
36
|
+
/**
|
|
37
|
+
* Permission-catalog collection SLUG the host authorizes reads against (the
|
|
38
|
+
* `<collection>` in `<collection>.read`, not a class name). Informational
|
|
39
|
+
* to the compiler; the host's `authorizeSource` consumes it.
|
|
40
|
+
*/
|
|
41
|
+
collection?: string;
|
|
42
|
+
description?: string;
|
|
43
|
+
}
|
|
44
|
+
export interface RuntimeReportCompileContext {
|
|
45
|
+
/** Server-owned allow-list. A spec can reach nothing outside it. */
|
|
46
|
+
sources: readonly RuntimeReportSourceDefinition[];
|
|
47
|
+
/**
|
|
48
|
+
* The live principal's permission slugs. A `readPermission`-gated field is
|
|
49
|
+
* admitted only when its permission is present. Omitted means none.
|
|
50
|
+
*/
|
|
51
|
+
permissions?: readonly string[];
|
|
52
|
+
/**
|
|
53
|
+
* Tenant to scope tenant-scoped sources to. Defaults to the ambient tenant
|
|
54
|
+
* context. There is intentionally no way to widen this to other tenants.
|
|
55
|
+
*/
|
|
56
|
+
tenantId?: string | null;
|
|
57
|
+
/** Host authorization for reading the source (for example RBAC `read`). */
|
|
58
|
+
authorizeSource?: (source: RuntimeReportSourceDefinition) => void | Promise<void>;
|
|
59
|
+
}
|
|
60
|
+
export type RuntimeReportColumnType = 'string' | 'integer' | 'decimal' | 'boolean' | 'datetime' | 'id';
|
|
61
|
+
export interface RuntimeReportColumn {
|
|
62
|
+
id: string;
|
|
63
|
+
label: string;
|
|
64
|
+
role: 'dimension' | 'measure';
|
|
65
|
+
type: RuntimeReportColumnType;
|
|
66
|
+
/** Source field this column derives from (absent for a row count). */
|
|
67
|
+
field?: string;
|
|
68
|
+
fn?: string;
|
|
69
|
+
bucket?: string;
|
|
70
|
+
/** Field format hint carried from the model (for example `money`). */
|
|
71
|
+
format?: string;
|
|
72
|
+
}
|
|
73
|
+
export interface CompiledRuntimeReport {
|
|
74
|
+
version: 1;
|
|
75
|
+
specHash: string;
|
|
76
|
+
spec: RuntimeReportSpec;
|
|
77
|
+
source: {
|
|
78
|
+
id: string;
|
|
79
|
+
className: string;
|
|
80
|
+
label: string;
|
|
81
|
+
table: string;
|
|
82
|
+
};
|
|
83
|
+
columns: RuntimeReportColumn[];
|
|
84
|
+
/** Aggregate plan; output aliases carry the internal `r_` prefix. */
|
|
85
|
+
aggregate: AggregateSpec;
|
|
86
|
+
limit: number;
|
|
87
|
+
tenant: {
|
|
88
|
+
scoped: boolean;
|
|
89
|
+
tenantId: string | null;
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
export interface RuntimeReportResult {
|
|
93
|
+
version: 1;
|
|
94
|
+
specHash: string;
|
|
95
|
+
title: string;
|
|
96
|
+
columns: RuntimeReportColumn[];
|
|
97
|
+
rows: Record<string, string | number | boolean | null>[];
|
|
98
|
+
rowCount: number;
|
|
99
|
+
/** True when more rows existed than the spec/host limit. */
|
|
100
|
+
truncated: boolean;
|
|
101
|
+
ranAt: string;
|
|
102
|
+
chart: RuntimeReportChart;
|
|
103
|
+
}
|
|
104
|
+
export interface RuntimeReportSourceDescription {
|
|
105
|
+
id: string;
|
|
106
|
+
label: string;
|
|
107
|
+
description?: string;
|
|
108
|
+
/** Fields the live principal may group by, filter on or aggregate. */
|
|
109
|
+
fields: {
|
|
110
|
+
name: string;
|
|
111
|
+
label: string;
|
|
112
|
+
type: RuntimeReportColumnType;
|
|
113
|
+
format?: string;
|
|
114
|
+
filterOperators: readonly string[];
|
|
115
|
+
aggregates: readonly string[];
|
|
116
|
+
}[];
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Describe what a spec may reference on one source for the live principal:
|
|
120
|
+
* exactly the field set the compiler would admit, so a model never has to
|
|
121
|
+
* guess names and never learns about hidden ones.
|
|
122
|
+
*/
|
|
123
|
+
export declare function describeRuntimeReportSource(context: RuntimeReportCompileContext, sourceId: string): Promise<RuntimeReportSourceDescription>;
|
|
124
|
+
/**
|
|
125
|
+
* Validate a parsed spec against the registry, the live principal's field
|
|
126
|
+
* policy, and tenancy, and compile it to an executable aggregate plan.
|
|
127
|
+
*/
|
|
128
|
+
export declare function compileRuntimeReportSpec(spec: RuntimeReportSpec, context: RuntimeReportCompileContext): Promise<CompiledRuntimeReport>;
|
|
129
|
+
export interface RunRuntimeReportOptions {
|
|
130
|
+
db: DatabaseInterface;
|
|
131
|
+
adapterType?: SqlAdapterType;
|
|
132
|
+
/** Host row ceiling applied on top of the spec's own limit. */
|
|
133
|
+
maxRows?: number;
|
|
134
|
+
now?: () => Date;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Execute a compiled plan. The caller owns authorization: compile with the
|
|
138
|
+
* live principal each time rather than running a plan compiled for someone
|
|
139
|
+
* else. Results are never cached here for exactly that reason.
|
|
140
|
+
*/
|
|
141
|
+
export declare function runRuntimeReport(compiled: CompiledRuntimeReport, options: RunRuntimeReportOptions): Promise<RuntimeReportResult>;
|
|
142
|
+
//# sourceMappingURL=runtime-compiler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runtime-compiler.d.ts","sourceRoot":"","sources":["../src/runtime-compiler.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stored runtime report definitions (#3711).
|
|
3
|
+
*
|
|
4
|
+
* A `RuntimeReport` row is a validated {@link RuntimeReportSpec} saved as data.
|
|
5
|
+
* It carries NO authority: the spec is re-parsed on every read and re-compiled
|
|
6
|
+
* against the live principal's field policy, permissions and tenant on every
|
|
7
|
+
* run, so a stored report can never expose more than its current viewer may
|
|
8
|
+
* see. Result rows are deliberately never persisted (a cached result computed
|
|
9
|
+
* for one principal must not be served to another).
|
|
10
|
+
*
|
|
11
|
+
* The assistant creates rows through {@link saveRuntimeReport}, so only a spec
|
|
12
|
+
* that compiled for the saving principal and was confirmed by a human is
|
|
13
|
+
* stored. That is enforced HERE, in the model layer, not just by which
|
|
14
|
+
* surfaces are generated: `validateBeforeSave()` refuses every insert, and
|
|
15
|
+
* every update that changes the spec (or title, source, description or author),
|
|
16
|
+
* unless the instance carries a confirmation proof that only
|
|
17
|
+
* {@link saveRuntimeReport} can attach. Rows built from JSON (a REST body, a
|
|
18
|
+
* manifest tool call) cannot carry the proof, so they are refused whatever
|
|
19
|
+
* surface delivered them. Archiving (a `status` change with the spec
|
|
20
|
+
* untouched) stays allowed. The generated surfaces expose `list`/`get`, plus
|
|
21
|
+
* `create` on the API only so the permission catalog and Postgres RLS bindings
|
|
22
|
+
* know the operation the confirmed save performs.
|
|
23
|
+
*/
|
|
24
|
+
import { SmrtCollection, SmrtObject, type SmrtObjectOptions } from '@happyvertical/smrt-core';
|
|
25
|
+
import type { DatabaseInterface } from '@happyvertical/sql';
|
|
26
|
+
import { type CompiledRuntimeReport, type RuntimeReportCompileContext, type RuntimeReportResult } from './runtime-compiler.js';
|
|
27
|
+
import { type RuntimeReportSpec } from './runtime-spec.js';
|
|
28
|
+
export type RuntimeReportStatus = 'active' | 'archived';
|
|
29
|
+
export declare class RuntimeReport extends SmrtObject {
|
|
30
|
+
tenantId: string;
|
|
31
|
+
title: string;
|
|
32
|
+
description: string;
|
|
33
|
+
/** Source id the spec names; resolved against a host allow-list at run. */
|
|
34
|
+
sourceId: string;
|
|
35
|
+
/** Normalized spec JSON; use {@link getSpec}/{@link setSpec}. */
|
|
36
|
+
spec: string;
|
|
37
|
+
/** Hash of the normalized spec; detects out-of-band edits to `spec`. */
|
|
38
|
+
specHash: string;
|
|
39
|
+
status: RuntimeReportStatus;
|
|
40
|
+
/** Principal that saved the report (an id string; not a foreign key). */
|
|
41
|
+
createdByUserId: string;
|
|
42
|
+
constructor(options?: SmrtObjectOptions);
|
|
43
|
+
/** Re-parse the stored JSON; never trust the column without validation. */
|
|
44
|
+
getSpec(): RuntimeReportSpec;
|
|
45
|
+
setSpec(spec: RuntimeReportSpec): void;
|
|
46
|
+
protected validateBeforeSave(): Promise<void>;
|
|
47
|
+
/**
|
|
48
|
+
* Invariant: a RuntimeReport row is inserted, or has its spec/title/source/
|
|
49
|
+
* description/author changed, only by the confirmed-save path. Anything else
|
|
50
|
+
* (REST `create` body, manifest tool args, `new RuntimeReport().save()`,
|
|
51
|
+
* `collection.update()` of the spec) is refused here. A save that leaves those
|
|
52
|
+
* columns as stored (an archive/unarchive status change) needs no proof.
|
|
53
|
+
*/
|
|
54
|
+
private assertConfirmedWrite;
|
|
55
|
+
}
|
|
56
|
+
export declare class RuntimeReportCollection extends SmrtCollection<RuntimeReport> {
|
|
57
|
+
static readonly _itemClass: typeof RuntimeReport;
|
|
58
|
+
}
|
|
59
|
+
export interface RuntimeReportStoreOptions {
|
|
60
|
+
db: DatabaseInterface;
|
|
61
|
+
}
|
|
62
|
+
export interface SaveRuntimeReportOptions extends RuntimeReportStoreOptions {
|
|
63
|
+
/** A plan compiled for the SAVING principal; the only accepted input. */
|
|
64
|
+
compiled: CompiledRuntimeReport;
|
|
65
|
+
/**
|
|
66
|
+
* The spec hash a human confirmed (the value handed to the application's
|
|
67
|
+
* confirmation host). It must equal the compiled plan's hash; the row is
|
|
68
|
+
* only minted when it does. Call this ONLY after that confirmation resolved.
|
|
69
|
+
*/
|
|
70
|
+
confirmedSpecHash: string;
|
|
71
|
+
createdByUserId: string;
|
|
72
|
+
/** Defaults to the tenant the plan was compiled for, then ambient. */
|
|
73
|
+
tenantId?: string | null;
|
|
74
|
+
}
|
|
75
|
+
/** Persist a spec that has already compiled for the saving principal. */
|
|
76
|
+
export declare function saveRuntimeReport(options: SaveRuntimeReportOptions): Promise<RuntimeReport>;
|
|
77
|
+
export declare function getRuntimeReport(options: RuntimeReportStoreOptions & {
|
|
78
|
+
ref: string;
|
|
79
|
+
/**
|
|
80
|
+
* Explicit owner tenant. When given, a row owned by any other tenant is
|
|
81
|
+
* treated as absent even if the ambient tenant context would admit it
|
|
82
|
+
* (a host that knows the principal's tenant should always pass it).
|
|
83
|
+
*/
|
|
84
|
+
tenantId?: string;
|
|
85
|
+
}): Promise<RuntimeReport | null>;
|
|
86
|
+
export declare function listRuntimeReports(options: RuntimeReportStoreOptions & {
|
|
87
|
+
status?: RuntimeReportStatus;
|
|
88
|
+
limit?: number;
|
|
89
|
+
/** Explicit owner tenant; filters in addition to the ambient context. */
|
|
90
|
+
tenantId?: string;
|
|
91
|
+
}): Promise<RuntimeReport[]>;
|
|
92
|
+
export declare function archiveRuntimeReport(options: RuntimeReportStoreOptions & {
|
|
93
|
+
ref: string;
|
|
94
|
+
}): Promise<RuntimeReport | null>;
|
|
95
|
+
/**
|
|
96
|
+
* Re-validate a stored report against the live principal and run it. The
|
|
97
|
+
* stored spec is parsed again and compiled with the caller's context, so
|
|
98
|
+
* policy changes made after the report was saved apply immediately.
|
|
99
|
+
*/
|
|
100
|
+
export declare function runStoredRuntimeReport(options: RuntimeReportStoreOptions & {
|
|
101
|
+
report: RuntimeReport;
|
|
102
|
+
context: RuntimeReportCompileContext;
|
|
103
|
+
maxRows?: number;
|
|
104
|
+
}): Promise<RuntimeReportResult>;
|
|
105
|
+
//# sourceMappingURL=runtime-report.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runtime-report.d.ts","sourceRoot":"","sources":["../src/runtime-report.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAEL,cAAc,EACd,UAAU,EACV,KAAK,iBAAiB,EAEvB,MAAM,0BAA0B,CAAC;AAMlC,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAC5D,OAAO,EACL,KAAK,qBAAqB,EAE1B,KAAK,2BAA2B,EAChC,KAAK,mBAAmB,EAEzB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAGL,KAAK,iBAAiB,EAGvB,MAAM,mBAAmB,CAAC;AAE3B,MAAM,MAAM,mBAAmB,GAAG,QAAQ,GAAG,UAAU,CAAC;AAYxD,qBAea,aAAc,SAAQ,UAAU;IAE3C,QAAQ,EAAE,MAAM,CAAM;IAGtB,KAAK,EAAE,MAAM,CAAM;IAGnB,WAAW,EAAE,MAAM,CAAM;IAEzB,2EAA2E;IAE3E,QAAQ,EAAE,MAAM,CAAM;IAEtB,iEAAiE;IAEjE,IAAI,EAAE,MAAM,CAAM;IAElB,wEAAwE;IAExE,QAAQ,EAAE,MAAM,CAAM;IAGtB,MAAM,EAAE,mBAAmB,CAAY;IAEvC,yEAAyE;IAEzE,eAAe,EAAE,MAAM,CAAM;gBAEjB,OAAO,GAAE,iBAAsB;IAM3C,2EAA2E;IAC3E,OAAO,IAAI,iBAAiB;IAY5B,OAAO,CAAC,IAAI,EAAE,iBAAiB,GAAG,IAAI;cASb,kBAAkB,IAAI,OAAO,CAAC,IAAI,CAAC;IAa5D;;;;;;OAMG;YACW,oBAAoB;CAgCnC;AAED,qBAAa,uBAAwB,SAAQ,cAAc,CAAC,aAAa,CAAC;IACxE,MAAM,CAAC,QAAQ,CAAC,UAAU,uBAAiB;CAC5C;AAED,MAAM,WAAW,yBAAyB;IACxC,EAAE,EAAE,iBAAiB,CAAC;CACvB;AAED,MAAM,WAAW,wBAAyB,SAAQ,yBAAyB;IACzE,yEAAyE;IACzE,QAAQ,EAAE,qBAAqB,CAAC;IAChC;;;;OAIG;IACH,iBAAiB,EAAE,MAAM,CAAC;IAC1B,eAAe,EAAE,MAAM,CAAC;IACxB,sEAAsE;IACtE,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAUD,yEAAyE;AACzE,wBAAsB,iBAAiB,CACrC,OAAO,EAAE,wBAAwB,GAChC,OAAO,CAAC,aAAa,CAAC,CAuCxB;AAED,wBAAsB,gBAAgB,CACpC,OAAO,EAAE,yBAAyB,GAAG;IACnC,GAAG,EAAE,MAAM,CAAC;IACZ;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,GACA,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC,CAO/B;AAED,wBAAsB,kBAAkB,CACtC,OAAO,EAAE,yBAAyB,GAAG;IACnC,MAAM,CAAC,EAAE,mBAAmB,CAAC;IAC7B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,yEAAyE;IACzE,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,GACA,OAAO,CAAC,aAAa,EAAE,CAAC,CAU1B;AAED,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,yBAAyB,GAAG;IAAE,GAAG,EAAE,MAAM,CAAA;CAAE,GACnD,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC,CAM/B;AAED;;;;GAIG;AACH,wBAAsB,sBAAsB,CAC1C,OAAO,EAAE,yBAAyB,GAAG;IACnC,MAAM,EAAE,aAAa,CAAC;IACtB,OAAO,EAAE,2BAA2B,CAAC;IACrC,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,GACA,OAAO,CAAC,mBAAmB,CAAC,CAgB9B"}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Declarative runtime report specification (#3711).
|
|
3
|
+
*
|
|
4
|
+
* A runtime report is DATA, never code: a closed, versioned structure that a
|
|
5
|
+
* user (or the assistant on a user's behalf) can propose at runtime. The spec
|
|
6
|
+
* grammar deliberately has no free-form SQL, no expression language, no raw
|
|
7
|
+
* identifiers and no join syntax. Every name in it is later resolved against
|
|
8
|
+
* the ObjectRegistry field policy by `compileRuntimeReportSpec()`; this module
|
|
9
|
+
* only enforces the STRUCTURE (shape, bounds, alias hygiene) and normalizes
|
|
10
|
+
* defaults so equal specs hash equally.
|
|
11
|
+
*
|
|
12
|
+
* The parser accepts no authority. Nothing in a spec names a tenant, a
|
|
13
|
+
* principal, a permission, a table, a column alias that is not a plain
|
|
14
|
+
* identifier, or an SQL fragment.
|
|
15
|
+
*/
|
|
16
|
+
export declare const RUNTIME_REPORT_SPEC_VERSION: 1;
|
|
17
|
+
export declare const RUNTIME_REPORT_LIMITS: Readonly<{
|
|
18
|
+
maxTitleLength: 120;
|
|
19
|
+
maxDescriptionLength: 500;
|
|
20
|
+
maxDimensions: 4;
|
|
21
|
+
maxMeasures: 6;
|
|
22
|
+
maxFilters: 12;
|
|
23
|
+
maxHaving: 4;
|
|
24
|
+
maxSort: 3;
|
|
25
|
+
maxInValues: 100;
|
|
26
|
+
maxStringValueLength: 256;
|
|
27
|
+
defaultRowLimit: 100;
|
|
28
|
+
maxRowLimit: 1000;
|
|
29
|
+
maxAliasLength: 40;
|
|
30
|
+
maxFieldNameLength: 64;
|
|
31
|
+
maxSourceIdLength: 120;
|
|
32
|
+
}>;
|
|
33
|
+
export declare const RUNTIME_REPORT_BUCKETS: readonly ["minute", "hour", "day", "week", "month", "quarter", "year"];
|
|
34
|
+
export type RuntimeReportBucket = (typeof RUNTIME_REPORT_BUCKETS)[number];
|
|
35
|
+
export declare const RUNTIME_REPORT_MEASURE_FNS: readonly ["count", "countDistinct", "sum", "avg", "min", "max"];
|
|
36
|
+
export type RuntimeReportMeasureFn = (typeof RUNTIME_REPORT_MEASURE_FNS)[number];
|
|
37
|
+
export declare const RUNTIME_REPORT_FILTER_OPS: readonly ["eq", "ne", "gt", "gte", "lt", "lte", "in", "notIn", "contains", "isNull", "isNotNull"];
|
|
38
|
+
export type RuntimeReportFilterOp = (typeof RUNTIME_REPORT_FILTER_OPS)[number];
|
|
39
|
+
export declare const RUNTIME_REPORT_HAVING_OPS: readonly ["eq", "ne", "gt", "gte", "lt", "lte"];
|
|
40
|
+
export type RuntimeReportHavingOp = (typeof RUNTIME_REPORT_HAVING_OPS)[number];
|
|
41
|
+
export declare const RUNTIME_REPORT_CHART_TYPES: readonly ["table", "bar", "line", "pie", "stat"];
|
|
42
|
+
export type RuntimeReportChartType = (typeof RUNTIME_REPORT_CHART_TYPES)[number];
|
|
43
|
+
export type RuntimeReportScalar = string | number | boolean;
|
|
44
|
+
export interface RuntimeReportDimension {
|
|
45
|
+
/** Source field name as declared on the model (for example `issuedAt`). */
|
|
46
|
+
field: string;
|
|
47
|
+
/** Output column alias. Defaults to the snake_case field (plus bucket). */
|
|
48
|
+
as: string;
|
|
49
|
+
/** Time bucket; only valid on datetime fields. */
|
|
50
|
+
bucket?: RuntimeReportBucket;
|
|
51
|
+
}
|
|
52
|
+
export interface RuntimeReportMeasure {
|
|
53
|
+
fn: RuntimeReportMeasureFn;
|
|
54
|
+
/** Source field; omitted only for a plain row `count`. */
|
|
55
|
+
field?: string;
|
|
56
|
+
as: string;
|
|
57
|
+
}
|
|
58
|
+
export interface RuntimeReportFilter {
|
|
59
|
+
field: string;
|
|
60
|
+
op: RuntimeReportFilterOp;
|
|
61
|
+
/** Scalar, or a bounded array for `in`/`notIn`; absent for null tests. */
|
|
62
|
+
value?: RuntimeReportScalar | RuntimeReportScalar[];
|
|
63
|
+
}
|
|
64
|
+
export interface RuntimeReportHaving {
|
|
65
|
+
/** A measure alias declared by this spec. */
|
|
66
|
+
measure: string;
|
|
67
|
+
op: RuntimeReportHavingOp;
|
|
68
|
+
value: number;
|
|
69
|
+
}
|
|
70
|
+
export interface RuntimeReportSort {
|
|
71
|
+
/** A dimension or measure alias declared by this spec. */
|
|
72
|
+
by: string;
|
|
73
|
+
direction: 'asc' | 'desc';
|
|
74
|
+
}
|
|
75
|
+
export interface RuntimeReportChart {
|
|
76
|
+
type: RuntimeReportChartType;
|
|
77
|
+
/** Dimension alias for the category/x axis. */
|
|
78
|
+
x?: string;
|
|
79
|
+
/** Measure aliases for the value/y axis. */
|
|
80
|
+
y?: string[];
|
|
81
|
+
}
|
|
82
|
+
export interface RuntimeReportSpec {
|
|
83
|
+
version: typeof RUNTIME_REPORT_SPEC_VERSION;
|
|
84
|
+
title: string;
|
|
85
|
+
description?: string;
|
|
86
|
+
/** A server-owned source id; resolved against an allow-list at compile. */
|
|
87
|
+
source: string;
|
|
88
|
+
dimensions: RuntimeReportDimension[];
|
|
89
|
+
measures: RuntimeReportMeasure[];
|
|
90
|
+
filters: RuntimeReportFilter[];
|
|
91
|
+
having: RuntimeReportHaving[];
|
|
92
|
+
sort: RuntimeReportSort[];
|
|
93
|
+
limit: number;
|
|
94
|
+
chart: RuntimeReportChart;
|
|
95
|
+
}
|
|
96
|
+
export type RuntimeReportErrorCode = 'invalid_spec' | 'unknown_source' | 'unknown_field' | 'invalid_operation' | 'invalid_value' | 'tenant_required' | 'confirmation_required' | 'unsupported_source' | 'invalid_result';
|
|
97
|
+
/**
|
|
98
|
+
* Raised for any spec or compile rejection. The message is safe to hand back
|
|
99
|
+
* to a model so it can repair the spec; it never echoes restricted-field
|
|
100
|
+
* metadata (a hidden field is reported exactly like an absent one).
|
|
101
|
+
*/
|
|
102
|
+
export declare class RuntimeReportError extends Error {
|
|
103
|
+
readonly code: RuntimeReportErrorCode;
|
|
104
|
+
readonly path: string;
|
|
105
|
+
/**
|
|
106
|
+
* HTTP-style status so tool loops classify it: 4xx input problems are
|
|
107
|
+
* repairable by the caller, `tenant_required` and `confirmation_required`
|
|
108
|
+
* are denials, results that cannot be represented are server errors.
|
|
109
|
+
*/
|
|
110
|
+
readonly status: number;
|
|
111
|
+
constructor(code: RuntimeReportErrorCode, path: string, message: string);
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Parse and normalize an untrusted spec (an object, or its JSON string form).
|
|
115
|
+
*
|
|
116
|
+
* Structural only: aliases are unique and well-formed, sort/having/chart refer
|
|
117
|
+
* to aliases the spec itself declares, and every bound is enforced. Field,
|
|
118
|
+
* source, permission and type checks happen at compile time because they
|
|
119
|
+
* depend on the registry and the live principal.
|
|
120
|
+
*/
|
|
121
|
+
export declare function parseRuntimeReportSpec(input: unknown): RuntimeReportSpec;
|
|
122
|
+
/** Stable fingerprint of a NORMALIZED spec; equal specs hash equally. */
|
|
123
|
+
export declare function runtimeReportSpecHash(spec: RuntimeReportSpec): string;
|
|
124
|
+
/** Stable JSON text for storage (sorted keys). */
|
|
125
|
+
export declare function serializeRuntimeReportSpec(spec: RuntimeReportSpec): string;
|
|
126
|
+
//# sourceMappingURL=runtime-spec.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runtime-spec.d.ts","sourceRoot":"","sources":["../src/runtime-spec.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAIH,eAAO,MAAM,2BAA2B,EAAG,CAAU,CAAC;AAEtD,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;EAehC,CAAC;AAEH,eAAO,MAAM,sBAAsB,wEAQzB,CAAC;AACX,MAAM,MAAM,mBAAmB,GAAG,CAAC,OAAO,sBAAsB,CAAC,CAAC,MAAM,CAAC,CAAC;AAE1E,eAAO,MAAM,0BAA0B,iEAO7B,CAAC;AACX,MAAM,MAAM,sBAAsB,GAChC,CAAC,OAAO,0BAA0B,CAAC,CAAC,MAAM,CAAC,CAAC;AAE9C,eAAO,MAAM,yBAAyB,mGAY5B,CAAC;AACX,MAAM,MAAM,qBAAqB,GAAG,CAAC,OAAO,yBAAyB,CAAC,CAAC,MAAM,CAAC,CAAC;AAE/E,eAAO,MAAM,yBAAyB,iDAO5B,CAAC;AACX,MAAM,MAAM,qBAAqB,GAAG,CAAC,OAAO,yBAAyB,CAAC,CAAC,MAAM,CAAC,CAAC;AAE/E,eAAO,MAAM,0BAA0B,kDAM7B,CAAC;AACX,MAAM,MAAM,sBAAsB,GAChC,CAAC,OAAO,0BAA0B,CAAC,CAAC,MAAM,CAAC,CAAC;AAE9C,MAAM,MAAM,mBAAmB,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;AAE5D,MAAM,WAAW,sBAAsB;IACrC,2EAA2E;IAC3E,KAAK,EAAE,MAAM,CAAC;IACd,2EAA2E;IAC3E,EAAE,EAAE,MAAM,CAAC;IACX,kDAAkD;IAClD,MAAM,CAAC,EAAE,mBAAmB,CAAC;CAC9B;AAED,MAAM,WAAW,oBAAoB;IACnC,EAAE,EAAE,sBAAsB,CAAC;IAC3B,0DAA0D;IAC1D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,EAAE,EAAE,MAAM,CAAC;CACZ;AAED,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,MAAM,CAAC;IACd,EAAE,EAAE,qBAAqB,CAAC;IAC1B,0EAA0E;IAC1E,KAAK,CAAC,EAAE,mBAAmB,GAAG,mBAAmB,EAAE,CAAC;CACrD;AAED,MAAM,WAAW,mBAAmB;IAClC,6CAA6C;IAC7C,OAAO,EAAE,MAAM,CAAC;IAChB,EAAE,EAAE,qBAAqB,CAAC;IAC1B,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,iBAAiB;IAChC,0DAA0D;IAC1D,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,KAAK,GAAG,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,sBAAsB,CAAC;IAC7B,+CAA+C;IAC/C,CAAC,CAAC,EAAE,MAAM,CAAC;IACX,4CAA4C;IAC5C,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC;CACd;AAED,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,OAAO,2BAA2B,CAAC;IAC5C,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,2EAA2E;IAC3E,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,sBAAsB,EAAE,CAAC;IACrC,QAAQ,EAAE,oBAAoB,EAAE,CAAC;IACjC,OAAO,EAAE,mBAAmB,EAAE,CAAC;IAC/B,MAAM,EAAE,mBAAmB,EAAE,CAAC;IAC9B,IAAI,EAAE,iBAAiB,EAAE,CAAC;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,kBAAkB,CAAC;CAC3B;AAED,MAAM,MAAM,sBAAsB,GAC9B,cAAc,GACd,gBAAgB,GAChB,eAAe,GACf,mBAAmB,GACnB,eAAe,GACf,iBAAiB,GACjB,uBAAuB,GACvB,oBAAoB,GACpB,gBAAgB,CAAC;AAErB;;;;GAIG;AACH,qBAAa,kBAAmB,SAAQ,KAAK;IAC3C,QAAQ,CAAC,IAAI,EAAE,sBAAsB,CAAC;IACtC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB;;;;OAIG;IACH,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;gBAEZ,IAAI,EAAE,sBAAsB,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM;CAYxE;AAwRD;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,OAAO,GAAG,iBAAiB,CAqKxE;AAcD,yEAAyE;AACzE,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,iBAAiB,GAAG,MAAM,CAIrE;AAED,kDAAkD;AAClD,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,iBAAiB,GAAG,MAAM,CAE1E"}
|