diff --git a/apps/server/src/core/base/formula/__tests__/formula-service.spec.ts b/apps/server/src/core/base/formula/__tests__/formula-service.spec.ts new file mode 100644 index 000000000..b19c80d51 --- /dev/null +++ b/apps/server/src/core/base/formula/__tests__/formula-service.spec.ts @@ -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({}); + }); +}); diff --git a/apps/server/src/core/base/services/base-row.service.ts b/apps/server/src/core/base/services/base-row.service.ts index 0ce49d147..4e8506736 100644 --- a/apps/server/src/core/base/services/base-row.service.ts +++ b/apps/server/src/core/base/services/base-row.service.ts @@ -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 = {}; 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) ?? {}), + ...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);