Governed substrate for autonomous agents: scoped identity (passports), audited actions, MCP workspace. Infra IPs and secrets redacted for public release.
202 lines
6 KiB
JavaScript
202 lines
6 KiB
JavaScript
/**
|
|
* ADR-026: Summary Variable Service
|
|
*
|
|
* Service for creating Variables from column summary aggregations.
|
|
* Allows exporting SUM, AVG, MIN, MAX, COUNT etc. to reusable Variables
|
|
* that can be referenced in formulas across the space.
|
|
*/
|
|
|
|
import { dbGet, dbRun, dbAll } from '../database/connection.js';
|
|
import logger from '../utils/logger.js';
|
|
import { generateBaseId } from '../utils/baseId.js';
|
|
|
|
const serviceLogger = logger.child({ service: 'SummaryVariableService' });
|
|
|
|
/**
|
|
* Valid aggregation types for summary variables
|
|
*/
|
|
export const VALID_AGGREGATIONS = [
|
|
'sum', 'avg', 'min', 'max', 'count',
|
|
'countUnique', 'countEmpty', 'countFilled',
|
|
'checked', 'unchecked', 'percentChecked',
|
|
'earliest', 'latest', 'dateRange',
|
|
'percentFilled'
|
|
];
|
|
|
|
/**
|
|
* Formula templates for each aggregation type
|
|
*/
|
|
export const AGGREGATION_FORMULAS = {
|
|
sum: (colName) => `SUM({{${colName}}})`,
|
|
avg: (colName) => `AVG({{${colName}}})`,
|
|
min: (colName) => `MIN({{${colName}}})`,
|
|
max: (colName) => `MAX({{${colName}}})`,
|
|
count: (colName) => `COUNT({{${colName}}})`,
|
|
countUnique: (colName) => `COUNTUNIQUE({{${colName}}})`,
|
|
countEmpty: (colName) => `COUNTEMPTY({{${colName}}})`,
|
|
countFilled: (colName) => `COUNTFILLED({{${colName}}})`,
|
|
checked: (colName) => `COUNTIF({{${colName}}}, true)`,
|
|
unchecked: (colName) => `COUNTIF({{${colName}}}, false)`,
|
|
percentChecked: (colName) => `PERCENTIF({{${colName}}}, true)`,
|
|
earliest: (colName) => `MIN({{${colName}}})`,
|
|
latest: (colName) => `MAX({{${colName}}})`,
|
|
dateRange: (colName) => `DATEDIFF(MAX({{${colName}}}), MIN({{${colName}}}))`,
|
|
percentFilled: (colName) => `PERCENTFILLED({{${colName}}})`,
|
|
};
|
|
|
|
/**
|
|
* Create a Variable from column summary aggregation
|
|
*
|
|
* @param {Object} params
|
|
* @param {number} params.tableId - Table ID
|
|
* @param {string} params.columnId - Column ID
|
|
* @param {string} params.aggregation - Aggregation type (sum, avg, min, max, count, etc.)
|
|
* @param {number} params.userId - User ID creating the variable
|
|
* @param {string} [params.variableName] - Optional custom variable name
|
|
* @returns {Promise<{success: boolean, variable: Object}>}
|
|
*/
|
|
export async function createSummaryVariable({ tableId, columnId, aggregation, userId, variableName }) {
|
|
serviceLogger.debug({ tableId, columnId, aggregation }, 'Creating summary variable');
|
|
|
|
// Validate aggregation type
|
|
if (!VALID_AGGREGATIONS.includes(aggregation)) {
|
|
throw new Error(`Invalid aggregation type: ${aggregation}. Valid types: ${VALID_AGGREGATIONS.join(', ')}`);
|
|
}
|
|
|
|
// Get table info
|
|
const table = await dbGet(`
|
|
SELECT id, name, display_name, project_id
|
|
FROM universal_tables
|
|
WHERE id = ?
|
|
`, [tableId]);
|
|
|
|
if (!table) {
|
|
throw new Error('Table not found');
|
|
}
|
|
|
|
// Get column info
|
|
const column = await dbGet(`
|
|
SELECT id, name, display_name, type
|
|
FROM table_columns
|
|
WHERE (id = ? OR name = ?) AND table_id = ?
|
|
`, [columnId, columnId, tableId]);
|
|
|
|
if (!column) {
|
|
throw new Error('Column not found');
|
|
}
|
|
|
|
// Get space info through project
|
|
const projectSpace = await dbGet(`
|
|
SELECT p.id as project_id, s.id as space_id
|
|
FROM projects p
|
|
JOIN spaces s ON s.id = p.space_id
|
|
WHERE p.id = ?
|
|
`, [table.project_id]);
|
|
|
|
if (!projectSpace) {
|
|
throw new Error('Space not found for table');
|
|
}
|
|
|
|
// Generate variable name if not provided
|
|
let finalName = variableName;
|
|
if (!finalName) {
|
|
finalName = `$${column.name}_${aggregation}`;
|
|
}
|
|
|
|
// Check if name already exists and make unique if needed
|
|
const existingVars = await dbAll(`
|
|
SELECT name FROM table_rows
|
|
WHERE table_id IN (
|
|
SELECT id FROM universal_tables
|
|
WHERE project_id IN (
|
|
SELECT id FROM projects
|
|
WHERE space_id = ? AND type = 'system_data'
|
|
)
|
|
AND name = 'Variables'
|
|
)
|
|
`, [projectSpace.space_id]);
|
|
|
|
const existingNames = new Set(existingVars.map(v => {
|
|
try {
|
|
const data = JSON.parse(v.name || '{}');
|
|
return data.name || v.name;
|
|
} catch {
|
|
return v.name;
|
|
}
|
|
}));
|
|
|
|
if (existingNames.has(finalName)) {
|
|
// Add suffix to make unique
|
|
let suffix = 1;
|
|
while (existingNames.has(`${finalName}_${suffix}`)) {
|
|
suffix++;
|
|
}
|
|
finalName = `${finalName}_${suffix}`;
|
|
}
|
|
|
|
// Generate formula
|
|
const formulaFn = AGGREGATION_FORMULAS[aggregation];
|
|
const formula = formulaFn(column.name);
|
|
|
|
// Get Variables table for this space
|
|
const variablesTable = await dbGet(`
|
|
SELECT ut.id
|
|
FROM universal_tables ut
|
|
JOIN projects p ON p.id = ut.project_id
|
|
WHERE p.space_id = ? AND p.type = 'system_data' AND ut.name = 'Variables'
|
|
`, [projectSpace.space_id]);
|
|
|
|
if (!variablesTable) {
|
|
throw new Error('Variables table not found. Please create it first via Space Settings.');
|
|
}
|
|
|
|
// Create the variable row
|
|
const now = new Date().toISOString();
|
|
const variableData = {
|
|
name: finalName,
|
|
scope_type: 'table',
|
|
scope_ref: tableId,
|
|
formula: formula,
|
|
description: `${aggregation.toUpperCase()} of ${column.display_name || column.name}`,
|
|
stream_id: 1,
|
|
order_index: 0,
|
|
cached_value: null,
|
|
cached_at: null,
|
|
};
|
|
|
|
const base_id = generateBaseId();
|
|
const result = await dbRun(`
|
|
INSERT INTO table_rows (table_id, base_id, data, created_by, created_at, updated_at)
|
|
VALUES (?, ?, ?, ?, ?, ?)
|
|
`, [variablesTable.id, base_id, JSON.stringify(variableData), userId, now, now]);
|
|
|
|
serviceLogger.info({ variableId: result.lastID, name: finalName }, 'Summary variable created');
|
|
|
|
return {
|
|
success: true,
|
|
variable: {
|
|
id: result.lastID,
|
|
name: finalName,
|
|
formula: formula,
|
|
scope: 'table',
|
|
scopeRef: tableId,
|
|
value: null, // Will be calculated on next recalculation
|
|
},
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Get all summary variables linked to a column
|
|
*/
|
|
export async function getColumnLinkedVariables(tableId, columnId) {
|
|
// This would query variables that reference this column in their formula
|
|
// For now, return empty - will be implemented with full variable resolution
|
|
return [];
|
|
}
|
|
|
|
export default {
|
|
createSummaryVariable,
|
|
getColumnLinkedVariables,
|
|
AGGREGATION_FORMULAS,
|
|
VALID_AGGREGATIONS,
|
|
};
|