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

teableio / teable / 10297357841

08 Aug 2024 06:51AM UTC coverage: 82.594% (+64.9%) from 17.728%
10297357841

Pull #721

github

web-flow
Merge a4d9fa381 into 4854acca2
Pull Request #721: feat: sql query

4344 of 4565 branches covered (95.16%)

663 of 710 new or added lines in 21 files covered. (93.38%)

29027 of 35144 relevant lines covered (82.59%)

1199.48 hits per line

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

92.58
/apps/nestjs-backend/src/features/base/base-query/base-query.service.ts
1
import { BadRequestException, Injectable, Logger, NotFoundException } from '@nestjs/common';
4✔
2
import { PrismaService } from '@teable/db-main-prisma';
4✔
3
import { BaseQueryColumnType, BaseQueryJoinType } from '@teable/openapi';
4✔
4
import type { IBaseQueryJoin, IBaseQuery, IBaseQueryVo, IBaseQueryColumn } from '@teable/openapi';
4✔
5
import { Knex } from 'knex';
4✔
6
import { InjectModel } from 'nest-knexjs';
4✔
7
import { ClsService } from 'nestjs-cls';
4✔
8
import { InjectDbProvider } from '../../../db-provider/db.provider';
4✔
9
import { IDbProvider } from '../../../db-provider/db.provider.interface';
4✔
10
import type { IClsStore } from '../../../types/cls';
4✔
11
import { FieldService } from '../../field/field.service';
4✔
12
import {
4✔
13
  convertFieldInstanceToFieldVo,
4✔
14
  createFieldInstanceByVo,
4✔
15
  type IFieldInstance,
4✔
16
} from '../../field/model/factory';
4✔
17
import { QueryAggregation } from './parse/aggregation';
4✔
18
import { QueryFilter } from './parse/filter';
4✔
19
import { QueryGroup } from './parse/group';
4✔
20
import { QueryOrder } from './parse/order';
4✔
21
import { QuerySelect } from './parse/select';
4✔
22
import { getQueryColumnTypeByFieldInstance } from './parse/utils';
4✔
23

