Skip to content

feat: Expression engine formulas, live report export, and transaction manager#425

Merged
hotlong merged 5 commits intomainfrom
copilot/full-stack-integration-enhancements
Feb 10, 2026
Merged

feat: Expression engine formulas, live report export, and transaction manager#425
hotlong merged 5 commits intomainfrom
copilot/full-stack-integration-enhancements

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Feb 10, 2026

Implements the three pillars of the @objectstack/client Full-Stack Integration roadmap: expression formula functions, DataSource-integrated report export, and transactional multi-step operations with rollback.

Expression Engine — FormulaFunctions

New FormulaFunctions class auto-injected into ExpressionEvaluator context. 20 built-in functions across 4 categories:

  • Aggregation: SUM, AVG, COUNT, MIN, MAX (supports nested arrays)
  • Date: TODAY, NOW, DATEADD, DATEDIFF
  • Logic: IF, AND, OR, NOT, SWITCH
  • String: CONCAT, LEFT, RIGHT, TRIM, UPPER, LOWER
const evaluator = new ExpressionEvaluator({ items: [10, 20, 30] });
evaluator.evaluateExpression('IF(SUM(items) > 50, "high", "low")'); // "high"
evaluator.evaluate('${DATEADD(TODAY(), 30, "days")}'); // ISO date string

Custom functions via evaluator.registerFunction('DOUBLE', (n) => n * 2).

Report Export — LiveReportExporter

  • exportWithLiveData() — Fetches via DataSource.find() then routes to any export format
  • exportExcelWithFormulas() — TSV with formula cells (=SUM(A2:A{ROW})), column formatting, and aggregation rows
  • createScheduleTrigger() — Returns a workflow-invokable async function that exports all scheduled formats and fires a completion callback

Transaction Manager

  • executeTransaction() — Sequential action execution with automatic rollback of completed operations on failure. Supports retryOnConflict with exponential backoff.
  • Optimistic updatesapplyOptimisticUpdate()confirmOptimisticUpdate() / rollbackOptimisticUpdate() lifecycle for instant UI feedback
  • executeBatch() — Batch CRUD with progress events, tries DataSource.bulk() first with per-item fallback and retry
const manager = new TransactionManager(dataSource, { maxRetries: 3 });
manager.onProgress((e) => console.log(`${e.percentage}%`));

const result = await manager.executeTransaction({
  name: 'Create Order',
  actions: [createOrder, updateInventory, sendNotification],
  retryOnConflict: true,
}, actionExecutor);
// result.rolledBack === true if any step failed

Test coverage

85 new tests across 3 test files (48 formula, 13 report export, 24 transaction). Full suite green: 2728 passed.


✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

@vercel
Copy link
Copy Markdown

vercel Bot commented Feb 10, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
objectui Error Error Feb 10, 2026 6:55am
objectui-console Error Error Feb 10, 2026 6:55am
objectui-storybook Ready Ready Preview, Comment Feb 10, 2026 6:55am

Request Review

…NT, MIN, MAX, TODAY, NOW, DATEADD, DATEDIFF, IF, AND, OR, NOT, SWITCH, CONCAT, LEFT, RIGHT, TRIM, UPPER, LOWER)

Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Copilot AI and others added 2 commits February 10, 2026 06:43
…le triggers

Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
… and batch progress tracking

Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
…Excel formula injection protection

Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Copilot AI changed the title [WIP] Add full-stack integration features for low-code platform feat: Expression engine formulas, live report export, and transaction manager Feb 10, 2026
Copilot AI requested a review from hotlong February 10, 2026 06:56
@hotlong hotlong marked this pull request as ready for review February 10, 2026 07:20
Copilot AI review requested due to automatic review settings February 10, 2026 07:20
@hotlong hotlong merged commit b9f8351 into main Feb 10, 2026
2 of 4 checks passed
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds three core integration capabilities across the monorepo: formula functions for the expression evaluator, DataSource-backed “live” report export utilities, and a transaction manager for multi-step operations (with rollback/batch/optimistic support).

