Skip to content

Commit ef032a2

Browse files
authored
Merge pull request #2366 from teableio/fix/T1506-create-system-formula
fix: ensure system field formulas evaluate on create T1506
2 parents cabd571 + f0458ad commit ef032a2

File tree

2 files changed

+112
-2
lines changed

2 files changed

+112
-2
lines changed

apps/nestjs-backend/src/features/record/record-modify/record-create.service.ts

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import { Injectable } from '@nestjs/common';
22
import type { IMakeOptional, TableDomain } from '@teable/core';
3-
import { CellFormat, FieldKeyType, HttpErrorCode, generateRecordId } from '@teable/core';
3+
import { CellFormat, FieldKeyType, FieldType, HttpErrorCode, generateRecordId } from '@teable/core';
44
import type { ICreateRecordsRo, ICreateRecordsVo } from '@teable/openapi';
55
import { ThresholdConfig, IThresholdConfig } from '../../../configs/threshold.config';
66
import { CustomHttpException } from '../../../custom.exception';
77
import { BatchService } from '../../calculation/batch.service';
88
import { LinkService } from '../../calculation/link.service';
9+
import type { ICellContext } from '../../calculation/utils/changes';
910
import { TableDomainQueryService } from '../../table-domain';
1011
import { ComputedOrchestratorService } from '../computed/services/computed-orchestrator.service';
1112
import type { IRecordInnerRo } from '../record.service';
@@ -85,10 +86,11 @@ export class RecordCreateService {
8586
await this.linkService.getDerivateByLink(table.id, createCtxs, undefined, projectionByTable);
8687
const changes = this.shared.compressAndFilterChanges(table, createCtxs);
8788
const opsMap = this.shared.formatChangesToOps(changes);
89+
const computedCtxs = this.appendSystemFieldContextsForCreate(table, recordIds, createCtxs);
8890
// Publish computed values (with old/new) around base updates
8991
await this.computedOrchestrator.computeCellChangesForRecords(
9092
table.id,
91-
createCtxs,
93+
computedCtxs,
9294
async (tables) => {
9395
await this.batchService.updateRecords(opsMap, undefined, undefined, tables);
9496
}
@@ -131,4 +133,39 @@ export class RecordCreateService {
131133

132134
return projectionIds.size ? { [table.id]: Array.from(projectionIds) } : undefined;
133135
}
136+
137+
private appendSystemFieldContextsForCreate(
138+
table: TableDomain,
139+
recordIds: string[],
140+
cellContexts: ICellContext[]
141+
): ICellContext[] {
142+
if (!recordIds.length) return cellContexts;
143+
144+
const systemFieldIds = table.fieldList
145+
.filter(
146+
(field) =>
147+
field.type === FieldType.CreatedTime ||
148+
field.type === FieldType.CreatedBy ||
149+
field.type === FieldType.LastModifiedTime ||
150+
field.type === FieldType.LastModifiedBy ||
151+
field.type === FieldType.AutoNumber
152+
)
153+
.map((field) => field.id);
154+
155+
if (!systemFieldIds.length) return cellContexts;
156+
157+
const existing = new Set(cellContexts.map((ctx) => `${ctx.recordId}:${ctx.fieldId}`));
158+
const extraContexts: ICellContext[] = [];
159+
160+
for (const recordId of recordIds) {
161+
for (const fieldId of systemFieldIds) {
162+
const key = `${recordId}:${fieldId}`;
163+
if (existing.has(key)) continue;
164+
existing.add(key);
165+
extraContexts.push({ recordId, fieldId });
166+
}
167+
}
168+
169+
return extraContexts.length ? cellContexts.concat(extraContexts) : cellContexts;
170+
}
134171
}

apps/nestjs-backend/test/formula.e2e-spec.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6575,6 +6575,79 @@ describe('OpenAPI formula (e2e)', () => {
65756575
expect(olderRecord.data.fields[formulaField.name]).toEqual('Older');
65766576
});
65776577

6578+
it('should evaluate formula referencing created time on record create', async () => {
6579+
const createdTimeField = await createField(table.id, {
6580+
name: 'Created time',
6581+
type: FieldType.CreatedTime,
6582+
});
6583+
6584+
const formulaField = await createField(table.id, {
6585+
name: 'Created age (days)',
6586+
type: FieldType.Formula,
6587+
options: {
6588+
expression: `DATETIME_DIFF(NOW(), {${createdTimeField.id}}, "day")`,
6589+
timeZone: 'UTC',
6590+
},
6591+
});
6592+
6593+
const created = await createRecords(table.id, {
6594+
fieldKeyType: FieldKeyType.Id,
6595+
records: [{ fields: {} }],
6596+
});
6597+
6598+
const record = await getRecord(table.id, created.records[0].id);
6599+
expect(record.data.fields[formulaField.name]).toEqual(0);
6600+
});
6601+
6602+
it('should evaluate formula referencing created by on record create', async () => {
6603+
const createdByField = await createField(table.id, {
6604+
name: 'Created by',
6605+
type: FieldType.CreatedBy,
6606+
});
6607+
6608+
const formulaField = await createField(table.id, {
6609+
name: 'Creator Name',
6610+
type: FieldType.Formula,
6611+
options: {
6612+
expression: `{${createdByField.id}}`,
6613+
},
6614+
});
6615+
6616+
const created = await createRecords(table.id, {
6617+
fieldKeyType: FieldKeyType.Id,
6618+
records: [{ fields: {} }],
6619+
});
6620+
6621+
const record = await getRecord(table.id, created.records[0].id);
6622+
const createdByValue = record.data.fields[createdByField.name] as { title?: string } | null;
6623+
expect(createdByValue?.title).toBeTruthy();
6624+
expect(record.data.fields[formulaField.name]).toEqual(createdByValue?.title);
6625+
});
6626+
6627+
it('should evaluate formula referencing auto number on record create', async () => {
6628+
const autoNumberField = await createField(table.id, {
6629+
name: 'Auto number',
6630+
type: FieldType.AutoNumber,
6631+
});
6632+
6633+
const formulaField = await createField(table.id, {
6634+
name: 'Auto number x2',
6635+
type: FieldType.Formula,
6636+
options: {
6637+
expression: `{${autoNumberField.id}} * 2`,
6638+
},
6639+
});
6640+
6641+
const created = await createRecords(table.id, {
6642+
fieldKeyType: FieldKeyType.Id,
6643+
records: [{ fields: {} }],
6644+
});
6645+
6646+
const record = await getRecord(table.id, created.records[0].id);
6647+
const autoNumberValue = record.data.fields[autoNumberField.name] as number;
6648+
expect(record.data.fields[formulaField.name]).toEqual(autoNumberValue * 2);
6649+
});
6650+
65786651
it('should evaluate timezone-aware formatting formulas referencing fields', async () => {
65796652
const dateField = await createField(table.id, {
65806653
name: 'tz source',

0 commit comments

Comments
 (0)