4✔
24
@Injectable()
4✔
25
export class BaseQueryService {
4✔
26
  private logger = new Logger(BaseQueryService.name);
78✔
27

78✔
28
  constructor(
78✔
29
    @InjectModel('CUSTOM_KNEX') private readonly knex: Knex,
78✔
30
    @InjectDbProvider() private readonly dbProvider: IDbProvider,
78✔
31

78✔
32
    private readonly fieldService: FieldService,
78✔
33
    private readonly prismaService: PrismaService,
78✔
34
    private readonly cls: ClsService<IClsStore>
78✔
35
  ) {}
78✔
36

78✔
37
  private convertFieldMapToColumn(fieldMap: Record<string, IFieldInstance>): IBaseQueryColumn[] {
78✔
38
    return Object.values(fieldMap).map((field) => {
56✔
39
      const type = getQueryColumnTypeByFieldInstance(field);
124✔
40

124✔
41
      return {
124✔
42
        column: type === BaseQueryColumnType.Field ? field.dbFieldName : field.id,
124✔
43
        name: field.name,
124✔
44
        type,
124✔
45
        fieldSource:
124✔
46
          type === BaseQueryColumnType.Field ? convertFieldInstanceToFieldVo(field) : undefined,
124✔
47
      };
124✔
48
    });
124✔
49
  }
56✔
50

78✔
51
  private handleBigIntRows(rows: { [key in string]: unknown }[]) {
78✔
52
    return rows.map((row) => {
28✔
53
      return Object.entries(row).reduce(
42✔
54
        (acc, [key, value]) => {
42✔
55
          if (typeof value === 'bigint') {
106✔
NEW
56
            acc[key] = Number(value);
×
57
          } else {
106✔
58
            acc[key] = value;
106✔
59
          }
106✔
60
          return acc;
106✔
61
        },
106✔
62
        {} as { [key in string]: unknown }
42✔
63
      );
42✔
64
    });
42✔
65
  }
28✔
66

78✔
67
  async baseQuery(baseId: string, baseQuery: IBaseQuery): Promise<IBaseQueryVo> {
78✔
68
    const { queryBuilder, fieldMap } = await this.parseBaseQuery(baseId, baseQuery, 0);
28✔
69
    const query = queryBuilder.toQuery();
28✔
70
    console.log('query', query);
28✔
71
    console.log('columns', this.convertFieldMapToColumn(fieldMap));
28✔
72
    const rows = await this.prismaService
28✔
73
      .$queryRawUnsafe<{ [key in string]: unknown }[]>(query)
28✔
74
      .catch((e) => {
28✔
NEW
75
        this.logger.error(e);
×
NEW
76
        throw new BadRequestException(`Query failed: ${query}, ${e.message}`);
×
NEW
77
      });
×
78

28✔
79
    return {
28✔
80
      rows: this.handleBigIntRows(rows),
28✔
81
      columns: this.convertFieldMapToColumn(fieldMap),
28✔
82
    };
28✔
83
  }
28✔
84

78✔
85
  async parseBaseQuery(
78✔
86
    baseId: string,
40✔
87
    baseQuery: IBaseQuery,
40✔
88
    depth: number = 0
40✔
89
  ): Promise<{ queryBuilder: Knex.QueryBuilder; fieldMap: Record<string, IFieldInstance> }> {
40✔
90
    if (typeof baseQuery.from === 'string') {
40✔
91
      const dbTableName = await this.getDbTableName(baseId, baseQuery.from);
28✔
92
      const queryBuilder = this.knex(dbTableName);
28✔
93
      const fieldMap = await this.getFieldMap(baseQuery.from, dbTableName);
28✔
94
      return this.parseBaseQueryFromTable(baseQuery, {
28✔
95
        fieldMap,
28✔
96
        queryBuilder,
28✔
97
        baseId,
28✔
98
      });
28✔
99
    }
28✔
100
    const { queryBuilder, fieldMap } = await this.parseBaseQuery(baseId, baseQuery.from, depth + 1);
12✔
101
    const alias = 'source_query';
12✔
102
    return this.parseBaseQueryFromTable(baseQuery, {
12✔
103
      fieldMap: Object.keys(fieldMap).reduce(
12✔
104
        (acc, key) => {
12✔
105
          acc[key] = createFieldInstanceByVo({
24✔
106
            ...fieldMap[key],
24✔
107
            dbFieldName: `${alias}.${fieldMap[key].dbFieldName}`,
24✔
108
          });
24✔
109
          return acc;
24✔
110
        },
24✔
111
        {} as Record<string, IFieldInstance>
12✔
112
      ),
12✔
113
      queryBuilder: this.knex(queryBuilder.as(alias)),
12✔
114
      baseId,
12✔
115
    });
12✔
116
  }
12✔
117

78✔
118
  async parseBaseQueryFromTable(
78✔
119
    baseQuery: IBaseQuery,
40✔
120
    context: {
40✔
121
      baseId: string;
40✔
122
      fieldMap: Record<string, IFieldInstance>;
40✔
123
      queryBuilder: Knex.QueryBuilder;
40✔
124
    }
40✔
125
  ): Promise<{ queryBuilder: Knex.QueryBuilder; fieldMap: Record<string, IFieldInstance> }> {
40✔
126
    const { fieldMap, baseId, queryBuilder } = context;
40✔
127
    let currentQueryBuilder = queryBuilder;
40✔
128
    let currentFieldMap = fieldMap;
40✔
129
    if (baseQuery.join) {
40✔
130
      const { queryBuilder: joinedQueryBuilder, fieldMap: joinedFieldMap } = await this.joinTable(
6✔
131
        baseQuery.join,
6✔
132
        { baseId, fieldMap, queryBuilder }
6✔
133
      );
6✔
134
      currentQueryBuilder = joinedQueryBuilder;
6✔
135
      currentFieldMap = joinedFieldMap;
6✔
136
    }
6✔
137

40✔
138
    const { fieldMap: filteredFieldMap, queryBuilder: filteredQueryBuilder } =
40✔
139
      new QueryFilter().parse(baseQuery.where, {
40✔
140
        dbProvider: this.dbProvider,
40✔
141
        queryBuilder: currentQueryBuilder,
40✔
142
        fieldMap: currentFieldMap,
40✔
143
        currentUserId: this.cls.get('user.id'),
40✔
144
      });
40✔
145
    currentFieldMap = filteredFieldMap;
40✔
146
    currentQueryBuilder = filteredQueryBuilder;
40✔
147

40✔
148
    const { queryBuilder: groupedQueryBuilder, fieldMap: groupedFieldMap } = new QueryGroup().parse(
40✔
149
      baseQuery.groupBy,
40✔
150
      {
40✔
151
        dbProvider: this.dbProvider,
40✔
152
        queryBuilder: currentQueryBuilder,
40✔
153
        fieldMap: currentFieldMap,
40✔
154
      }
40✔
155
    );
40✔
156
    currentFieldMap = groupedFieldMap;
40✔
157
    currentQueryBuilder = groupedQueryBuilder;
40✔
158

40✔
159
    if (baseQuery.limit) {
40✔
160
      currentQueryBuilder.limit(baseQuery.limit);
2✔
161
    }
2✔
162
    if (baseQuery.offset) {
40✔
163
      currentQueryBuilder.offset(baseQuery.offset);
2✔
164
    }
2✔
165
    // clear select before aggregation and clear select in group by
40✔
166
    queryBuilder.clear('select');
40✔
167
    const { queryBuilder: aggregatedQueryBuilder, fieldMap: aggregatedFieldMap } =
40✔
168
      new QueryAggregation().parse(baseQuery.aggregation, {
40✔
169
        queryBuilder: currentQueryBuilder,
40✔
170
        fieldMap: currentFieldMap,
40✔
171
        dbTableName: '',
40✔
172
        dbProvider: this.dbProvider,
40✔
173
      });
40✔
174
    currentFieldMap = aggregatedFieldMap;
40✔
175
    currentQueryBuilder = aggregatedQueryBuilder;
40✔
176

40✔
177
    const { queryBuilder: orderedQueryBuilder, fieldMap: orderedFieldMap } = new QueryOrder().parse(
40✔
178
      baseQuery.orderBy,
40✔
179
      {
40✔
180
        dbProvider: this.dbProvider,
40✔
181
        queryBuilder: currentQueryBuilder,
40✔
182
        fieldMap: currentFieldMap,
40✔
183
      }
40✔
184
    );
40✔
185
    currentFieldMap = orderedFieldMap;
40✔
186
    currentQueryBuilder = orderedQueryBuilder;
40✔
187

40✔
188
    const { queryBuilder: selectedQueryBuilder, fieldMap: selectedFieldMap } =
40✔
189
      new QuerySelect().parse(baseQuery.select, {
40✔
190
        queryBuilder: currentQueryBuilder,
40✔
191
        fieldMap: currentFieldMap,
40✔
192
        // column must appear in the GROUP BY clause or be used in an aggregate function
40✔
193
        aggregation: baseQuery.aggregation,
40✔
194
        groupBy: baseQuery.groupBy,
40✔
195
      });
40✔
196

40✔
197
    return { queryBuilder: selectedQueryBuilder, fieldMap: selectedFieldMap };
40✔
198
  }
40✔
199

78✔
200
  async joinTable(
78✔
201
    joins: IBaseQueryJoin[],
6✔
202
    context: {
6✔
203
      baseId: string;
6✔
204
      fieldMap: Record<string, IFieldInstance>;
6✔
205
      queryBuilder: Knex.QueryBuilder;
6✔
206
    }
6✔
207
  ) {
6✔
208
    const { baseId, fieldMap, queryBuilder } = context;
6✔
209
    let resFieldMap = { ...fieldMap };
6✔
210
    for (const join of joins) {
6✔
211
      const joinTable = join.table;
6✔
212
      const joinDbTableName = await this.getDbTableName(baseId, joinTable);
6✔
213
      const joinFieldMap = await this.getFieldMap(joinTable, joinDbTableName);
6✔
214
      const joinedField = fieldMap[join.on[0]];
6✔
215
      const joinField = joinFieldMap[join.on[1]];
6✔
216
      resFieldMap = { ...resFieldMap, ...joinFieldMap };
6✔
217
      switch (join.type) {
6✔
218
        case BaseQueryJoinType.Inner:
6✔
219
          queryBuilder.innerJoin(
2✔
220
            joinDbTableName,
2✔
221
            joinedField.dbFieldName,
2✔
222
            '=',
2✔
223
            joinField.dbFieldName
2✔
224
          );
2✔
225
          break;
2✔
226
        case BaseQueryJoinType.Left:
6✔
227
          queryBuilder.leftJoin(
4✔
228
            joinDbTableName,
4✔
229
            joinedField.dbFieldName,
4✔
230
            '=',
4✔
231
            joinField.dbFieldName
4✔
232
          );
4✔
233
          break;
4✔
234
        case BaseQueryJoinType.Right:
6✔
NEW
235
          queryBuilder.rightJoin(
×
NEW
236
            joinDbTableName,
×
NEW
237
            joinedField.dbFieldName,
×
NEW
238
            '=',
×
NEW
239
            joinField.dbFieldName
×
NEW
240
          );
×
NEW
241
          break;
×
242
        case BaseQueryJoinType.Full:
6✔
NEW
243
          queryBuilder.fullOuterJoin(
×
NEW
244
            joinDbTableName,
×
NEW
245
            joinedField.dbFieldName,
×
NEW
246
            '=',
×
NEW
247
            joinField.dbFieldName
×
NEW
248
          );
×
NEW
249
          break;
×
250
        default:
6✔
NEW
251
          throw new BadRequestException(`Invalid join type: ${join.type}`);
×
252
      }
6✔
253
    }
6✔
254
    return { queryBuilder, fieldMap: resFieldMap };
6✔
255
  }
6✔
256

78✔
257
  async getFieldMap(tableId: string, dbTableName?: string) {
78✔
258
    const fields = await this.fieldService.getFieldInstances(tableId, {});
34✔
259
    return fields.reduce(
34✔
260
      (acc, field) => {
34✔
261
        if (dbTableName) {
90✔
262
          field.dbFieldName = `${dbTableName}.${field.dbFieldName}`;
90✔
263
        }
90✔
264
        acc[field.id] = field;
90✔
265
        return acc;
90✔
266
      },
90✔
267
      {} as Record<string, IFieldInstance>
34✔
268
    );
34✔
269
  }
34✔
270

78✔
271
  private async getDbTableName(baseId: string, tableId: string) {
78✔
272
    const tableMeta = await this.prismaService
34✔
273
      .txClient()
34✔
274
      .tableMeta.findUniqueOrThrow({
34✔
275
        where: { id: tableId, baseId },
34✔
276
        select: { dbTableName: true },
34✔
277
      })
34✔
278
      .catch(() => {
34✔
NEW
279
        throw new NotFoundException('Table not found');
×
NEW
280
      });
×
281
    return tableMeta.dbTableName;
34✔
282
  }
34✔
283
}
78✔
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