Changes:

  • Introduces FormulaFunctions and injects them into ExpressionEvaluator (plus tests and exports).
  • Adds LiveReportExporter helpers (exportWithLiveData, Excel-with-formulas TSV export, schedule trigger) and exports them from plugin-report (plus tests).
  • Adds TransactionManager to @object-ui/core/actions and exports it (plus tests).

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
packages/plugin-report/src/index.tsx Re-exports new live export APIs/types from LiveReportExporter.
packages/plugin-report/src/LiveReportExporter.ts Implements DataSource-integrated export, TSV “Excel with formulas”, and schedule trigger helper.
packages/plugin-report/src/tests/LiveReportExporter.test.ts Adds unit tests for live export, TSV export, and scheduling trigger behavior.
packages/core/src/evaluator/index.ts Exports FormulaFunctions from evaluator barrel.
packages/core/src/evaluator/FormulaFunctions.ts Implements built-in formula registry (aggregation/date/logic/string).
packages/core/src/evaluator/tests/FormulaFunctions.test.ts Adds coverage for built-ins and ExpressionEvaluator integration + custom registrations.
packages/core/src/evaluator/ExpressionEvaluator.ts Injects formulas into evaluation context; adds registerFunction() and formula sharing behavior.
packages/core/src/actions/index.ts Exports TransactionManager from actions barrel.
packages/core/src/actions/TransactionManager.ts Implements transaction execution with rollback recording, optimistic updates, and batch operations with retries/progress.
packages/core/src/actions/tests/TransactionManager.test.ts Adds unit tests for transaction execution, rollback, optimistic updates, and batching.

Comment on lines +118 to +120
format: exportFormat,
...report.exportConfigs?.[exportFormat],
...exportConfig,
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mergedConfig sets format: exportFormat but then spreads report.exportConfigs?.[exportFormat] and exportConfig, both of which can contain a format key and silently desync mergedConfig.format from the actual exportFormat passed to exportReport. Consider omitting format from spread inputs (or forcing mergedConfig.format = exportFormat after merging) to keep config consistent.

Suggested change
format: exportFormat,
...report.exportConfigs?.[exportFormat],
...exportConfig,
...report.exportConfigs?.[exportFormat],
...exportConfig,
format: exportFormat,

Copilot uses AI. Check for mistakes.
Comment on lines +218 to +222
// Build TSV content with BOM
const tsvContent = '\uFEFF' + [
headers.join('\t'),
...rows.map(r => r.join('\t')),
].join('\n');
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The TSV generation joins cells with tabs/newlines without escaping, so any value containing \t, \n, or quotes will corrupt row/column structure in Excel. ReportExportEngine.exportAsExcel() already handles escaping/quoting for these cases; consider reusing that escaping logic here (including for formula results when they produce text).

