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

teableio / teable / 8419847094

25 Mar 2024 12:12PM CUT coverage: 26.087% (-53.9%) from 79.94%
8419847094

push

github

web-flow
chore: husky to v9 and upgrade more deps (#494)

2100 of 3363 branches covered (62.44%)

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

12.48
/apps/nestjs-backend/src/features/calculation/link.service.ts
1
import { BadRequestException, Injectable } from '@nestjs/common';
1✔
2
import type { ILinkCellValue, ILinkFieldOptions } from '@teable/core';
1✔
3
import { FieldType, Relationship } from '@teable/core';
1✔
4
import type { Field } from '@teable/db-main-prisma';
1✔
5
import { PrismaService } from '@teable/db-main-prisma';
1✔
6
import { Knex } from 'knex';
1✔
7
import { cloneDeep, keyBy, difference, groupBy, isEqual, set } from 'lodash';
1✔
8
import { InjectModel } from 'nest-knexjs';
1✔
9
import type { IFieldInstance, IFieldMap } from '../field/model/factory';
1✔
10
import { createFieldInstanceByRaw } from '../field/model/factory';
1✔
11
import type { LinkFieldDto } from '../field/model/field-dto/link-field.dto';
1✔
12
import { SchemaType } from '../field/util';
1✔
13
import { BatchService } from './batch.service';
1✔
14
import type { ICellChange } from './utils/changes';
1✔
15
import { isLinkCellValue } from './utils/detect-link';
1✔
16

1✔
17
export interface IFkRecordMap {
1✔
18
  [fieldId: string]: {
1✔
19
    [recordId: string]: IFkRecordItem;
1✔
20
  };
1✔
21
}
1✔
22

1✔
23
export interface IFkRecordItem {
1✔
24
  oldKey: string | string[] | null; // null means record have no foreignKey
1✔
25
  newKey: string | string[] | null; // null means to delete the foreignKey
1✔
26
}
1✔
27

1✔
28
export interface IRecordMapByTableId {
1✔
29
  [tableId: string]: {
1✔
30
    [recordId: string]: {
1✔
31
      [fieldId: string]: unknown;
1✔
32
    };
1✔
33
  };
1✔
34
}
1✔
35

1✔
36
export interface IFieldMapByTableId {
1✔
37
  [tableId: string]: {
1✔
38
    [fieldId: string]: IFieldInstance;
1✔
39
  };
1✔
40
}
1✔
41

1✔
42
export interface ILinkCellContext {
1✔
43
  recordId: string;
1✔
44
  fieldId: string;
1✔
45
  newValue?: { id: string }[] | { id: string };
1✔
46
  oldValue?: { id: string }[] | { id: string };
1✔
47
}
1✔
48

1✔
49
export interface ICellContext {
1✔
50
  recordId: string;
1✔
51
  fieldId: string;
1✔
52
  newValue?: unknown;
1✔
53
  oldValue?: unknown;
1✔
54
}
1✔
55

1✔
56
@Injectable()
1✔
57
export class LinkService {
1✔
58
  constructor(
150✔
59
    private readonly prismaService: PrismaService,
150✔
60
    private readonly batchService: BatchService,
150✔
61
    @InjectModel('CUSTOM_KNEX') private readonly knex: Knex
150✔
62
  ) {}
150✔
63

150✔
64
  private validateLinkCell(cell: ILinkCellContext) {
150✔
65
    if (!Array.isArray(cell.newValue)) {
×
66
      return cell;
×
67
    }
×
68
    const checkSet = new Set<string>();
×
69
    cell.newValue.forEach((v) => {
×
70
      if (checkSet.has(v.id)) {
×
71
        throw new BadRequestException(`Cannot set duplicate recordId: ${v.id} in the same cell`);
×
72
      }
×
73
      checkSet.add(v.id);
×
74
    });
×
75
    return cell;
×
76
  }
×
77

150✔
78
  private filterLinkContext(contexts: ILinkCellContext[]): ILinkCellContext[] {
150✔
79
    return contexts
×
80
      .filter((ctx) => {
×
81
        if (isLinkCellValue(ctx.newValue)) {
×
82
          return true;
×
83
        }
×
84

×
85
        return isLinkCellValue(ctx.oldValue);
×
86
      })
×
87
      .map((ctx) => {
×
88
        this.validateLinkCell(ctx);
×
89
        return { ...ctx, oldValue: isLinkCellValue(ctx.oldValue) ? ctx.oldValue : undefined };
×
90
      });
×
91
  }
×
92

150✔
93
  private async getRelatedFieldMap(fieldIds: string[]): Promise<IFieldMapByTableId> {
150✔
94
    const fieldRaws = await this.prismaService.txClient().field.findMany({
×
95
      where: { id: { in: fieldIds } },
×
96
    });
×
97
    const fields = fieldRaws.map(createFieldInstanceByRaw) as LinkFieldDto[];
×
98

×
99
    const symmetricFieldRaws = await this.prismaService.txClient().field.findMany({
×
100
      where: {
×
101
        id: {
×
102
          in: fields
×
103
            .filter((field) => field.options.symmetricFieldId)
×
104
            .map((field) => field.options.symmetricFieldId as string),
×
105
        },
×
106
      },
×
107
    });
×
108

×
109
    const symmetricFields = symmetricFieldRaws.map(createFieldInstanceByRaw) as LinkFieldDto[];
×
110

×
111
    const lookedFieldRaws = await this.prismaService.txClient().field.findMany({
×
112
      where: {
×
113
        id: {
×
114
          in: fields
×
115
            .map((field) => field.options.lookupFieldId)
×
116
            .concat(symmetricFields.map((field) => field.options.lookupFieldId)),
×
117
        },
×
118
      },
×
119
    });
×
120
    const lookedFields = lookedFieldRaws.map(createFieldInstanceByRaw);
×
121

×
122
    const instanceMap = keyBy([...fields, ...symmetricFields, ...lookedFields], 'id');
×
123

×
124
    return [...fieldRaws, ...symmetricFieldRaws, ...lookedFieldRaws].reduce<IFieldMapByTableId>(
×
125
      (acc, field) => {
×
126
        const { tableId, id } = field;
×
127
        if (!acc[tableId]) {
×
128
          acc[tableId] = {};
×
129
        }
×
130
        acc[tableId][id] = instanceMap[id];
×
131
        return acc;
×
132
      },
×
133
      {}
×
134
    );
×
135
  }
×
136

150✔
137
  // eslint-disable-next-line sonarjs/cognitive-complexity
150✔
138
  private updateForeignCellForManyMany(params: {
150✔
139
    fkItem: IFkRecordItem;
×
140
    recordId: string;
×
141
    symmetricFieldId: string;
×
142
    sourceLookedFieldId: string;
×
143
    sourceRecordMap: IRecordMapByTableId['tableId'];
×
144
    foreignRecordMap: IRecordMapByTableId['tableId'];
×
145
  }) {
×
146
    const {
×
147
      fkItem,
×
148
      recordId,
×
149
      symmetricFieldId,
×
150
      sourceLookedFieldId,
×
151
      foreignRecordMap,
×
152
      sourceRecordMap,
×
153
    } = params;
×
154
    const oldKey = (fkItem.oldKey || []) as string[];
×
155
    const newKey = (fkItem.newKey || []) as string[];
×
156

×
157
    const toDelete = difference(oldKey, newKey);
×
158
    const toAdd = difference(newKey, oldKey);
×
159

×
160
    // Update link cell values for symmetric field of the foreign table
×
161
    if (toDelete.length) {
×
162
      toDelete.forEach((foreignRecordId) => {
×
163
        const foreignCellValue = foreignRecordMap[foreignRecordId][symmetricFieldId] as
×
164
          | ILinkCellValue[]
×
165
          | null;
×
166

×
167
        if (foreignCellValue) {
×
168
          const filteredCellValue = foreignCellValue.filter((item) => item.id !== recordId);
×
169
          foreignRecordMap[foreignRecordId][symmetricFieldId] = filteredCellValue.length
×
170
            ? filteredCellValue
×
171
            : null;
×
172
        }
×
173
      });
×
174
    }
×
175

×
176
    if (toAdd.length) {
×
177
      toAdd.forEach((foreignRecordId) => {
×
178
        const sourceRecordTitle = sourceRecordMap[recordId][sourceLookedFieldId] as
×
179
          | string
×
180
          | undefined;
×
181
        const newForeignRecord = foreignRecordMap[foreignRecordId];
×
182
        if (!newForeignRecord) {
×
183
          throw new BadRequestException(
×
184
            `Consistency error, recordId ${foreignRecordId} is not exist`
×
185
          );
×
186
        }
×
187
        const foreignCellValue = newForeignRecord[symmetricFieldId] as ILinkCellValue[] | null;
×
188
        if (foreignCellValue) {
×
189
          newForeignRecord[symmetricFieldId] = foreignCellValue.concat({
×
190
            id: recordId,
×
191
            title: sourceRecordTitle,
×
192
          });
×
193
        } else {
×
194
          newForeignRecord[symmetricFieldId] = [{ id: recordId, title: sourceRecordTitle }];
×
195
        }
×
196
      });
×
197
    }
×
198
  }
×
199

150✔
200
  private updateForeignCellForManyOne(params: {
150✔
201
    fkItem: IFkRecordItem;
×
202
    recordId: string;
×
203
    symmetricFieldId: string;
×
204
    sourceLookedFieldId: string;
×
205
    sourceRecordMap: IRecordMapByTableId['tableId'];
×
206
    foreignRecordMap: IRecordMapByTableId['tableId'];
×
207
  }) {
×
208
    const {
×
209
      fkItem,
×
210
      recordId,
×
211
      symmetricFieldId,
×
212
      sourceLookedFieldId,
×
213
      foreignRecordMap,
×
214
      sourceRecordMap,
×
215
    } = params;
×
216
    const oldKey = fkItem.oldKey as string | null;
×
217
    const newKey = fkItem.newKey as string | null;
×
218

×
219
    // Update link cell values for symmetric field of the foreign table
×
220
    if (oldKey) {
×
221
      const foreignCellValue = foreignRecordMap[oldKey][symmetricFieldId] as
×
222
        | ILinkCellValue[]
×
223
        | null;
×
224

×
225
      if (foreignCellValue) {
×
226
        const filteredCellValue = foreignCellValue.filter((item) => item.id !== recordId);
×
227
        foreignRecordMap[oldKey][symmetricFieldId] = filteredCellValue.length
×
228
          ? filteredCellValue
×
229
          : null;
×
230
      }
×
231
    }
×
232

×
233
    if (newKey) {
×
234
      const sourceRecordTitle = sourceRecordMap[recordId][sourceLookedFieldId] as
×
235
        | string
×
236
        | undefined;
×
237
      const newForeignRecord = foreignRecordMap[newKey];
×
238
      if (!newForeignRecord) {
×
239
        throw new BadRequestException(`Consistency error, recordId ${newKey} is not exist`);
×
240
      }
×
241
      const foreignCellValue = newForeignRecord[symmetricFieldId] as ILinkCellValue[] | null;
×
242
      if (foreignCellValue) {
×
243
        newForeignRecord[symmetricFieldId] = foreignCellValue.concat({
×
244
          id: recordId,
×
245
          title: sourceRecordTitle,
×
246
        });
×
247
      } else {
×
248
        newForeignRecord[symmetricFieldId] = [{ id: recordId, title: sourceRecordTitle }];
×
249
      }
×
250
    }
×
251
  }
×
252

150✔
253
  private updateForeignCellForOneMany(params: {
150✔
254
    fkItem: IFkRecordItem;
×
255
    recordId: string;
×
256
    symmetricFieldId: string;
×
257
    sourceLookedFieldId: string;
×
258
    sourceRecordMap: IRecordMapByTableId['tableId'];
×
259
    foreignRecordMap: IRecordMapByTableId['tableId'];
×
260
  }) {
×
261
    const {
×
262
      fkItem,
×
263
      recordId,
×
264
      symmetricFieldId,
×
265
      sourceLookedFieldId,
×
266
      foreignRecordMap,
×
267
      sourceRecordMap,
×
268
    } = params;
×
269

×
270
    const oldKey = (fkItem.oldKey || []) as string[];
×
271
    const newKey = (fkItem.newKey || []) as string[];
×
272

×
273
    const toDelete = difference(oldKey, newKey);
×
274
    const toAdd = difference(newKey, oldKey);
×
275

×
276
    if (toDelete.length) {
×
277
      toDelete.forEach((foreignRecordId) => {
×
278
        foreignRecordMap[foreignRecordId][symmetricFieldId] = null;
×
279
      });
×
280
    }
×
281

×
282
    if (toAdd.length) {
×
283
      const sourceRecordTitle = sourceRecordMap[recordId][sourceLookedFieldId] as
×
284
        | string
×
285
        | undefined;
×
286

×
287
      toAdd.forEach((foreignRecordId) => {
×
288
        foreignRecordMap[foreignRecordId][symmetricFieldId] = {
×
289
          id: recordId,
×
290
          title: sourceRecordTitle,
×
291
        };
×
292
      });
×
293
    }
×
294
  }
×
295

150✔
296
  private updateForeignCellForOneOne(params: {
150✔
297
    fkItem: IFkRecordItem;
×
298
    recordId: string;
×
299
    symmetricFieldId: string;
×
300
    sourceLookedFieldId: string;
×
301
    sourceRecordMap: IRecordMapByTableId['tableId'];
×
302
    foreignRecordMap: IRecordMapByTableId['tableId'];
×
303
  }) {
×
304
    const {
×
305
      fkItem,
×
306
      recordId,
×
307
      symmetricFieldId,
×
308
      sourceLookedFieldId,
×
309
      foreignRecordMap,
×
310
      sourceRecordMap,
×
311
    } = params;
×
312

×
313
    const oldKey = fkItem.oldKey as string | undefined;
×
314
    const newKey = fkItem.newKey as string | undefined;
×
315

×
316
    if (oldKey) {
×
317
      foreignRecordMap[oldKey][symmetricFieldId] = null;
×
318
    }
×
319

×
320
    if (newKey) {
×
321
      const sourceRecordTitle = sourceRecordMap[recordId][sourceLookedFieldId] as
×
322
        | string
×
323
        | undefined;
×
324

×
325
      foreignRecordMap[newKey][symmetricFieldId] = {
×
326
        id: recordId,
×
327
        title: sourceRecordTitle,
×
328
      };
×
329
    }
×
330
  }
×
331

150✔
332
  // update link cellValue title for the user input value of the source table
150✔
333
  private fixLinkCellTitle(params: {
150✔
334
    newKey: string | string[] | null;
×
335
    recordId: string;
×
336
    linkFieldId: string;
×
337
    foreignLookedFieldId: string;
×
338
    sourceRecordMap: IRecordMapByTableId['tableId'];
×
339
    foreignRecordMap: IRecordMapByTableId['tableId'];
×
340
  }) {
×
341
    const {
×
342
      newKey,
×
343
      recordId,
×
344
      linkFieldId,
×
345
      foreignLookedFieldId,
×
346
      foreignRecordMap,
×
347
      sourceRecordMap,
×
348
    } = params;
×
349

×
350
    if (!newKey) {
×
351
      return;
×
352
    }
×
353

×
354
    if (Array.isArray(newKey)) {
×
355
      sourceRecordMap[recordId][linkFieldId] = newKey.map((key) => ({
×
356
        id: key,
×
357
        title: foreignRecordMap[key][foreignLookedFieldId] as string | undefined,
×
358
      }));
×
359
      return;
×
360
    }
×
361

×
362
    const foreignRecordTitle = foreignRecordMap[newKey][foreignLookedFieldId] as string | undefined;
×
363
    sourceRecordMap[recordId][linkFieldId] = { id: newKey, title: foreignRecordTitle };
×
364
  }
×
365

150✔
366
  // eslint-disable-next-line sonarjs/cognitive-complexity
150✔
367
  private async updateLinkRecord(
150✔
368
    tableId: string,
×
369
    fkRecordMap: IFkRecordMap,
×
370
    fieldMapByTableId: { [tableId: string]: IFieldMap },
×
371
    originRecordMapByTableId: IRecordMapByTableId
×
372
  ): Promise<IRecordMapByTableId> {
×
373
    const recordMapByTableId = cloneDeep(originRecordMapByTableId);
×
374
    for (const fieldId in fkRecordMap) {
×
375
      const linkField = fieldMapByTableId[tableId][fieldId] as LinkFieldDto;
×
376
      const linkFieldId = linkField.id;
×
377
      const relationship = linkField.options.relationship;
×
378
      const foreignTableId = linkField.options.foreignTableId;
×
379
      const foreignLookedFieldId = linkField.options.lookupFieldId;
×
380

×
381
      const sourceRecordMap = recordMapByTableId[tableId];
×
382
      const foreignRecordMap = recordMapByTableId[foreignTableId];
×
383
      const symmetricFieldId = linkField.options.symmetricFieldId;
×
384

×
385
      for (const recordId in fkRecordMap[fieldId]) {
×
386
        const fkItem = fkRecordMap[fieldId][recordId];
×
387

×
388
        this.fixLinkCellTitle({
×
389
          newKey: fkItem.newKey,
×
390
          recordId,
×
391
          linkFieldId,
×
392
          foreignLookedFieldId,
×
393
          sourceRecordMap,
×
394
          foreignRecordMap,
×
395
        });
×
396

×
397
        if (!symmetricFieldId) {
×
398
          continue;
×
399
        }
×
400
        const symmetricField = fieldMapByTableId[foreignTableId][symmetricFieldId] as LinkFieldDto;
×
401
        const sourceLookedFieldId = symmetricField.options.lookupFieldId;
×
402
        const params = {
×
403
          fkItem,
×
404
          recordId,
×
405
          symmetricFieldId,
×
406
          sourceLookedFieldId,
×
407
          sourceRecordMap,
×
408
          foreignRecordMap,
×
409
        };
×
410
        if (relationship === Relationship.ManyMany) {
×
411
          this.updateForeignCellForManyMany(params);
×
412
        }
×
413
        if (relationship === Relationship.ManyOne) {
×
414
          this.updateForeignCellForManyOne(params);
×
415
        }
×
416
        if (relationship === Relationship.OneMany) {
×
417
          this.updateForeignCellForOneMany(params);
×
418
        }
×
419
        if (relationship === Relationship.OneOne) {
×
420
          this.updateForeignCellForOneOne(params);
×
421
        }
×
422
      }
×
423
    }
×
424
    return recordMapByTableId;
×
425
  }
×
426

150✔
427
  private async getForeignKeys(
150✔
428
    recordIds: string[],
×
429
    linkRecordIds: string[],
×
430
    options: ILinkFieldOptions
×
431
  ) {
×
432
    const { fkHostTableName, selfKeyName, foreignKeyName } = options;
×
433

×
434
    const query = this.knex(fkHostTableName)
×
435
      .select({
×
436
        id: selfKeyName,
×
437
        foreignId: foreignKeyName,
×
438
      })
×
439
      .whereIn(selfKeyName, recordIds)
×
440
      .orWhereIn(foreignKeyName, linkRecordIds)
×
441
      .whereNotNull(selfKeyName)
×
442
      .whereNotNull(foreignKeyName)
×
443
      .toQuery();
×
444

×
445
    return this.prismaService
×
446
      .txClient()
×
447
      .$queryRawUnsafe<{ id: string; foreignId: string }[]>(query);
×
448
  }
×
449

150✔
450
  async getAllForeignKeys(options: ILinkFieldOptions) {
150✔
451
    const { fkHostTableName, selfKeyName, foreignKeyName } = options;
×
452

×
453
    const query = this.knex(fkHostTableName)
×
454
      .select({
×
455
        id: selfKeyName,
×
456
        foreignId: foreignKeyName,
×
457
      })
×
458
      .whereNotNull(selfKeyName)
×
459
      .whereNotNull(foreignKeyName)
×
460
      .toQuery();
×
461

×
462
    return this.prismaService
×
463
      .txClient()
×
464
      .$queryRawUnsafe<{ id: string; foreignId: string }[]>(query);
×
465
  }
×
466

150✔
467
  private async getJoinedForeignKeys(linkRecordIds: string[], options: ILinkFieldOptions) {
150✔
468
    const { fkHostTableName, selfKeyName, foreignKeyName } = options;
×
469

×
470
    const query = this.knex(fkHostTableName)
×
471
      .select({
×
472
        id: `a.${selfKeyName}`,
×
473
        foreignId: `b.${foreignKeyName}`,
×
474
      })
×
475
      .from(this.knex.ref(fkHostTableName).as('a'))
×
476
      .join(`${fkHostTableName} AS b`, `a.${selfKeyName}`, '=', `b.${selfKeyName}`)
×
477
      .whereIn(`a.${foreignKeyName}`, linkRecordIds)
×
478
      .whereNotNull(`a.${selfKeyName}`)
×
479
      .whereNotNull(`b.${foreignKeyName}`)
×
480
      .toQuery();
×
481

×
482
    return this.prismaService
×
483
      .txClient()
×
484
      .$queryRawUnsafe<{ id: string; foreignId: string }[]>(query);
×
485
  }
×
486

150✔
487
  /**
150✔
488
   * Checks if there are duplicate associations in one-to-one and one-to-many relationships.
150✔
489
   */
150✔
490
  private checkForIllegalDuplicateLinks(
150✔
491
    field: LinkFieldDto,
×
492
    recordIds: string[],
×
493
    indexedCellContext: Record<string, ILinkCellContext>
×
494
  ) {
×
495
    const relationship = field.options.relationship;
×
496
    if (relationship === Relationship.ManyMany || relationship === Relationship.ManyOne) {
×
497
      return;
×
498
    }
×
499
    const checkSet = new Set<string>();
×
500

×
501
    recordIds.forEach((recordId) => {
×
502
      const cellValue = indexedCellContext[`${field.id}-${recordId}`].newValue;
×
503
      if (!cellValue) {
×
504
        return;
×
505
      }
×
506
      if (Array.isArray(cellValue)) {
×
507
        cellValue.forEach((item) => {
×
508
          if (checkSet.has(item.id)) {
×
509
            throw new BadRequestException(
×
510
              `Consistency error, ${relationship} link field {${field.id}} unable to link a record (${item.id}) more than once`
×
511
            );
×
512
          }
×
513
          checkSet.add(item.id);
×
514
        });
×
515
        return;
×
516
      }
×
517
      if (checkSet.has(cellValue.id)) {
×
518
        throw new BadRequestException(
×
519
          `Consistency error, ${relationship} link field {${field.id}} unable to link a record (${cellValue.id}) more than once`
×
520
        );
×
521
      }
×
522
      checkSet.add(cellValue.id);
×
523
    });
×
524
  }
×
525

150✔
526
  // eslint-disable-next-line sonarjs/cognitive-complexity
150✔
527
  private parseFkRecordItem(
150✔
528
    field: LinkFieldDto,
×
529
    cellContexts: ILinkCellContext[],
×
530
    foreignKeys: {
×
531
      id: string;
×
532
      foreignId: string;
×
533
    }[]
×
534
  ): Record<string, IFkRecordItem> {
×
535
    const relationship = field.options.relationship;
×
536
    const foreignKeysIndexed = groupBy(foreignKeys, 'id');
×
537
    const foreignKeysReverseIndexed =
×
538
      relationship === Relationship.OneMany || relationship === Relationship.OneOne
×
539
        ? groupBy(foreignKeys, 'foreignId')
×
540
        : undefined;
×
541

×
542
    // eslint-disable-next-line sonarjs/cognitive-complexity
×
543
    return cellContexts.reduce<IFkRecordMap['fieldId']>((acc, cellContext) => {
×
544
      // this two relations only have one key in one recordId
×
545
      const id = cellContext.recordId;
×
546
      const foreignKeys = foreignKeysIndexed[id];
×
547
      if (relationship === Relationship.OneOne || relationship === Relationship.ManyOne) {
×
548
        const newCellValue = cellContext.newValue as ILinkCellValue | undefined;
×
549
        if ((foreignKeys?.length ?? 0) > 1) {
×
550
          throw new Error('duplicate foreign key from database');
×
551
        }
×
552

×
553
        const foreignRecordId = foreignKeys?.[0].foreignId;
×
554
        const oldKey = foreignRecordId || null;
×
555
        const newKey = newCellValue?.id || null;
×
556
        if (oldKey === newKey) {
×
557
          return acc;
×
558
        }
×
559

×
560
        if (newKey && foreignKeysReverseIndexed?.[newKey]) {
×
561
          throw new BadRequestException(
×
562
            `Consistency error, ${relationship} link field {${field.id}} unable to link a record (${newKey}) more than once`
×
563
          );
×
564
        }
×
565

×
566
        acc[id] = { oldKey, newKey };
×
567
        return acc;
×
568
      }
×
569

×
570
      if (relationship === Relationship.ManyMany || relationship === Relationship.OneMany) {
×
571
        const newCellValue = cellContext.newValue as ILinkCellValue[] | undefined;
×
572
        const oldKey = foreignKeys?.map((key) => key.foreignId) ?? null;
×
573
        const newKey = newCellValue?.map((item) => item.id) ?? null;
×
574

×
575
        const extraKey = difference(newKey ?? [], oldKey ?? []);
×
576

×
577
        extraKey.forEach((key) => {
×
578
          if (foreignKeysReverseIndexed?.[key]) {
×
579
            throw new BadRequestException(
×
580
              `Consistency error, ${relationship} link field {${field.id}} unable to link a record (${key}) more than once`
×
581
            );
×
582
          }
×
583
        });
×
584
        acc[id] = {
×
585
          oldKey,
×
586
          newKey,
×
587
        };
×
588
        return acc;
×
589
      }
×
590
      return acc;
×
591
    }, {});
×
592
  }
×
593

150✔
594
  /**
150✔
595
   * Tip: for single source of truth principle, we should only trust foreign key recordId
150✔
596
   *
150✔
597
   * 1. get all edited recordId and group by fieldId
150✔
598
   * 2. get all exist foreign key recordId
150✔
599
   */
150✔
600
  private async getFkRecordMap(
150✔
601
    fieldMap: IFieldMap,
×
602
    cellContexts: ILinkCellContext[]
×
603
  ): Promise<IFkRecordMap> {
×
604
    const fkRecordMap: IFkRecordMap = {};
×
605

×
606
    const cellGroupByFieldId = groupBy(cellContexts, (ctx) => ctx.fieldId);
×
607
    const indexedCellContext = keyBy(cellContexts, (ctx) => `${ctx.fieldId}-${ctx.recordId}`);
×
608
    for (const fieldId in cellGroupByFieldId) {
×
609
      const field = fieldMap[fieldId];
×
610
      if (!field) {
×
611
        throw new BadRequestException(`Field ${fieldId} not found`);
×
612
      }
×
613

×
614
      if (field.type !== FieldType.Link) {
×
615
        throw new BadRequestException(`Field ${fieldId} is not link field`);
×
616
      }
×
617

×
618
      const recordIds = cellGroupByFieldId[fieldId].map((ctx) => ctx.recordId);
×
619
      const linkRecordIds = cellGroupByFieldId[fieldId]
×
620
        .map((ctx) =>
×
621
          [ctx.oldValue, ctx.newValue]
×
622
            .flat()
×
623
            .filter(Boolean)
×
624
            .map((item) => item?.id as string)
×
625
        )
×
626
        .flat();
×
627

×
628
      const foreignKeys = await this.getForeignKeys(recordIds, linkRecordIds, field.options);
×
629
      this.checkForIllegalDuplicateLinks(field, recordIds, indexedCellContext);
×
630

×
631
      fkRecordMap[fieldId] = this.parseFkRecordItem(
×
632
        field,
×
633
        cellGroupByFieldId[fieldId],
×
634
        foreignKeys
×
635
      );
×
636
    }
×
637

×
638
    return fkRecordMap;
×
639
  }
×
640

150✔
641
  // create the key for recordMapByTableId but leave the undefined value for the next step
150✔
642
  private getRecordMapStruct(
150✔
643
    tableId: string,
×
644
    fieldMapByTableId: { [tableId: string]: IFieldMap },
×
645
    cellContexts: ILinkCellContext[]
×
646
  ) {
×
647
    const recordMapByTableId: IRecordMapByTableId = {};
×
648

×
649
    for (const cellContext of cellContexts) {
×
650
      const { recordId, fieldId, newValue, oldValue } = cellContext;
×
651
      const linkRecordIds = [oldValue, newValue]
×
652
        .flat()
×
653
        .filter(Boolean)
×
654
        .map((item) => item?.id as string);
×
655
      const field = fieldMapByTableId[tableId][fieldId] as LinkFieldDto;
×
656
      const foreignTableId = field.options.foreignTableId;
×
657
      const symmetricFieldId = field.options.symmetricFieldId;
×
658
      const symmetricField = symmetricFieldId
×
659
        ? (fieldMapByTableId[foreignTableId][symmetricFieldId] as LinkFieldDto)
×
660
        : undefined;
×
661
      const foreignLookedFieldId = field.options.lookupFieldId;
×
662
      const lookedFieldId = symmetricField?.options.lookupFieldId;
×
663

×
664
      set(recordMapByTableId, [tableId, recordId, fieldId], undefined);
×
665
      lookedFieldId && set(recordMapByTableId, [tableId, recordId, lookedFieldId], undefined);
×
666

×
667
      // create object key for record in looked field
×
668
      linkRecordIds.forEach((linkRecordId) => {
×
669
        symmetricFieldId &&
×
670
          set(recordMapByTableId, [foreignTableId, linkRecordId, symmetricFieldId], undefined);
×
671
        set(recordMapByTableId, [foreignTableId, linkRecordId, foreignLookedFieldId], undefined);
×
672
      });
×
673
    }
×
674

×
675
    return recordMapByTableId;
×
676
  }
×
677

150✔
678
  // eslint-disable-next-line sonarjs/cognitive-complexity
150✔
679
  private async fetchRecordMap(
150✔
680
    tableId2DbTableName: { [tableId: string]: string },
×
681
    fieldMapByTableId: { [tableId: string]: IFieldMap },
×
682
    recordMapByTableId: IRecordMapByTableId,
×
683
    cellContexts: ICellContext[],
×
684
    fromReset?: boolean
×
685
  ): Promise<IRecordMapByTableId> {
×
686
    const cellContextGroup = keyBy(cellContexts, (ctx) => `${ctx.recordId}-${ctx.fieldId}`);
×
687
    for (const tableId in recordMapByTableId) {
×
688
      const recordLookupFieldsMap = recordMapByTableId[tableId];
×
689
      const recordIds = Object.keys(recordLookupFieldsMap);
×
690
      const fieldIds = Array.from(
×
691
        Object.values(recordLookupFieldsMap).reduce<Set<string>>((pre, cur) => {
×
692
          for (const fieldId in cur) {
×
693
            pre.add(fieldId);
×
694
          }
×
695
          return pre;
×
696
        }, new Set())
×
697
      );
×
698

×
699
      const dbFieldName2FieldId: { [dbFieldName: string]: string } = {};
×
700
      const dbFieldNames = fieldIds.map((fieldId) => {
×
701
        const field = fieldMapByTableId[tableId][fieldId];
×
702
        // dbForeignName is not exit in fieldMapByTableId
×
703
        if (!field) {
×
704
          return fieldId;
×
705
        }
×
706
        dbFieldName2FieldId[field.dbFieldName] = fieldId;
×
707
        return field.dbFieldName;
×
708
      });
×
709

×
710
      const nativeQuery = this.knex(tableId2DbTableName[tableId])
×
711
        .select(dbFieldNames.concat('__id'))
×
712
        .whereIn('__id', recordIds)
×
713
        .toQuery();
×
714

×
715
      const recordRaw = await this.prismaService
×
716
        .txClient()
×
717
        .$queryRawUnsafe<{ [dbTableName: string]: unknown }[]>(nativeQuery);
×
718

×
719
      recordRaw.forEach((record) => {
×
720
        const recordId = record.__id as string;
×
721
        delete record.__id;
×
722
        for (const dbFieldName in record) {
×
723
          const fieldId = dbFieldName2FieldId[dbFieldName];
×
724
          let cellValue = record[dbFieldName];
×
725

×
726
          // dbForeignName is not exit in fieldMapByTableId
×
727
          if (!fieldId) {
×
728
            recordLookupFieldsMap[recordId][dbFieldName] = cellValue;
×
729
            continue;
×
730
          }
×
731
          const field = fieldMapByTableId[tableId][fieldId];
×
732
          if (fromReset && field.type === FieldType.Link) {
×
733
            continue;
×
734
          }
×
735

×
736
          // Overlay with new data, especially cellValue in primary field
×
737
          const inputData = cellContextGroup[`${recordId}-${fieldId}`];
×
738
          if (field.type !== FieldType.Link && inputData !== undefined) {
×
739
            recordLookupFieldsMap[recordId][fieldId] = inputData.newValue ?? undefined;
×
740
            continue;
×
741
          }
×
742

×
743
          cellValue = field.convertDBValue2CellValue(cellValue);
×
744

×
745
          recordLookupFieldsMap[recordId][fieldId] = cellValue ?? undefined;
×
746
        }
×
747
      }, {});
×
748
    }
×
749

×
750
    return recordMapByTableId;
×
751
  }
×
752

150✔
753
  private async getTableId2DbTableName(tableIds: string[]) {
150✔
754
    const tableRaws = await this.prismaService.txClient().tableMeta.findMany({
×
755
      where: {
×
756
        id: {
×
757
          in: tableIds,
×
758
        },
×
759
      },
×
760
      select: {
×
761
        id: true,
×
762
        dbTableName: true,
×
763
      },
×
764
    });
×
765
    return tableRaws.reduce<{ [tableId: string]: string }>((acc, cur) => {
×
766
      acc[cur.id] = cur.dbTableName;
×
767
      return acc;
×
768
    }, {});
×
769
  }
×
770

150✔
771
  private diffLinkCellChange(
150✔
772
    fieldMapByTableId: { [tableId: string]: IFieldMap },
×
773
    originRecordMapByTableId: IRecordMapByTableId,
×
774
    updatedRecordMapByTableId: IRecordMapByTableId
×
775
  ): ICellChange[] {
×
776
    const changes: ICellChange[] = [];
×
777

×
778
    for (const tableId in originRecordMapByTableId) {
×
779
      const originRecords = originRecordMapByTableId[tableId];
×
780
      const updatedRecords = updatedRecordMapByTableId[tableId];
×
781
      const fieldMap = fieldMapByTableId[tableId];
×
782

×
783
      for (const recordId in originRecords) {
×
784
        const originFields = originRecords[recordId];
×
785
        const updatedFields = updatedRecords[recordId];
×
786

×
787
        for (const fieldId in originFields) {
×
788
          if (fieldMap[fieldId].type !== FieldType.Link) {
×
789
            continue;
×
790
          }
×
791

×
792
          const oldValue = originFields[fieldId];
×
793
          const newValue = updatedFields[fieldId];
×
794

×
795
          if (!isEqual(oldValue, newValue)) {
×
796
            changes.push({ tableId, recordId, fieldId, oldValue, newValue });
×
797
          }
×
798
        }
×
799
      }
×
800
    }
×
801

×
802
    return changes;
×
803
  }
×
804

150✔
805
  private async getDerivateByCellContexts(
150✔
806
    tableId: string,
×
807
    tableId2DbTableName: { [tableId: string]: string },
×
808
    fieldMapByTableId: { [tableId: string]: IFieldMap },
×
809
    linkContexts: ILinkCellContext[],
×
810
    cellContexts: ICellContext[],
×
811
    fromReset?: boolean
×
812
  ): Promise<{
×
813
    cellChanges: ICellChange[];
×
814
    saveForeignKeyToDb: () => Promise<void>;
×
815
  }> {
×
816
    const fieldMap = fieldMapByTableId[tableId];
×
817
    const recordMapStruct = this.getRecordMapStruct(tableId, fieldMapByTableId, linkContexts);
×
818

×
819
    // console.log('fieldMapByTableId', fieldMapByTableId);
×
820
    const fkRecordMap = await this.getFkRecordMap(fieldMap, linkContexts);
×
821

×
822
    const originRecordMapByTableId = await this.fetchRecordMap(
×
823
      tableId2DbTableName,
×
824
      fieldMapByTableId,
×
825
      recordMapStruct,
×
826
      cellContexts,
×
827
      fromReset
×
828
    );
×
829

×
830
    const updatedRecordMapByTableId = await this.updateLinkRecord(
×
831
      tableId,
×
832
      fkRecordMap,
×
833
      fieldMapByTableId,
×
834
      originRecordMapByTableId
×
835
    );
×
836

×
837
    const cellChanges = this.diffLinkCellChange(
×
838
      fieldMapByTableId,
×
839
      originRecordMapByTableId,
×
840
      updatedRecordMapByTableId
×
841
    );
×
842

×
843
    return {
×
844
      cellChanges,
×
845
      saveForeignKeyToDb: async () => {
×
846
        return this.saveForeignKeyToDb(fieldMapByTableId[tableId], fkRecordMap);
×
847
      },
×
848
    };
×
849
  }
×
850

150✔
851
  private async saveForeignKeyForManyMany(
150✔
852
    field: LinkFieldDto,
×
853
    fkMap: { [recordId: string]: IFkRecordItem }
×
854
  ) {
×
855
    const { selfKeyName, foreignKeyName, fkHostTableName } = field.options;
×
856

×
857
    const toDelete: [string, string][] = [];
×
858
    const toAdd: [string, string][] = [];
×
859
    for (const recordId in fkMap) {
×
860
      const fkItem = fkMap[recordId];
×
861
      const oldKey = (fkItem.oldKey || []) as string[];
×
862
      const newKey = (fkItem.newKey || []) as string[];
×
863

×
864
      difference(oldKey, newKey).forEach((key) => toDelete.push([recordId, key]));
×
865
      difference(newKey, oldKey).forEach((key) => toAdd.push([recordId, key]));
×
866
    }
×
867

×
868
    if (toDelete.length) {
×
869
      const query = this.knex(fkHostTableName)
×
870
        .whereIn([selfKeyName, foreignKeyName], toDelete)
×
871
        .delete()
×
872
        .toQuery();
×
873
      await this.prismaService.txClient().$executeRawUnsafe(query);
×
874
    }
×
875

×
876
    if (toAdd.length) {
×
877
      const query = this.knex(fkHostTableName)
×
878
        .insert(
×
879
          toAdd.map(([source, target]) => ({
×
880
            [selfKeyName]: source,
×
881
            [foreignKeyName]: target,
×
882
          }))
×
883
        )
×
884
        .toQuery();
×
885
      await this.prismaService.txClient().$executeRawUnsafe(query);
×
886
    }
×
887
  }
×
888

150✔
889
  private async saveForeignKeyForManyOne(
150✔
890
    field: LinkFieldDto,
×
891
    fkMap: { [recordId: string]: IFkRecordItem }
×
892
  ) {
×
893
    const { selfKeyName, foreignKeyName, fkHostTableName } = field.options;
×
894

×
895
    const toDelete: [string, string][] = [];
×
896
    const toAdd: [string, string][] = [];
×
897
    for (const recordId in fkMap) {
×
898
      const fkItem = fkMap[recordId];
×
899
      const oldKey = fkItem.oldKey as string | null;
×
900
      const newKey = fkItem.newKey as string | null;
×
901

×
902
      oldKey && toDelete.push([recordId, oldKey]);
×
903
      newKey && toAdd.push([recordId, newKey]);
×
904
    }
×
905

×
906
    if (toDelete.length) {
×
907
      const query = this.knex(fkHostTableName)
×
908
        .update({ [foreignKeyName]: null })
×
909
        .whereIn([selfKeyName, foreignKeyName], toDelete)
×
910
        .toQuery();
×
911
      await this.prismaService.txClient().$executeRawUnsafe(query);
×
912
    }
×
913

×
914
    if (toAdd.length) {
×
915
      await this.batchService.batchUpdateDB(
×
916
        fkHostTableName,
×
917
        selfKeyName,
×
918
        [{ dbFieldName: foreignKeyName, schemaType: SchemaType.String }],
×
919
        toAdd.map(([recordId, foreignRecordId]) => ({
×
920
          id: recordId,
×
921
          values: { [foreignKeyName]: foreignRecordId },
×
922
        }))
×
923
      );
×
924
    }
×
925
  }
×
926

150✔
927
  private async saveForeignKeyForOneMany(
150✔
928
    field: LinkFieldDto,
×
929
    fkMap: { [recordId: string]: IFkRecordItem }
×
930
  ) {
×
931
    const { selfKeyName, foreignKeyName, fkHostTableName, isOneWay } = field.options;
×
932

×
933
    if (isOneWay) {
×
934
      this.saveForeignKeyForManyMany(field, fkMap);
×
935
      return;
×
936
    }
×
937
    const toDelete: [string, string][] = [];
×
938
    const toAdd: [string, string][] = [];
×
939
    for (const recordId in fkMap) {
×
940
      const fkItem = fkMap[recordId];
×
941
      const oldKey = (fkItem.oldKey || []) as string[];
×
942
      const newKey = (fkItem.newKey || []) as string[];
×
943

×
944
      difference(oldKey, newKey).forEach((key) => toDelete.push([recordId, key]));
×
945
      difference(newKey, oldKey).forEach((key) => toAdd.push([recordId, key]));
×
946
    }
×
947

×
948
    if (toDelete.length) {
×
949
      const query = this.knex(fkHostTableName)
×
950
        .update({ [selfKeyName]: null })
×
951
        .whereIn([selfKeyName, foreignKeyName], toDelete)
×
952
        .toQuery();
×
953
      await this.prismaService.txClient().$executeRawUnsafe(query);
×
954
    }
×
955

×
956
    if (toAdd.length) {
×
957
      await this.batchService.batchUpdateDB(
×
958
        fkHostTableName,
×
959
        foreignKeyName,
×
960
        [{ dbFieldName: selfKeyName, schemaType: SchemaType.String }],
×
961
        toAdd.map(([recordId, foreignRecordId]) => ({
×
962
          id: foreignRecordId,
×
963
          values: { [selfKeyName]: recordId },
×
964
        }))
×
965
      );
×
966
    }
×
967
  }
×
968

150✔
969
  private async saveForeignKeyForOneOne(
150✔
970
    field: LinkFieldDto,
×
971
    fkMap: { [recordId: string]: IFkRecordItem }
×
972
  ) {
×
973
    const { selfKeyName, foreignKeyName, fkHostTableName } = field.options;
×
974
    if (selfKeyName === '__id') {
×
975
      await this.saveForeignKeyForManyOne(field, fkMap);
×
976
    } else {
×
977
      const toDelete: [string, string][] = [];
×
978
      const toAdd: [string, string][] = [];
×
979
      for (const recordId in fkMap) {
×
980
        const fkItem = fkMap[recordId];
×
981
        const oldKey = fkItem.oldKey as string | null;
×
982
        const newKey = fkItem.newKey as string | null;
×
983

×
984
        oldKey && toDelete.push([recordId, oldKey]);
×
985
        newKey && toAdd.push([recordId, newKey]);
×
986
      }
×
987

×
988
      if (toDelete.length) {
×
989
        const query = this.knex(fkHostTableName)
×
990
          .update({ [selfKeyName]: null })
×
991
          .whereIn([selfKeyName, foreignKeyName], toDelete)
×
992
          .toQuery();
×
993
        await this.prismaService.txClient().$executeRawUnsafe(query);
×
994
      }
×
995

×
996
      if (toAdd.length) {
×
997
        await this.batchService.batchUpdateDB(
×
998
          fkHostTableName,
×
999
          foreignKeyName,
×
1000
          [{ dbFieldName: selfKeyName, schemaType: SchemaType.String }],
×
1001
          toAdd.map(([recordId, foreignRecordId]) => ({
×
1002
            id: foreignRecordId,
×
1003
            values: { [selfKeyName]: recordId },
×
1004
          }))
×
1005
        );
×
1006
      }
×
1007
    }
×
1008
  }
×
1009

150✔
1010
  private async saveForeignKeyToDb(fieldMap: IFieldMap, fkRecordMap: IFkRecordMap) {
150✔
1011
    for (const fieldId in fkRecordMap) {
×
1012
      const fkMap = fkRecordMap[fieldId];
×
1013
      const field = fieldMap[fieldId] as LinkFieldDto;
×
1014
      const relationship = field.options.relationship;
×
1015
      if (relationship === Relationship.ManyMany) {
×
1016
        await this.saveForeignKeyForManyMany(field, fkMap);
×
1017
      }
×
1018
      if (relationship === Relationship.ManyOne) {
×
1019
        await this.saveForeignKeyForManyOne(field, fkMap);
×
1020
      }
×
1021
      if (relationship === Relationship.OneMany) {
×
1022
        await this.saveForeignKeyForOneMany(field, fkMap);
×
1023
      }
×
1024
      if (relationship === Relationship.OneOne) {
×
1025
        await this.saveForeignKeyForOneOne(field, fkMap);
×
1026
      }
×
1027
    }
×
1028
  }
×
1029

150✔
1030
  /**
150✔
1031
   * strategy
150✔
1032
   * 0: define `main table` is where foreign key located in, `foreign table` is where foreign key referenced to
150✔
1033
   * 1. generate foreign key changes, cache effected recordIds, both main table and foreign table
150✔
1034
   * 2. update foreign key by changes and submit origin op
150✔
1035
   * 3. check and generate op to update main table by cached recordIds
150✔
1036
   * 4. check and generate op to update foreign table by cached recordIds
150✔
1037
   */
150✔
1038
  async getDerivateByLink(tableId: string, cellContexts: ICellContext[], fromReset?: boolean) {
150✔
1039
    const linkContexts = this.filterLinkContext(cellContexts as ILinkCellContext[]);
×
1040
    if (!linkContexts.length) {
×
1041
      return;
×
1042
    }
×
1043
    const fieldIds = linkContexts.map((ctx) => ctx.fieldId);
×
1044
    const fieldMapByTableId = await this.getRelatedFieldMap(fieldIds);
×
1045
    const tableId2DbTableName = await this.getTableId2DbTableName(Object.keys(fieldMapByTableId));
×
1046

×
1047
    return this.getDerivateByCellContexts(
×
1048
      tableId,
×
1049
      tableId2DbTableName,
×
1050
      fieldMapByTableId,
×
1051
      linkContexts,
×
1052
      cellContexts,
×
1053
      fromReset
×
1054
    );
×
1055
  }
×
1056

150✔
1057
  private parseFkRecordItemToDelete(
150✔
1058
    options: ILinkFieldOptions,
×
1059
    toDeleteRecordIds: string[],
×
1060
    foreignKeys: {
×
1061
      id: string;
×
1062
      foreignId: string;
×
1063
    }[]
×
1064
  ): Record<string, IFkRecordItem> {
×
1065
    const relationship = options.relationship;
×
1066
    const foreignKeysIndexed = groupBy(foreignKeys, 'id');
×
1067
    const toDeleteSet = new Set(toDeleteRecordIds);
×
1068

×
1069
    return Object.keys(foreignKeysIndexed).reduce<IFkRecordMap['fieldId']>((acc, id) => {
×
1070
      // this two relations only have one key in one recordId
×
1071
      const foreignKeys = foreignKeysIndexed[id];
×
1072
      if (relationship === Relationship.OneOne || relationship === Relationship.ManyOne) {
×
1073
        if ((foreignKeys?.length ?? 0) > 1) {
×
1074
          throw new Error('duplicate foreign key from database');
×
1075
        }
×
1076

×
1077
        const foreignRecordId = foreignKeys?.[0].foreignId;
×
1078
        const oldKey = foreignRecordId || null;
×
1079
        if (!toDeleteSet.has(foreignRecordId)) {
×
1080
          return acc;
×
1081
        }
×
1082

×
1083
        acc[id] = { oldKey, newKey: null };
×
1084
        return acc;
×
1085
      }
×
1086

×
1087
      if (relationship === Relationship.ManyMany || relationship === Relationship.OneMany) {
×
1088
        const oldKey = foreignKeys?.map((key) => key.foreignId) ?? null;
×
1089
        if (!oldKey) {
×
1090
          return acc;
×
1091
        }
×
1092

×
1093
        const newKey = oldKey.filter((key) => !toDeleteSet.has(key));
×
1094

×
1095
        if (newKey.length === oldKey.length) {
×
1096
          return acc;
×
1097
        }
×
1098

×
1099
        acc[id] = {
×
1100
          oldKey,
×
1101
          newKey: newKey.length ? newKey : null,
×
1102
        };
×
1103
        return acc;
×
1104
      }
×
1105
      return acc;
×
1106
    }, {});
×
1107
  }
×
1108

150✔
1109
  private async getContextByDelete(linkFieldRaws: Field[], recordIds: string[]) {
150✔
1110
    const cellContextsMap: { [tableId: string]: ICellContext[] } = {};
×
1111

×
1112
    const keyToValue = (key: string | string[] | null) =>
×
1113
      key ? (Array.isArray(key) ? key.map((id) => ({ id })) : { id: key }) : null;
×
1114

×
1115
    for (const fieldRaws of linkFieldRaws) {
×
1116
      const options = JSON.parse(fieldRaws.options as string) as ILinkFieldOptions;
×
1117
      const tableId = fieldRaws.tableId;
×
1118
      const foreignKeys = await this.getJoinedForeignKeys(recordIds, options);
×
1119
      const fieldItems = this.parseFkRecordItemToDelete(options, recordIds, foreignKeys);
×
1120
      if (!cellContextsMap[tableId]) {
×
1121
        cellContextsMap[tableId] = [];
×
1122
      }
×
1123
      Object.keys(fieldItems).forEach((recordId) => {
×
1124
        const { oldKey, newKey } = fieldItems[recordId];
×
1125
        cellContextsMap[tableId].push({
×
1126
          fieldId: fieldRaws.id,
×
1127
          recordId,
×
1128
          oldValue: keyToValue(oldKey),
×
1129
          newValue: keyToValue(newKey),
×
1130
        });
×
1131
      });
×
1132
    }
×
1133

×
1134
    return cellContextsMap;
×
1135
  }
×
1136

150✔
1137
  async getRelatedLinkFieldRaws(tableId: string) {
150✔
1138
    const { id: primaryFieldId } = await this.prismaService
×
1139
      .txClient()
×
1140
      .field.findFirstOrThrow({
×
1141
        where: { tableId, deletedTime: null, isPrimary: true },
×
1142
        select: { id: true },
×
1143
      })
×
1144
      .catch(() => {
×
1145
        throw new BadRequestException(`Primary field not found`);
×
1146
      });
×
1147

×
1148
    const references = await this.prismaService.txClient().reference.findMany({
×
1149
      where: { fromFieldId: primaryFieldId },
×
1150
      select: { toFieldId: true },
×
1151
    });
×
1152

×
1153
    const referenceFieldIds = references.map((ref) => ref.toFieldId);
×
1154

×
1155
    return await this.prismaService.txClient().field.findMany({
×
1156
      where: {
×
1157
        id: { in: referenceFieldIds },
×
1158
        type: FieldType.Link,
×
1159
        isLookup: null,
×
1160
        deletedTime: null,
×
1161
      },
×
1162
    });
×
1163
  }
×
1164

150✔
1165
  async getDeleteRecordUpdateContext(tableId: string, recordIds: string[]) {
150✔
1166
    const linkFieldRaws = await this.getRelatedLinkFieldRaws(tableId);
×
1167

×
1168
    return await this.getContextByDelete(linkFieldRaws, recordIds);
×
1169
  }
×
1170
}
150✔
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