• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

teableio / teable / 8389227144

22 Mar 2024 10:56AM UTC coverage: 26.087% (-53.9%) from 79.937%
8389227144

push

github

web-flow
refactor: move zod schema to openapi (#487)

2100 of 3363 branches covered (62.44%)

282 of 757 new or added lines in 74 files covered. (37.25%)

14879 existing lines in 182 files now uncovered.

25574 of 98035 relevant lines covered (26.09%)

5.17 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

15.71
/apps/nestjs-backend/src/features/record/record-calculate/record-calculate.service.ts
1
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
1✔
2
import { FieldKeyType, generateRecordId, RecordOpBuilder, FieldType } from '@teable/core';
1✔
3
import { PrismaService } from '@teable/db-main-prisma';
1✔
4
import type { ICreateRecordsRo, ICreateRecordsVo, IRecord } from '@teable/openapi';
1✔
5
import { isEmpty, keyBy } from 'lodash';
1✔
6
import { BatchService } from '../../calculation/batch.service';
1✔
7
import { FieldCalculationService } from '../../calculation/field-calculation.service';
1✔
8
import { LinkService } from '../../calculation/link.service';
1✔
9
import type { ICellContext } from '../../calculation/link.service';
1✔
10
import type { IOpsMap } from '../../calculation/reference.service';
1✔
11
import { ReferenceService } from '../../calculation/reference.service';
1✔
12
import { SystemFieldService } from '../../calculation/system-field.service';
1✔
13
import { formatChangesToOps } from '../../calculation/utils/changes';
1✔
14
import { composeOpMaps } from '../../calculation/utils/compose-maps';
1✔
15
import { RecordService } from '../record.service';
1✔
16

1✔
17
@Injectable()
1✔
18
export class RecordCalculateService {
1✔
19
  constructor(
41✔
20
    private readonly batchService: BatchService,
41✔
21
    private readonly prismaService: PrismaService,
41✔
22
    private readonly recordService: RecordService,
41✔
23
    private readonly linkService: LinkService,
41✔
24
    private readonly referenceService: ReferenceService,
41✔
25
    private readonly fieldCalculationService: FieldCalculationService,
41✔
26
    private readonly systemFieldService: SystemFieldService
41✔
27
  ) {}
41✔
28

41✔
29
  async multipleCreateRecords(
41✔
30
    tableId: string,
×
31
    createRecordsRo: ICreateRecordsRo
×
32
  ): Promise<ICreateRecordsVo> {
×
33
    return await this.prismaService.$tx(async () => {
×
34
      return await this.createRecords(
×
35
        tableId,
×
36
        createRecordsRo.records,
×
37
        createRecordsRo.fieldKeyType
×
38
      );
×
39
    });
×
40
  }
×
41

41✔
42
  private async generateCellContexts(
41✔
UNCOV
43
    tableId: string,
×
UNCOV
44
    fieldKeyType: FieldKeyType,
×
UNCOV
45
    records: { id: string; fields: { [fieldNameOrId: string]: unknown } }[],
×
UNCOV
46
    isNewRecord?: boolean
×
UNCOV
47
  ) {
×
UNCOV
48
    const fieldKeys = Array.from(
×
UNCOV
49
      records.reduce<Set<string>>((acc, record) => {
×
UNCOV
50
        Object.keys(record.fields).forEach((fieldNameOrId) => acc.add(fieldNameOrId));
×
UNCOV
51
        return acc;
×
UNCOV
52
      }, new Set())
×
UNCOV
53
    );
×
UNCOV
54

×
UNCOV
55
    const fieldRaws = await this.prismaService.txClient().field.findMany({
×
UNCOV
56
      where: { tableId, [fieldKeyType]: { in: fieldKeys } },
×
UNCOV
57
      select: { id: true, name: true },
×
UNCOV
58
    });
×
UNCOV
59
    const fieldIdMap = keyBy(fieldRaws, fieldKeyType);
×
UNCOV
60

×
UNCOV
61
    const cellContexts: ICellContext[] = [];
×
UNCOV
62

×
UNCOV
63
    let oldRecordsMap: Record<string, IRecord> = {};
×
UNCOV
64
    if (!isNewRecord) {
×
UNCOV
65
      const oldRecords = (
×
UNCOV
66
        await this.recordService.getSnapshotBulk(
×
UNCOV
67
          tableId,
×
UNCOV
68
          records.map((r) => r.id)
×
UNCOV
69
        )
×
UNCOV
70
      ).map((s) => s.data);
×
UNCOV
71
      oldRecordsMap = keyBy(oldRecords, 'id');
×
UNCOV
72
    }
×
UNCOV
73

×
UNCOV
74
    for (const record of records) {
×
UNCOV
75
      Object.entries(record.fields).forEach(([fieldNameOrId, value]) => {
×
UNCOV
76
        if (!fieldIdMap[fieldNameOrId]) {
×
77
          throw new NotFoundException(`Field ${fieldNameOrId} not found`);
×
78
        }
×
UNCOV
79
        const fieldId = fieldIdMap[fieldNameOrId].id;
×
UNCOV
80
        const oldCellValue = isNewRecord ? null : oldRecordsMap[record.id].fields[fieldId];
×
UNCOV
81
        cellContexts.push({
×
UNCOV
82
          recordId: record.id,
×
UNCOV
83
          fieldId,
×
UNCOV
84
          newValue: value,
×
UNCOV
85
          oldValue: oldCellValue,
×
UNCOV
86
        });
×
UNCOV
87
      });
×
UNCOV
88
    }
×
UNCOV
89
    return cellContexts;
×
UNCOV
90
  }
×
91

41✔
92
  private async getRecordUpdateDerivation(
41✔
UNCOV
93
    tableId: string,
×
UNCOV
94
    opsMapOrigin: IOpsMap,
×
UNCOV
95
    opContexts: ICellContext[]
×
UNCOV
96
  ) {
×
UNCOV
97
    const derivate = await this.linkService.getDerivateByLink(tableId, opContexts);
×
UNCOV
98

×
UNCOV
99
    const cellChanges = derivate?.cellChanges || [];
×
UNCOV
100

×
UNCOV
101
    const opsMapByLink = cellChanges.length ? formatChangesToOps(cellChanges) : {};
×
UNCOV
102
    const composedOpsMap = composeOpMaps([opsMapOrigin, opsMapByLink]);
×
UNCOV
103
    const systemFieldOpsMap = await this.systemFieldService.getOpsMapBySystemField(composedOpsMap);
×
UNCOV
104

×
UNCOV
105
    // calculate by origin ops and link derivation
×
UNCOV
106
    const {
×
UNCOV
107
      opsMap: opsMapByCalculation,
×
UNCOV
108
      fieldMap,
×
UNCOV
109
      tableId2DbTableName,
×
UNCOV
110
    } = await this.referenceService.calculateOpsMap(composedOpsMap, derivate?.saveForeignKeyToDb);
×
UNCOV
111

×
UNCOV
112
    // console.log('opsMapByCalculation', JSON.stringify(opsMapByCalculation, null, 2));
×
UNCOV
113
    return {
×
UNCOV
114
      opsMap: composeOpMaps([opsMapOrigin, opsMapByLink, opsMapByCalculation, systemFieldOpsMap]),
×
UNCOV
115
      fieldMap,
×
UNCOV
116
      tableId2DbTableName,
×
UNCOV
117
    };
×
UNCOV
118
  }
×
119

41✔
120
  async calculateDeletedRecord(tableId: string, recordIds: string[]) {
41✔
UNCOV
121
    const cellContextsByTableId = await this.linkService.getDeleteRecordUpdateContext(
×
UNCOV
122
      tableId,
×
UNCOV
123
      recordIds
×
UNCOV
124
    );
×
UNCOV
125

×
UNCOV
126
    // console.log('calculateDeletedRecord', tableId, recordIds);
×
UNCOV
127

×
UNCOV
128
    for (const effectedTableId in cellContextsByTableId) {
×
UNCOV
129
      const cellContexts = cellContextsByTableId[effectedTableId];
×
UNCOV
130
      const opsMapOrigin = formatChangesToOps(
×
UNCOV
131
        cellContexts.map((data) => {
×
UNCOV
132
          return {
×
UNCOV
133
            tableId: effectedTableId,
×
UNCOV
134
            recordId: data.recordId,
×
UNCOV
135
            fieldId: data.fieldId,
×
UNCOV
136
            newValue: data.newValue,
×
UNCOV
137
            oldValue: data.oldValue,
×
UNCOV
138
          };
×
UNCOV
139
        })
×
UNCOV
140
      );
×
UNCOV
141

×
UNCOV
142
      // 2. get cell changes by derivation
×
UNCOV
143
      const { opsMap, fieldMap, tableId2DbTableName } = await this.getRecordUpdateDerivation(
×
UNCOV
144
        effectedTableId,
×
UNCOV
145
        opsMapOrigin,
×
UNCOV
146
        cellContexts
×
UNCOV
147
      );
×
UNCOV
148

×
UNCOV
149
      // 3. save all ops
×
UNCOV
150
      if (!isEmpty(opsMap)) {
×
UNCOV
151
        await this.batchService.updateRecords(opsMap, fieldMap, tableId2DbTableName);
×
UNCOV
152
      }
×
UNCOV
153
    }
×
UNCOV
154
  }
×
155

41✔
156
  async calculateUpdatedRecord(
41✔
UNCOV
157
    tableId: string,
×
UNCOV
158
    fieldKeyType: FieldKeyType = FieldKeyType.Name,
×
UNCOV
159
    records: { id: string; fields: { [fieldNameOrId: string]: unknown } }[],
×
UNCOV
160
    isNewRecord?: boolean
×
UNCOV
161
  ) {
×
UNCOV
162
    // 1. generate Op by origin submit
×
UNCOV
163
    const opsContexts = await this.generateCellContexts(
×
UNCOV
164
      tableId,
×
UNCOV
165
      fieldKeyType,
×
UNCOV
166
      records,
×
UNCOV
167
      isNewRecord
×
UNCOV
168
    );
×
UNCOV
169

×
UNCOV
170
    const opsMapOrigin = formatChangesToOps(
×
UNCOV
171
      opsContexts.map((data) => {
×
UNCOV
172
        return {
×
UNCOV
173
          tableId,
×
UNCOV
174
          recordId: data.recordId,
×
UNCOV
175
          fieldId: data.fieldId,
×
UNCOV
176
          newValue: data.newValue,
×
UNCOV
177
          oldValue: data.oldValue,
×
UNCOV
178
        };
×
UNCOV
179
      })
×
UNCOV
180
    );
×
UNCOV
181

×
UNCOV
182
    // 2. get cell changes by derivation
×
UNCOV
183
    const { opsMap, fieldMap, tableId2DbTableName } = await this.getRecordUpdateDerivation(
×
UNCOV
184
      tableId,
×
UNCOV
185
      opsMapOrigin,
×
UNCOV
186
      opsContexts
×
UNCOV
187
    );
×
UNCOV
188

×
UNCOV
189
    // console.log('final:opsMap', JSON.stringify(opsMap, null, 2));
×
UNCOV
190

×
UNCOV
191
    // 3. save all ops
×
UNCOV
192
    if (!isEmpty(opsMap)) {
×
UNCOV
193
      await this.batchService.updateRecords(opsMap, fieldMap, tableId2DbTableName);
×
UNCOV
194
    }
×
UNCOV
195
  }
×
196

41✔
197
  private async appendDefaultValue(
41✔
UNCOV
198
    tableId: string,
×
UNCOV
199
    records: { id: string; fields: { [fieldNameOrId: string]: unknown } }[],
×
UNCOV
200
    fieldKeyType: FieldKeyType
×
UNCOV
201
  ) {
×
UNCOV
202
    const fieldRaws = await this.prismaService.txClient().field.findMany({
×
UNCOV
203
      where: { tableId, deletedTime: null },
×
UNCOV
204
      select: { id: true, name: true, type: true, options: true },
×
UNCOV
205
    });
×
UNCOV
206

×
UNCOV
207
    return records.map((record) => {
×
UNCOV
208
      const fields: { [fieldIdOrName: string]: unknown } = { ...record.fields };
×
UNCOV
209
      for (const fieldRaw of fieldRaws) {
×
UNCOV
210
        const { type, options } = fieldRaw;
×
UNCOV
211
        if (options == null) continue;
×
UNCOV
212
        const { defaultValue } = JSON.parse(options) || {};
×
UNCOV
213
        if (defaultValue == null) continue;
×
UNCOV
214
        const fieldIdOrName = fieldRaw[fieldKeyType];
×
UNCOV
215
        if (fields[fieldIdOrName] != null) continue;
×
UNCOV
216
        fields[fieldIdOrName] = this.getDefaultValue(type as FieldType, defaultValue);
×
UNCOV
217
      }
×
UNCOV
218

×
UNCOV
219
      return {
×
UNCOV
220
        ...record,
×
UNCOV
221
        fields,
×
UNCOV
222
      };
×
UNCOV
223
    });
×
UNCOV
224
  }
×
225

41✔
226
  private getDefaultValue(type: FieldType, defaultValue: unknown) {
41✔
UNCOV
227
    if (type === FieldType.Date && defaultValue === 'now') {
×
UNCOV
228
      return new Date().toISOString();
×
UNCOV
229
    }
×
230
    return defaultValue;
×
231
  }
×
232

41✔
233
  async createRecords(
41✔
UNCOV
234
    tableId: string,
×
UNCOV
235
    recordsRo: {
×
UNCOV
236
      id?: string;
×
UNCOV
237
      fields: Record<string, unknown>;
×
UNCOV
238
    }[],
×
UNCOV
239
    fieldKeyType: FieldKeyType = FieldKeyType.Name,
×
UNCOV
240
    orderIndex?: { viewId: string; indexes: number[] }
×
UNCOV
241
  ): Promise<ICreateRecordsVo> {
×
UNCOV
242
    if (recordsRo.length === 0) {
×
243
      throw new BadRequestException('Create records is empty');
×
244
    }
×
UNCOV
245

×
UNCOV
246
    const emptyRecords = recordsRo.map((record) => {
×
UNCOV
247
      const recordId = record.id || generateRecordId();
×
UNCOV
248
      return RecordOpBuilder.creator.build({
×
UNCOV
249
        id: recordId,
×
UNCOV
250
        fields: {},
×
UNCOV
251
      });
×
UNCOV
252
    });
×
UNCOV
253

×
UNCOV
254
    await this.recordService.batchCreateRecords(tableId, emptyRecords, orderIndex);
×
UNCOV
255

×
UNCOV
256
    // submit auto fill changes
×
UNCOV
257
    const plainRecords = await this.appendDefaultValue(
×
UNCOV
258
      tableId,
×
UNCOV
259
      recordsRo.map((s, i) => ({ id: emptyRecords[i].id, fields: s.fields })),
×
UNCOV
260
      fieldKeyType
×
UNCOV
261
    );
×
UNCOV
262

×
UNCOV
263
    const recordIds = plainRecords.map((r) => r.id);
×
UNCOV
264

×
UNCOV
265
    await this.calculateUpdatedRecord(tableId, fieldKeyType, plainRecords, true);
×
UNCOV
266

×
UNCOV
267
    await this.fieldCalculationService.calculateFieldsByRecordIds(tableId, recordIds);
×
UNCOV
268

×
UNCOV
269
    const snapshots = await this.recordService.getSnapshotBulk(
×
UNCOV
270
      tableId,
×
UNCOV
271
      recordIds,
×
UNCOV
272
      undefined,
×
UNCOV
273
      fieldKeyType
×
UNCOV
274
    );
×
UNCOV
275

×
UNCOV
276
    return {
×
UNCOV
277
      records: snapshots.map((snapshot) => snapshot.data),
×
UNCOV
278
    };
×
UNCOV
279
  }
×
280
}
41✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc