feat(base): wire inline formula evaluation into row service

This commit is contained in:
Philipinho
2026-04-24 00:16:32 +01:00
parent 2da8779b34
commit 5b5c98daa8
2 changed files with 77 additions and 4 deletions
@@ -0,0 +1,45 @@
import { FormulaService } from "../formula.service";
import type { BaseProperty } from "@docmost/db/types/entity.types";
const mkProp = (
id: string, type: string, typeOptions: any = {},
name = id,
): BaseProperty => ({
id, baseId: "base_1", name, type: type as any, position: "a",
typeOptions, isPrimary: false, workspaceId: "ws_1",
createdAt: new Date(), updatedAt: new Date(),
schemaVersion: 0, pendingType: null, pendingTypeOptions: null,
} as any);
describe("FormulaService.evaluateInline", () => {
const svc = new FormulaService({ add: jest.fn() } as any);
it("computes a formula on create", () => {
const price = mkProp("prop_price", "number", {}, "Price");
const qty = mkProp("prop_qty", "number", {}, "Qty");
const total = mkProp("prop_total", "formula", {
source: 'prop("Price") * prop("Qty")',
ast: { t: "op", op: "*", args: [
{ t: "prop", id: "prop_price" },
{ t: "prop", id: "prop_qty" },
]},
resultType: "number",
dependencies: ["prop_price", "prop_qty"],
astVersion: 1,
}, "Total");
const patch = svc.evaluateInline({
properties: [price, qty, total],
row: { prop_price: 10, prop_qty: 3 },
dirtyProps: ["prop_price", "prop_qty", "prop_total"],
});
expect(patch).toEqual({ prop_total: 30 });
});
it("returns empty patch when no formula is affected", () => {
const price = mkProp("prop_price", "number", {}, "Price");
expect(
svc.evaluateInline({ properties: [price], row: { prop_price: 10 }, dirtyProps: ["prop_price"] }),
).toEqual({});
});
});
@@ -41,6 +41,7 @@ import {
BaseRowUpdatedEvent,
BaseRowsDeletedEvent,
} from '../events/base-events';
import { FormulaService } from '../formula/formula.service';
@Injectable()
export class BaseRowService {
@@ -50,6 +51,7 @@ export class BaseRowService {
private readonly basePropertyRepo: BasePropertyRepo,
private readonly baseViewRepo: BaseViewRepo,
private readonly eventEmitter: EventEmitter2,
private readonly formulaService: FormulaService,
) {}
async create(userId: string, workspaceId: string, dto: CreateRowDto) {
@@ -70,15 +72,29 @@ export class BaseRowService {
position = generateJitteredKeyBetween(lastPosition, null);
}
const properties = await this.basePropertyRepo.findByBaseId(dto.baseId);
let validatedCells: Record<string, unknown> = {};
if (dto.cells && Object.keys(dto.cells).length > 0) {
const properties = await this.basePropertyRepo.findByBaseId(dto.baseId);
validatedCells = this.validateCells(dto.cells, properties);
}
// On create, treat every user-provided cell plus every formula property
// as dirty. The formula patch is merged into the cells we persist.
const dirtyProps = Object.keys(validatedCells);
const formulaPatch = this.formulaService.evaluateInline({
properties,
row: validatedCells,
dirtyProps: [
...dirtyProps,
...properties.filter((p) => p.type === 'formula').map((p) => p.id),
],
});
const finalCells = { ...validatedCells, ...formulaPatch };
const created = await this.baseRowRepo.insertRow({
baseId: dto.baseId,
cells: validatedCells as any,
cells: finalCells as any,
position,
creatorId: userId,
workspaceId,
@@ -108,9 +124,21 @@ export class BaseRowService {
const properties = await this.basePropertyRepo.findByBaseId(dto.baseId);
const validatedCells = this.validateCells(dto.cells, properties);
const existing = await this.baseRowRepo.findById(dto.rowId, { workspaceId });
const mergedRow = {
...((existing?.cells as Record<string, unknown>) ?? {}),
...validatedCells,
};
const formulaPatch = this.formulaService.evaluateInline({
properties,
row: mergedRow,
dirtyProps: Object.keys(validatedCells),
});
const finalCells = { ...validatedCells, ...formulaPatch };
const updated = await this.baseRowRepo.updateCells(
dto.rowId,
validatedCells,
finalCells,
{
baseId: dto.baseId,
workspaceId,
@@ -129,7 +157,7 @@ export class BaseRowService {
requestId: dto.requestId ?? null,
rowId: dto.rowId,
patch: dto.cells,
updatedCells: validatedCells,
updatedCells: finalCells,
};
this.eventEmitter.emit(EventName.BASE_ROW_UPDATED, event);