Copilot uses AI. Check for mistakes.
Comment on lines +305 to +310
return { style: 'percent', minimumFractionDigits: 0 };
}
const decimals = (format.match(/0/g) || []).length;
return {
minimumFractionDigits: Math.max(0, decimals - 1),
maximumFractionDigits: Math.max(0, decimals - 1),
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

inferLocaleOptions() infers decimal places by counting all 0 characters in the format string, which miscalculates for formats that include multiple integer-place zeros (e.g. 0000.00 would be treated as 5 fraction digits). Consider deriving fraction digits from the substring after the decimal point only, or requiring explicit Intl.NumberFormatOptions instead of a spreadsheet-style format string.

Suggested change
return { style: 'percent', minimumFractionDigits: 0 };
}
const decimals = (format.match(/0/g) || []).length;
return {
minimumFractionDigits: Math.max(0, decimals - 1),
maximumFractionDigits: Math.max(0, decimals - 1),
// Keep existing behavior for percent-style formats to avoid changing semantics.
return { style: 'percent', minimumFractionDigits: 0 };
}
let decimals = 0;
const decimalPointIndex = format.indexOf('.');
if (decimalPointIndex !== -1) {
const fractionalPart = format.slice(decimalPointIndex + 1);
decimals = (fractionalPart.match(/0/g) || []).length;
}
return {
minimumFractionDigits: decimals,
maximumFractionDigits: decimals,

Copilot uses AI. Check for mistakes.
Comment on lines +345 to +355
function downloadFile(content: string, filename: string, mimeType: string): void {
const blob = new Blob([content], { type: `${mimeType};charset=utf-8` });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
}
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

downloadFile() is duplicated here and in ReportExportEngine.ts (same Blob/URL/anchor logic). Consider extracting a shared helper (e.g. downloadFile.ts) and reusing it to avoid divergence (this file also uses different Excel injection sanitization than sanitizeCSVValue).

Copilot uses AI. Check for mistakes.
Comment on lines +141 to +157
date.setDate(date.getDate() + amount);
break;
case 'month':
case 'months':
date.setMonth(date.getMonth() + amount);
break;
case 'year':
case 'years':
date.setFullYear(date.getFullYear() + amount);
break;
case 'hour':
case 'hours':
date.setHours(date.getHours() + amount);
break;
case 'minute':
case 'minutes':
date.setMinutes(date.getMinutes() + amount);
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DATEADD uses local-time mutators (setDate, setMonth, setFullYear, etc.). When dateStr is an ISO string (often UTC), mixing local mutators with UTC-based inputs can cause off-by-one-hour/day issues around timezones/DST and make behavior environment-dependent. Consider using the UTC variants (setUTCDate/setUTCMonth/...) or doing arithmetic on UTC timestamps to keep results stable.

Suggested change
date.setDate(date.getDate() + amount);
break;
case 'month':
case 'months':
date.setMonth(date.getMonth() + amount);
break;
case 'year':
case 'years':
date.setFullYear(date.getFullYear() + amount);
break;
case 'hour':
case 'hours':
date.setHours(date.getHours() + amount);
break;
case 'minute':
case 'minutes':
date.setMinutes(date.getMinutes() + amount);
date.setUTCDate(date.getUTCDate() + amount);
break;
case 'month':
case 'months':
date.setUTCMonth(date.getUTCMonth() + amount);
break;
case 'year':
case 'years':
date.setUTCFullYear(date.getUTCFullYear() + amount);
break;
case 'hour':
case 'hours':
date.setUTCHours(date.getUTCHours() + amount);
break;
case 'minute':
case 'minutes':
date.setUTCMinutes(date.getUTCMinutes() + amount);

Copilot uses AI. Check for mistakes.
Comment on lines +175 to +193
if (result.success) {
break;
} else {
lastError = result.error;
// Only retry on conflict if enabled
if (config.retryOnConflict && attempt < maxRetries) {
await delay(this.retryDelay * Math.pow(2, attempt));
continue;
}
break;
}
} catch (error) {
lastError = (error as Error).message;
if (config.retryOnConflict && attempt < maxRetries) {
await delay(this.retryDelay * Math.pow(2, attempt));
continue;
}
result = { success: false, error: lastError };
break;
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

retryOnConflict currently retries on any ActionResult.success === false or thrown error, without checking that the failure is actually a conflict. This makes retryOnConflict behave like a generic retry and can amplify non-conflict failures. Consider adding a conflict predicate (e.g. isConflict(result/error) or a status/code on ActionResult) and only retry when it matches.

Copilot uses AI. Check for mistakes.
Comment on lines +145 to +149
const maxRetries = config.maxRetries ?? this.maxRetries;
const actionResults: ActionResult[] = [];

this.operations = [];
let completed = 0;
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

executeTransaction() resets this.operations on the manager instance and uses it for rollback. If the same TransactionManager instance is used for concurrent transactions, this shared mutable state can interleave and rollback the wrong operations. Consider scoping the operations list to the executeTransaction call (or preventing concurrent execution per instance).

Copilot uses AI. Check for mistakes.
Comment on lines +149 to +155
// Inject formula functions into the evaluation context
const formulaObj = this.formulas.toObject();
const mergedContext = { ...formulaObj, ...contextObj };

// Build safe function with context variables
const varNames = Object.keys(contextObj);
const varValues = Object.values(contextObj);
const varNames = Object.keys(mergedContext);
const varValues = Object.values(mergedContext);
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In evaluateExpression, mergedContext spreads formulaObj first and then contextObj, which allows user-provided context keys (e.g. SUM) to shadow built-in formulas and break expression evaluation unexpectedly. Consider giving formulas precedence (spread context first) or explicitly preventing/throwing on collisions with reserved formula names.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants