teableio/teable

View on GitHub
apps/nestjs-backend/test/reference.e2e-spec.ts.bak

Summary

Maintainability
Test Coverage
/* eslint-disable @typescript-eslint/naming-convention */
import type { TestingModule } from '@nestjs/testing';
import { Test } from '@nestjs/testing';
import type { IRecord } from '@teable/core';
import {
  CellValueType,
  DbFieldType,
  FieldType,
  NumberFormattingType,
  Relationship,
} from '@teable/core';
import { PrismaService } from '@teable/db-main-prisma';
import type { Knex } from 'knex';
import { CalculationModule } from '../src/features/calculation/calculation.module';
import type { ITopoItemWithRecords } from '../src/features/calculation/reference.service';
import { ReferenceService } from '../src/features/calculation/reference.service';
import type { IFieldInstance } from '../src/features/field/model/factory';
import { createFieldInstanceByVo } from '../src/features/field/model/factory';
import type { FormulaFieldDto } from '../src/features/field/model/field-dto/formula-field.dto';
import type { LinkFieldDto } from '../src/features/field/model/field-dto/link-field.dto';
import type { NumberFieldDto } from '../src/features/field/model/field-dto/number-field.dto';
import type { SingleLineTextFieldDto } from '../src/features/field/model/field-dto/single-line-text-field.dto';
import { GlobalModule } from '../src/global/global.module';

describe('Reference Service (e2e)', () => {
  describe('ReferenceService data retrieval', () => {
    let service: ReferenceService;
    let prisma: PrismaService;
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    let initialReferences: {
      fromFieldId: string;
      toFieldId: string;
    }[];
    let db: Knex;
    const s = JSON.stringify;
    beforeAll(async () => {
      const module: TestingModule = await Test.createTestingModule({
        imports: [GlobalModule, CalculationModule],
      }).compile();
      service = module.get<ReferenceService>(ReferenceService);
      prisma = module.get<PrismaService>(PrismaService);
      db = module.get('CUSTOM_KNEX');
    });
    afterAll(async () => {
      await prisma.$disconnect();
    });
    async function executeKnex(builder: Knex.SchemaBuilder | Knex.QueryBuilder) {
      const sql = builder.toSQL();
      if (Array.isArray(sql)) {
        for (const item of sql) {
          await prisma.$executeRawUnsafe(item.sql, ...item.bindings);
        }
      } else {
        const nativeSql = sql.toNative();
        await prisma.$executeRawUnsafe(nativeSql.sql, ...nativeSql.bindings);
      }
    }
    beforeEach(async () => {
      // create tables
      await executeKnex(
        db.schema.createTable('A', (table) => {
          table.string('__id').primary();
          table.string('fieldA');
          table.string('oneToManyB');
        })
      );
      await executeKnex(
        db.schema.createTable('B', (table) => {
          table.string('__id').primary();
          table.string('fieldB');
          table.string('manyToOneA');
          table.string('__fk_manyToOneA');
          table.string('oneToManyC');
        })
      );
      await executeKnex(
        db.schema.createTable('C', (table) => {
          table.string('__id').primary();
          table.string('fieldC');
          table.string('manyToOneB');
          table.string('__fk_manyToOneB');
        })
      );
      initialReferences = [
        { fromFieldId: 'f1', toFieldId: 'f2' },
        { fromFieldId: 'f2', toFieldId: 'f3' },
        { fromFieldId: 'f2', toFieldId: 'f4' },
        { fromFieldId: 'f3', toFieldId: 'f6' },
        { fromFieldId: 'f5', toFieldId: 'f4' },
        { fromFieldId: 'f7', toFieldId: 'f8' },
      ];
      for (const data of initialReferences) {
        await prisma.reference.create({
          data,
        });
      }
    });
    afterEach(async () => {
      // Delete test data
      for (const data of initialReferences) {
        await prisma.reference.deleteMany({
          where: { fromFieldId: data.fromFieldId, AND: { toFieldId: data.toFieldId } },
        });
      }
      // delete data
      await executeKnex(db('A').truncate());
      await executeKnex(db('B').truncate());
      await executeKnex(db('C').truncate());
      // delete table
      await executeKnex(db.schema.dropTable('A'));
      await executeKnex(db.schema.dropTable('B'));
      await executeKnex(db.schema.dropTable('C'));
    });
    it('many to one link relationship order for getAffectedRecords', async () => {
      // fill data
      await executeKnex(
        db('A').insert([
          { __id: 'idA1', fieldA: 'A1', oneToManyB: s(['B1', 'B2']) },
          { __id: 'idA2', fieldA: 'A2', oneToManyB: s(['B3']) },
        ])
      );
      await executeKnex(
        db('B').insert([
          /* eslint-disable prettier/prettier */
          {
            __id: 'idB1',
            fieldB: 'A1',
            manyToOneA: 'A1',
            __fk_manyToOneA: 'idA1',
            oneToManyC: s(['C1', 'C2']),
          },
          {
            __id: 'idB2',
            fieldB: 'A1',
            manyToOneA: 'A1',
            __fk_manyToOneA: 'idA1',
            oneToManyC: s(['C3']),
          },
          {
            __id: 'idB3',
            fieldB: 'A2',
            manyToOneA: 'A2',
            __fk_manyToOneA: 'idA2',
            oneToManyC: s(['C4']),
          },
          { __id: 'idB4', fieldB: null, manyToOneA: null, __fk_manyToOneA: null, oneToManyC: null },
          /* eslint-enable prettier/prettier */
        ])
      );
      await executeKnex(
        db('C').insert([
          { __id: 'idC1', fieldC: 'C1', manyToOneB: 'A1', __fk_manyToOneB: 'idB1' },
          { __id: 'idC2', fieldC: 'C2', manyToOneB: 'A1', __fk_manyToOneB: 'idB1' },
          { __id: 'idC3', fieldC: 'C3', manyToOneB: 'A1', __fk_manyToOneB: 'idB2' },
          { __id: 'idC4', fieldC: 'C4', manyToOneB: 'A2', __fk_manyToOneB: 'idB3' },
        ])
      );
      const topoOrder = [
        {
          dbTableName: 'B',
          fieldId: 'manyToOneA',
          foreignKeyField: '__fk_manyToOneA',
          relationship: Relationship.ManyOne,
          linkedTable: 'A',
          dependencies: ['fieldA'],
        },
        {
          dbTableName: 'C',
          fieldId: 'manyToOneB',
          foreignKeyField: '__fk_manyToOneB',
          relationship: Relationship.ManyOne,
          linkedTable: 'B',
          dependencies: ['fieldB'],
        },
      ];
      const records = await service['getAffectedRecordItems'](topoOrder, [
        { id: 'idA1', dbTableName: 'A' },
      ]);
      expect(records).toEqual([
        { id: 'idA1', dbTableName: 'A' },
        { id: 'idB1', dbTableName: 'B', fieldId: 'manyToOneA', relationTo: 'idA1' },
        { id: 'idB2', dbTableName: 'B', fieldId: 'manyToOneA', relationTo: 'idA1' },
        { id: 'idC1', dbTableName: 'C', fieldId: 'manyToOneB', relationTo: 'idB1' },
        { id: 'idC2', dbTableName: 'C', fieldId: 'manyToOneB', relationTo: 'idB1' },
        { id: 'idC3', dbTableName: 'C', fieldId: 'manyToOneB', relationTo: 'idB2' },
      ]);
      const recordsWithMultiInput = await service['getAffectedRecordItems'](topoOrder, [
        { id: 'idA1', dbTableName: 'A' },
        { id: 'idA2', dbTableName: 'A' },
      ]);
      expect(recordsWithMultiInput).toEqual([
        { id: 'idA1', dbTableName: 'A' },
        { id: 'idA2', dbTableName: 'A' },
        { id: 'idB1', dbTableName: 'B', relationTo: 'idA1', fieldId: 'manyToOneA' },
        { id: 'idB2', dbTableName: 'B', relationTo: 'idA1', fieldId: 'manyToOneA' },
        { id: 'idB3', dbTableName: 'B', relationTo: 'idA2', fieldId: 'manyToOneA' },
        { id: 'idC1', dbTableName: 'C', relationTo: 'idB1', fieldId: 'manyToOneB' },
        { id: 'idC2', dbTableName: 'C', relationTo: 'idB1', fieldId: 'manyToOneB' },
        { id: 'idC3', dbTableName: 'C', relationTo: 'idB2', fieldId: 'manyToOneB' },
        { id: 'idC4', dbTableName: 'C', relationTo: 'idB3', fieldId: 'manyToOneB' },
      ]);
    });
    it('one to many link relationship order for getAffectedRecords', async () => {
      await executeKnex(
        db('A').insert([{ __id: 'idA1', fieldA: 'A1', oneToManyB: s(['C1, C2', 'C3']) }])
      );
      await executeKnex(
        db('B').insert([
          /* eslint-disable prettier/prettier */
          {
            __id: 'idB1',
            fieldB: 'C1, C2',
            manyToOneA: 'A1',
            __fk_manyToOneA: 'idA1',
            oneToManyC: s(['C1', 'C2']),
          },
          {
            __id: 'idB2',
            fieldB: 'C3',
            manyToOneA: 'A1',
            __fk_manyToOneA: 'idA1',
            oneToManyC: s(['C3']),
          },
          /* eslint-enable prettier/prettier */
        ])
      );
      await executeKnex(
        db('C').insert([
          { __id: 'idC1', fieldC: 'C1', manyToOneB: 'C1, C2', __fk_manyToOneB: 'idB1' },
          { __id: 'idC2', fieldC: 'C2', manyToOneB: 'C1, C2', __fk_manyToOneB: 'idB1' },
          { __id: 'idC3', fieldC: 'C3', manyToOneB: 'C3', __fk_manyToOneB: 'idB2' },
        ])
      );
      // topoOrder Graph:
      // C.fieldC -> B.oneToManyC -> B.fieldB -> A.oneToManyB
      //                                      -> C.manyToOneB
      const topoOrder = [
        {
          dbTableName: 'B',
          fieldId: 'oneToManyC',
          foreignKeyField: '__fk_manyToOneB',
          relationship: Relationship.OneMany,
          linkedTable: 'C',
        },
        {
          dbTableName: 'A',
          fieldId: 'oneToManyB',
          foreignKeyField: '__fk_manyToOneA',
          relationship: Relationship.OneMany,
          linkedTable: 'B',
        },
        {
          dbTableName: 'C',
          fieldId: 'manyToOneB',
          foreignKeyField: '__fk_manyToOneB',
          relationship: Relationship.ManyOne,
          linkedTable: 'B',
        },
      ];
      const records = await service['getAffectedRecordItems'](topoOrder, [
        { id: 'idC1', dbTableName: 'C' },
      ]);
      // manyToOneB: ['B1', 'B2']
      expect(records).toEqual([
        { id: 'idC1', dbTableName: 'C' },
        { id: 'idB1', dbTableName: 'B', fieldId: 'oneToManyC', selectIn: 'C#__fk_manyToOneB' },
        { id: 'idA1', dbTableName: 'A', fieldId: 'oneToManyB', selectIn: 'B#__fk_manyToOneA' },
        { id: 'idC1', dbTableName: 'C', fieldId: 'manyToOneB', relationTo: 'idB1' },
        { id: 'idC2', dbTableName: 'C', fieldId: 'manyToOneB', relationTo: 'idB1' },
      ]);
      const extraRecords = await service['getDependentRecordItems'](records);
      expect(extraRecords).toEqual([
        { id: 'idB1', dbTableName: 'B', fieldId: 'oneToManyB', relationTo: 'idA1' },
        { id: 'idB2', dbTableName: 'B', fieldId: 'oneToManyB', relationTo: 'idA1' },
        { id: 'idC1', dbTableName: 'C', fieldId: 'oneToManyC', relationTo: 'idB1' },
        { id: 'idC2', dbTableName: 'C', fieldId: 'oneToManyC', relationTo: 'idB1' },
      ]);
    });
    it('getDependentNodesCTE should return all dependent nodes', async () => {
      const result = await service['getDependentNodesCTE'](['f2']);
      const resultData = [...initialReferences];
      resultData.pop();
      expect(result).toEqual(expect.arrayContaining(resultData));
    });
    it('should filter full graph by fieldIds', async () => {
      /**
       * f1 -> f3 -> f4
       * f2 -> f3
       */
      const graph = [
        {
          fromFieldId: 'f1',
          toFieldId: 'f3',
        },
        {
          fromFieldId: 'f2',
          toFieldId: 'f3',
        },
        {
          fromFieldId: 'f3',
          toFieldId: 'f4',
        },
      ];
      expect(service['filterDirectedGraph'](graph, ['f1'])).toEqual(expect.arrayContaining(graph));
      expect(service['filterDirectedGraph'](graph, ['f2'])).toEqual(expect.arrayContaining(graph));
      expect(service['filterDirectedGraph'](graph, ['f3'])).toEqual(
        expect.arrayContaining([
          {
            fromFieldId: 'f3',
            toFieldId: 'f4',
          },
        ])
      );
    });
  });
  describe('ReferenceService calculation', () => {
    let service: ReferenceService;
    let fieldMap: { [oneToMany: string]: IFieldInstance };
    let fieldId2TableId: { [fieldId: string]: string };
    let recordMap: { [recordId: string]: IRecord };
    let ordersWithRecords: ITopoItemWithRecords[];
    let tableId2DbTableName: { [tableId: string]: string };
    beforeAll(async () => {
      const module: TestingModule = await Test.createTestingModule({
        imports: [GlobalModule, CalculationModule],
      }).compile();
      service = module.get<ReferenceService>(ReferenceService);
    });
    beforeEach(() => {
      fieldMap = {
        fieldA: createFieldInstanceByVo({
          id: 'fieldA',
          name: 'fieldA',
          type: FieldType.Link,
          options: {
            relationship: Relationship.OneMany,
            foreignTableId: 'foreignTable1',
            lookupFieldId: 'lookupField1',
            dbForeignKeyName: 'dbForeignKeyName1',
            symmetricFieldId: 'symmetricField1',
          },
          cellValueType: CellValueType.String,
          dbFieldType: DbFieldType.Json,
          isMultipleCellValue: true,
        } as LinkFieldDto),
        // {
        //   dbTableName: 'A',
        //   fieldId: 'oneToManyB',
        //   foreignKeyField: '__fk_manyToOneA',
        //   relationship: Relationship.OneMany,
        //   linkedTable: 'B',
        // },
        oneToManyB: createFieldInstanceByVo({
          id: 'oneToManyB',
          name: 'oneToManyB',
          type: FieldType.Link,
          options: {
            relationship: Relationship.OneMany,
            foreignTableId: 'B',
            lookupFieldId: 'fieldB',
            dbForeignKeyName: '__fk_manyToOneA',
            symmetricFieldId: 'manyToOneA',
          },
          cellValueType: CellValueType.String,
          dbFieldType: DbFieldType.Json,
          isMultipleCellValue: true,
        } as LinkFieldDto),
        // fieldB is a special field depend on oneToManyC, may be convert it to formula field
        fieldB: createFieldInstanceByVo({
          id: 'fieldB',
          name: 'fieldB',
          type: FieldType.Formula,
          options: {
            expression: '{oneToManyC}',
          },
          cellValueType: CellValueType.String,
          dbFieldType: DbFieldType.Text,
          isMultipleCellValue: true,
          isComputed: true,
        } as FormulaFieldDto),
        manyToOneA: createFieldInstanceByVo({
          id: 'manyToOneA',
          name: 'manyToOneA',
          type: FieldType.Link,
          options: {
            relationship: Relationship.ManyOne,
            foreignTableId: 'A',
            lookupFieldId: 'fieldA',
            dbForeignKeyName: '__fk_manyToOneA',
            symmetricFieldId: 'oneToManyB',
          },
          cellValueType: CellValueType.String,
          dbFieldType: DbFieldType.Json,
        } as LinkFieldDto),
        // {
        //   dbTableName: 'B',
        //   fieldId: 'oneToManyC',
        //   foreignKeyField: '__fk_manyToOneB',
        //   relationship: Relationship.OneMany,
        //   linkedTable: 'C',
        // },
        oneToManyC: createFieldInstanceByVo({
          id: 'oneToManyC',
          name: 'oneToManyC',
          type: FieldType.Link,
          options: {
            relationship: Relationship.OneMany,
            foreignTableId: 'C',
            lookupFieldId: 'fieldC',
            dbForeignKeyName: '__fk_manyToOneB',
            symmetricFieldId: 'manyToOneB',
          },
          cellValueType: CellValueType.String,
          dbFieldType: DbFieldType.Json,
          isMultipleCellValue: true,
        } as LinkFieldDto),
        fieldC: createFieldInstanceByVo({
          id: 'fieldC',
          name: 'fieldC',
          type: FieldType.SingleLineText,
          cellValueType: CellValueType.String,
          dbFieldType: DbFieldType.Text,
        } as SingleLineTextFieldDto),
        // {
        //   dbTableName: 'C',
        //   fieldId: 'manyToOneB',
        //   foreignKeyField: '__fk_manyToOneB',
        //   relationship: Relationship.ManyOne,
        //   linkedTable: 'B',
        // },
        manyToOneB: createFieldInstanceByVo({
          id: 'manyToOneB',
          name: 'manyToOneB',
          type: FieldType.Link,
          options: {
            relationship: Relationship.ManyOne,
            foreignTableId: 'B',
            lookupFieldId: 'fieldB',
            dbForeignKeyName: '__fk_manyToOneB',
            symmetricFieldId: 'oneToManyC',
          },
          cellValueType: CellValueType.String,
          dbFieldType: DbFieldType.Json,
        } as LinkFieldDto),
      };
      fieldId2TableId = {
        fieldA: 'A',
        oneToManyB: 'A',
        fieldB: 'B',
        manyToOneA: 'B',
        oneToManyC: 'B',
        fieldC: 'C',
        manyToOneB: 'C',
      };
      tableId2DbTableName = {
        A: 'A',
        B: 'B',
        C: 'C',
      };
      recordMap = {
        // use new value fieldC: 'CX' here
        idC1: {
          id: 'idC1',
          fields: { fieldC: 'CX', manyToOneB: { title: 'C1, C2', id: 'idB1' } },
        },
        idC2: {
          id: 'idC2',
          fields: { fieldC: 'C2', manyToOneB: { title: 'C1, C2', id: 'idB1' } },
        },
        idC3: {
          id: 'idC3',
          fields: { fieldC: 'C3', manyToOneB: { title: 'C3', id: 'idB2' } },
        },
        idB1: {
          id: 'idB1',
          fields: {
            fieldB: ['C1', 'C2'],
            manyToOneA: { title: 'A1', id: 'idA1' },
            oneToManyC: [
              { title: 'C1', id: 'idC1' },
              { title: 'C2', id: 'idC2' },
            ],
          },
        },
        idB2: {
          id: 'idB2',
          fields: {
            fieldB: ['C3'],
            manyToOneA: { title: 'A1', id: 'idA1' },
            oneToManyC: [{ title: 'C3', id: 'idC3' }],
          },
        },
        idA1: {
          id: 'idA1',
          fields: {
            fieldA: 'A1',
            oneToManyB: [
              { title: 'C1, C2', id: 'idB1' },
              { title: 'C3', id: 'idB2' },
            ],
          },
        },
      };
      // topoOrder Graph:
      // C.fieldC -> B.oneToManyC -> B.fieldB -> A.oneToManyB
      //                                      -> C.manyToOneB
      ordersWithRecords = [
        {
          id: 'oneToManyC',
          dependencies: ['fieldC'],
          recordItemMap: [
            {
              record: recordMap['idB1'],
              dependencies: [recordMap['idC1'], recordMap['idC2']],
            },
          ],
        },
        {
          id: 'fieldB',
          dependencies: ['oneToManyC'],
          recordItemMap: [
            {
              record: recordMap['idB1'],
            },
          ],
        },
        {
          id: 'oneToManyB',
          dependencies: ['fieldB'],
          recordItemMap: [
            {
              record: recordMap['idA1'],
              dependencies: [recordMap['idB1'], recordMap['idB2']],
            },
          ],
        },
        {
          id: 'manyToOneB',
          dependencies: ['fieldB'],
          recordItemMap: [
            {
              record: recordMap['idC1'],
              dependencies: recordMap['idB1'],
            },
            {
              record: recordMap['idC2'],
              dependencies: recordMap['idB1'],
            },
          ],
        },
      ];
    });
    it('should correctly collect changes for Link and Computed fields', () => {
      // 2. Act
      const changes = service['collectChanges'](ordersWithRecords, fieldMap, fieldId2TableId);
      // 3. Assert
      // topoOrder Graph:
      // C.fieldC -> B.oneToManyC -> B.fieldB -> A.oneToManyB
      //                                      -> C.manyToOneB
      // change from: idC1.fieldC      = 'C1' -> 'CX'
      // change affected:
      // idB1.oneToManyC  = ['C1', 'C2'] -> ['CX', 'C2']
      // idB1.fieldB      = ['C1', 'C2'] -> ['CX', 'C2']
      // idA1.oneToManyB  = ['C1, C2', 'C3'] -> ['CX, C2', 'C3']
      // idC1.manyToOneB  = 'C1, C2' -> 'CX, C2'
      // idC2.manyToOneB  = 'C1, C2' -> 'CX, C2'
      expect(changes).toEqual([
        {
          tableId: 'B',
          recordId: 'idB1',
          fieldId: 'oneToManyC',
          oldValue: [
            { title: 'C1', id: 'idC1' },
            { title: 'C2', id: 'idC2' },
          ],
          newValue: [
            { title: 'CX', id: 'idC1' },
            { title: 'C2', id: 'idC2' },
          ],
        },
        {
          tableId: 'B',
          recordId: 'idB1',
          fieldId: 'fieldB',
          oldValue: ['C1', 'C2'],
          newValue: ['CX', 'C2'],
        },
        {
          tableId: 'A',
          recordId: 'idA1',
          fieldId: 'oneToManyB',
          oldValue: [
            { title: 'C1, C2', id: 'idB1' },
            { title: 'C3', id: 'idB2' },
          ],
          newValue: [
            { title: 'CX, C2', id: 'idB1' },
            { title: 'C3', id: 'idB2' },
          ],
        },
        {
          tableId: 'C',
          recordId: 'idC1',
          fieldId: 'manyToOneB',
          oldValue: { title: 'C1, C2', id: 'idB1' },
          newValue: { title: 'CX, C2', id: 'idB1' },
        },
        {
          tableId: 'C',
          recordId: 'idC2',
          fieldId: 'manyToOneB',
          oldValue: { title: 'C1, C2', id: 'idB1' },
          newValue: { title: 'CX, C2', id: 'idB1' },
        },
      ]);
    });
    it('should createTopoItemWithRecords from prepared context', () => {
      const tableId2DbTableName = {
        A: 'A',
        B: 'B',
        C: 'C',
      };
      const dbTableName2records = {
        A: [recordMap['idA1']],
        B: [recordMap['idB1'], recordMap['idB2']],
        C: [recordMap['idC1'], recordMap['idC2'], recordMap['idC3']],
      };
      const affectedRecordItems = [
        { id: 'idB1', dbTableName: 'B', fieldId: 'oneToManyC', selectIn: 'C#__fk_manyToOneB' },
        { id: 'idA1', dbTableName: 'A', fieldId: 'oneToManyB', selectIn: 'B#__fk_manyToOneA' },
        { id: 'idC1', dbTableName: 'C', fieldId: 'manyToOneB', relationTo: 'idB1' },
        { id: 'idC2', dbTableName: 'C', fieldId: 'manyToOneB', relationTo: 'idB1' },
      ];
      const dependentRecordItems = [
        { id: 'idB1', dbTableName: 'B', fieldId: 'oneToManyB', relationTo: 'idA1' },
        { id: 'idB2', dbTableName: 'B', fieldId: 'oneToManyB', relationTo: 'idA1' },
        { id: 'idC1', dbTableName: 'C', fieldId: 'oneToManyC', relationTo: 'idB1' },
        { id: 'idC2', dbTableName: 'C', fieldId: 'oneToManyC', relationTo: 'idB1' },
      ];
      // topoOrder Graph:
      // C.fieldC -> B.oneToManyC -> B.fieldB -> A.oneToManyB
      //                                      -> C.manyToOneB
      const topoOrders = [
        {
          id: 'oneToManyC',
          dependencies: ['fieldC'],
        },
        {
          id: 'fieldB',
          dependencies: ['oneToManyC'],
        },
        {
          id: 'oneToManyB',
          dependencies: ['fieldB'],
        },
        {
          id: 'manyToOneB',
          dependencies: ['fieldB'],
        },
      ];
      const topoItems = service['createTopoItemWithRecords']({
        tableId2DbTableName,
        dbTableName2recordMap: dbTableName2records,
        affectedRecordItems,
        dependentRecordItems,
        fieldMap,
        fieldId2TableId,
        topoOrders,
      });
      expect(topoItems).toEqual(ordersWithRecords);
    });
  });
  describe('ReferenceService simple formula calculation', () => {
    let service: ReferenceService;
    beforeAll(async () => {
      const module: TestingModule = await Test.createTestingModule({
        imports: [GlobalModule, CalculationModule],
      }).compile();
      service = module.get<ReferenceService>(ReferenceService);
    });
    it('should correctly collect changes for Computed fields', () => {
      const fieldMap = {
        fieldA: createFieldInstanceByVo({
          id: 'fieldA',
          name: 'fieldA',
          type: FieldType.Number,
          options: {
            formatting: { type: NumberFormattingType.Decimal, precision: 1 },
          },
          cellValueType: CellValueType.Number,
          dbFieldType: DbFieldType.Real,
        } as NumberFieldDto),
        fieldB: createFieldInstanceByVo({
          id: 'fieldB',
          name: 'fieldB',
          type: FieldType.Formula,
          options: {
            expression: '{fieldA} & {fieldC}',
          },
          cellValueType: CellValueType.String,
          dbFieldType: DbFieldType.Text,
          isComputed: true,
        } as FormulaFieldDto),
        fieldC: createFieldInstanceByVo({
          id: 'fieldC',
          name: 'fieldC',
          type: FieldType.SingleLineText,
          cellValueType: CellValueType.String,
          dbFieldType: DbFieldType.Text,
        } as SingleLineTextFieldDto),
      };
      const fieldId2TableId = {
        fieldA: 'A',
        fieldB: 'A',
        fieldC: 'A',
      };
      const recordMap = {
        // use new value fieldA: 1 here
        idA1: { id: 'idA1', fields: { fieldA: 1, fieldB: null, fieldC: 'X' } },
      };
      // topoOrder Graph:
      // A.fieldA -> A.fieldB
      const ordersWithRecords = [
        {
          id: 'fieldB',
          dependencies: ['fieldA', 'fieldC'],
          recordItems: [
            {
              record: recordMap['idA1'],
            },
          ],
        },
      ];
      const changes = service['collectChanges'](ordersWithRecords, fieldMap, fieldId2TableId);
      expect(changes).toEqual([
        {
          tableId: 'A',
          recordId: 'idA1',
          fieldId: 'fieldB',
          oldValue: null,
          newValue: '1X',
        },
      ]);
    });
  });
});