Skip to content

Commit 27b5414

Browse files
committed
feat: implement record query builder module and service with dependency injection
1 parent 833b6cf commit 27b5414

File tree

8 files changed

+196
-27
lines changed

8 files changed

+196
-27
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export type { IRecordQueryBuilder, IRecordQueryParams } from './record-query-builder.interface';
2+
export { RecordQueryBuilderService } from './record-query-builder.service';
3+
export { RecordQueryBuilderModule } from './record-query-builder.module';
4+
export { RECORD_QUERY_BUILDER_SYMBOL } from './record-query-builder.symbol';
5+
export { InjectRecordQueryBuilder } from './record-query-builder.provider';
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import type { Knex } from 'knex';
2+
import type { IFieldInstance } from '../../field/model/factory';
3+
4+
/**
5+
* Interface for record query builder service
6+
* This interface defines the public API for building table record queries
7+
*/
8+
export interface IRecordQueryBuilder {
9+
/**
10+
* Build a query builder with select fields for the given table and fields
11+
* @param queryBuilder - existing query builder to use
12+
* @param tableId - The table ID
13+
* @param viewId - Optional view ID for filtering
14+
* @param fields - Array of field instances to select
15+
* @returns Promise<Knex.QueryBuilder> - The configured query builder
16+
*/
17+
buildQuery(
18+
queryBuilder: Knex.QueryBuilder,
19+
tableId: string,
20+
viewId: string | undefined,
21+
fields: IFieldInstance[]
22+
): Knex.QueryBuilder;
23+
}
24+
25+
/**
26+
* Parameters for building record queries
27+
*/
28+
export interface IRecordQueryParams {
29+
/** The table ID */
30+
tableId: string;
31+
/** Optional view ID */
32+
viewId?: string;
33+
/** Array of field instances */
34+
fields: IFieldInstance[];
35+
/** Optional database table name (if already known) */
36+
dbTableName?: string;
37+
/** Optional existing query builder */
38+
queryBuilder: Knex.QueryBuilder;
39+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { Module } from '@nestjs/common';
2+
import { PrismaModule } from '@teable/db-main-prisma';
3+
import { DbProvider } from '../../../db-provider/db.provider';
4+
import { RecordQueryBuilderService } from './record-query-builder.service';
5+
import { RECORD_QUERY_BUILDER_SYMBOL } from './record-query-builder.symbol';
6+
7+
/**
8+
* Module for record query builder functionality
9+
* This module provides services for building table record queries
10+
*/
11+
@Module({
12+
imports: [PrismaModule],
13+
providers: [
14+
DbProvider,
15+
{
16+
provide: RECORD_QUERY_BUILDER_SYMBOL,
17+
useClass: RecordQueryBuilderService,
18+
},
19+
],
20+
exports: [RECORD_QUERY_BUILDER_SYMBOL],
21+
})
22+
export class RecordQueryBuilderModule {}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { Inject } from '@nestjs/common';
2+
import { RECORD_QUERY_BUILDER_SYMBOL } from './record-query-builder.symbol';
3+
4+
// eslint-disable-next-line @typescript-eslint/naming-convention
5+
export const InjectRecordQueryBuilder = () => Inject(RECORD_QUERY_BUILDER_SYMBOL);
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import { Injectable } from '@nestjs/common';
2+
import { FieldType, type IFormulaConversionContext } from '@teable/core';
3+
import { PrismaService } from '@teable/db-main-prisma';
4+
import { Knex } from 'knex';
5+
import { InjectModel } from 'nest-knexjs';
6+
import { InjectDbProvider } from '../../../db-provider/db.provider';
7+
import { IDbProvider } from '../../../db-provider/db.provider.interface';
8+
import { FieldSelectVisitor } from '../../field/field-select-visitor';
9+
import type { IFieldInstance } from '../../field/model/factory';
10+
import type { IRecordQueryBuilder, IRecordQueryParams } from './record-query-builder.interface';
11+
12+
/**
13+
* Service for building table record queries
14+
* This service encapsulates the logic for creating Knex query builders
15+
* with proper field selection using the visitor pattern
16+
*/
17+
@Injectable()
18+
export class RecordQueryBuilderService implements IRecordQueryBuilder {
19+
constructor(
20+
private readonly prismaService: PrismaService,
21+
@InjectModel('CUSTOM_KNEX') private readonly knex: Knex,
22+
@InjectDbProvider() private readonly dbProvider: IDbProvider
23+
) {}
24+
25+
/**
26+
* Build a query builder with select fields for the given table and fields
27+
*/
28+
buildQuery(
29+
queryBuilder: Knex.QueryBuilder,
30+
tableId: string,
31+
viewId: string | undefined,
32+
fields: IFieldInstance[]
33+
): Knex.QueryBuilder {
34+
const params: IRecordQueryParams = {
35+
tableId,
36+
viewId,
37+
fields,
38+
queryBuilder,
39+
};
40+
41+
return this.buildQueryWithParams(params);
42+
}
43+
44+
/**
45+
* Build query with detailed parameters
46+
*/
47+
private buildQueryWithParams(params: IRecordQueryParams): Knex.QueryBuilder {
48+
const { fields, queryBuilder } = params;
49+
50+
// Build formula conversion context
51+
const context = this.buildFormulaContext(fields);
52+
53+
// Build select fields
54+
return this.buildSelect(queryBuilder, fields, context);
55+
}
56+
57+
/**
58+
* Build select fields using visitor pattern
59+
*/
60+
private buildSelect(
61+
qb: Knex.QueryBuilder,
62+
fields: IFieldInstance[],
63+
context: IFormulaConversionContext
64+
): Knex.QueryBuilder {
65+
const visitor = new FieldSelectVisitor(this.knex, qb, this.dbProvider, context);
66+
67+
// Add default system fields
68+
qb.select(['__id', '__version', '__created_time', '__last_modified_time']);
69+
70+
// Add field-specific selections using visitor pattern
71+
for (const field of fields) {
72+
field.accept(visitor);
73+
}
74+
75+
return qb;
76+
}
77+
78+
/**
79+
* Get database table name for a given table ID
80+
*/
81+
private async getDbTableName(tableId: string): Promise<string> {
82+
const table = await this.prismaService.txClient().tableMeta.findUniqueOrThrow({
83+
where: { id: tableId },
84+
select: { dbTableName: true },
85+
});
86+
87+
return table.dbTableName;
88+
}
89+
90+
/**
91+
* Build formula conversion context from fields
92+
*/
93+
private buildFormulaContext(fields: IFieldInstance[]): IFormulaConversionContext {
94+
return {
95+
fieldMap: fields.reduce(
96+
(acc, field) => {
97+
acc[field.id] = {
98+
columnName: field.dbFieldName,
99+
fieldType: field.type,
100+
dbGenerated: field.type === FieldType.Formula && field.options.dbGenerated,
101+
};
102+
return acc;
103+
},
104+
{} as Record<string, IFormulaConversionContext['fieldMap'][string]>
105+
),
106+
};
107+
}
108+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
/* eslint-disable @typescript-eslint/naming-convention */
2+
/**
3+
* Injection token for the record query builder service
4+
* This symbol is used for dependency injection to avoid direct class references
5+
*/
6+
export const RECORD_QUERY_BUILDER_SYMBOL = Symbol('RECORD_QUERY_BUILDER');

apps/nestjs-backend/src/features/record/record-query.service.ts

Lines changed: 9 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { FieldSelectVisitor } from '../field/field-select-visitor';
1212
import type { IFieldInstance } from '../field/model/factory';
1313
import { createFieldInstanceByRaw } from '../field/model/factory';
1414
import type { FormulaFieldDto } from '../field/model/field-dto/formula-field.dto';
15+
import { InjectRecordQueryBuilder, IRecordQueryBuilder } from './query-builder';
1516

1617
/**
1718
* Service for querying record data
@@ -24,7 +25,8 @@ export class RecordQueryService {
2425
constructor(
2526
private readonly prismaService: PrismaService,
2627
@InjectModel('CUSTOM_KNEX') private readonly knex: Knex,
27-
@InjectDbProvider() private readonly dbProvider: IDbProvider
28+
@InjectDbProvider() private readonly dbProvider: IDbProvider,
29+
@InjectRecordQueryBuilder() private readonly recordQueryBuilder: IRecordQueryBuilder
2830
) {}
2931

3032
/**
@@ -70,37 +72,18 @@ export class RecordQueryService {
7072

7173
const qb = this.knex(table.dbTableName);
7274

73-
const context = {
74-
fieldMap: fields.reduce(
75-
(acc, field) => {
76-
acc[field.id] = {
77-
columnName: field.dbFieldName,
78-
fieldType: field.type,
79-
dbGenerated: field.type === FieldType.Formula && field.options.dbGenerated,
80-
};
81-
82-
return acc;
83-
},
84-
{} as Record<string, { columnName: string; fieldType: string; dbGenerated: boolean }>
85-
),
86-
};
87-
88-
const visitor = new FieldSelectVisitor(this.knex, qb, this.dbProvider, context);
89-
90-
qb.select(['__id', '__version', '__created_time', '__last_modified_time']);
91-
92-
for (const field of fields) {
93-
field.accept(visitor);
94-
}
75+
const sql = this.recordQueryBuilder
76+
.buildQuery(qb, tableId, undefined, fields)
77+
.whereIn('__id', recordIds)
78+
.toQuery();
9579

9680
// Query records from database
97-
const query = qb.whereIn('__id', recordIds);
9881

99-
this.logger.debug(`Querying records: ${query.toQuery()}`);
82+
this.logger.debug(`Querying records: ${sql}`);
10083

10184
const rawRecords = await this.prismaService
10285
.txClient()
103-
.$queryRawUnsafe<{ [key: string]: unknown }[]>(query.toQuery());
86+
.$queryRawUnsafe<{ [key: string]: unknown }[]>(sql);
10487

10588
// Convert raw records to IRecord format
10689
const snapshots: { id: string; data: IRecord }[] = [];

apps/nestjs-backend/src/features/record/record.module.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,14 @@ import { DbProvider } from '../../db-provider/db.provider';
33
import { AttachmentsStorageModule } from '../attachments/attachments-storage.module';
44
import { CalculationModule } from '../calculation/calculation.module';
55
import { TableIndexService } from '../table/table-index.service';
6+
import { RecordQueryBuilderModule } from './query-builder';
67
import { RecordPermissionService } from './record-permission.service';
78
import { RecordQueryService } from './record-query.service';
89
import { RecordService } from './record.service';
910
import { UserNameListener } from './user-name.listener.service';
1011

1112
@Module({
12-
imports: [CalculationModule, AttachmentsStorageModule],
13+
imports: [CalculationModule, AttachmentsStorageModule, RecordQueryBuilderModule],
1314
providers: [
1415
UserNameListener,
1516
RecordService,

0 commit comments

Comments
 (0)