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

teableio / teable / 19229647709

10 Nov 2025 11:12AM UTC coverage: 75.121% (+0.01%) from 75.11%
19229647709

push

github

web-flow
fix: fix formula on calculation (#2110)

* fix: fix formula on calculation

* fix: fix multi value cast type

11081 of 11936 branches covered (92.84%)

69 of 69 new or added lines in 3 files covered. (100.0%)

9 existing lines in 1 file now uncovered.

53560 of 71298 relevant lines covered (75.12%)

4301.56 hits per line

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

71.7
/apps/nestjs-backend/src/features/base-sql-executor/base-sql-executor.service.ts
1
import { BadRequestException, Injectable, Logger } from '@nestjs/common';
1✔
2
import { ConfigService } from '@nestjs/config';
3
import type { IDsn } from '@teable/core';
4
import { DriverClient, parseDsn } from '@teable/core';
5
import { Prisma, PrismaService, PrismaClient } from '@teable/db-main-prisma';
6
import { Knex } from 'knex';
7
import { InjectModel } from 'nest-knexjs';
8
import { BASE_READ_ONLY_ROLE_PREFIX, BASE_SCHEMA_TABLE_READ_ONLY_ROLE_NAME } from './const';
9
import { checkTableAccess, validateRoleOperations } from './utils';
10

11
@Injectable()
12
export class BaseSqlExecutorService {
1✔
13
  private db?: PrismaClient;
92✔
14
  private readonly dsn: IDsn;
92✔
15
  readonly driver: DriverClient;
92✔
16
  private hasPgReadAllDataRole?: boolean;
92✔
17
  private readonly logger = new Logger(BaseSqlExecutorService.name);
92✔
18

19
  constructor(
92✔
20
    private readonly prismaService: PrismaService,
92✔
21
    private readonly configService: ConfigService,
92✔
22
    @InjectModel('CUSTOM_KNEX') private readonly knex: Knex
92✔
23
  ) {
92✔
24
    this.dsn = parseDsn(this.getDatabaseUrl());
92✔
25
    this.driver = this.dsn.driver as DriverClient;
92✔
26
  }
92✔
27

28
  private getDatabaseUrl() {
92✔
29
    return (
92✔
30
      this.configService.get<string>('PRISMA_DATABASE_URL_FOR_SQL_EXECUTOR') ||
92✔
31
      this.configService.getOrThrow<string>('PRISMA_DATABASE_URL')
92✔
32
    );
33
  }
92✔
34

35
  private getDisablePreSqlExecutorCheck() {
92✔
36
    return this.configService.get<string>('DISABLE_PRE_SQL_EXECUTOR_CHECK') === 'true';
92✔
37
  }
92✔
38

39
  private async getReadOnlyDatabaseConnectionConfig(): Promise<string | undefined> {
92✔
40
    if (this.driver === DriverClient.Sqlite) {
92!
41
      return;
×
42
    }
×
43
    if (!this.hasPgReadAllDataRole) {
92!
44
      return;
×
45
    }
×
46
    const isExistReadOnlyRole = await this.roleExits(BASE_SCHEMA_TABLE_READ_ONLY_ROLE_NAME);
92✔
47
    if (!isExistReadOnlyRole) {
92✔
48
      await this.prismaService.$tx(async (prisma) => {
1✔
49
        try {
1✔
50
          await prisma.$executeRawUnsafe(
1✔
51
            this.knex
1✔
52
              .raw(
1✔
53
                `CREATE ROLE ?? WITH LOGIN PASSWORD ? NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION`,
1✔
54
                [BASE_SCHEMA_TABLE_READ_ONLY_ROLE_NAME, this.dsn.pass]
1✔
55
              )
56
              .toQuery()
1✔
57
          );
58
          await prisma.$executeRawUnsafe(
1✔
59
            this.knex
1✔
60
              .raw(`GRANT pg_read_all_data TO ??`, [BASE_SCHEMA_TABLE_READ_ONLY_ROLE_NAME])
1✔
61
              .toQuery()
1✔
62
          );
63
        } catch (error) {
1!
UNCOV
64
          if (
×
UNCOV
65
            error instanceof Prisma.PrismaClientKnownRequestError &&
×
UNCOV
66
            (error?.meta?.code === '42710' ||
×
67
              error?.meta?.code === '23505' ||
×
UNCOV
68
              error?.meta?.code === 'XX000')
×
UNCOV
69
          ) {
×
UNCOV
70
            this.logger.warn(
×
UNCOV
71
              `read only role ${BASE_SCHEMA_TABLE_READ_ONLY_ROLE_NAME} already exists or concurrent update detected, error code: ${error?.meta?.code}`
×
72
            );
UNCOV
73
            return;
×
UNCOV
74
          }
×
75
          throw error;
×
76
        }
×
77
      });
1✔
78
    }
1✔
79
    return `postgresql://${BASE_SCHEMA_TABLE_READ_ONLY_ROLE_NAME}:${this.dsn.pass}@${this.dsn.host}:${this.dsn.port}/${this.dsn.db}${
92✔
80
      this.dsn.params
92✔
81
        ? `?${Object.entries(this.dsn.params)
92✔
82
            .map(([key, value]) => `${key}=${value}`)
92✔
83
            .join('&')}`
92✔
84
        : ''
×
85
    }`;
92✔
86
  }
92✔
87

88
  async onModuleInit() {
92✔
89
    if (this.driver !== DriverClient.Pg) {
92!
90
      return;
×
91
    }
×
92
    if (this.getDisablePreSqlExecutorCheck()) {
92!
93
      return;
×
94
    }
×
95
    // if pg_read_all_data role not exist, no need to create read only role
92✔
96
    this.hasPgReadAllDataRole = await this.roleExits('pg_read_all_data');
92✔
97
    if (!this.hasPgReadAllDataRole) {
92!
98
      return;
×
99
    }
×
100
    this.db = await this.createConnection();
92✔
101
  }
92✔
102

103
  async onModuleDestroy() {
92✔
104
    await this.db?.$disconnect();
91✔
105
  }
91✔
106

107
  private async createConnection(): Promise<PrismaClient | undefined> {
92✔
108
    if (this.db) {
92!
109
      return this.db;
×
110
    }
×
111
    const connectionConfig = await this.getReadOnlyDatabaseConnectionConfig();
92✔
112
    if (!connectionConfig) {
92!
113
      return;
×
114
    }
×
115
    const connection = new PrismaClient({
92✔
116
      datasources: {
92✔
117
        db: {
92✔
118
          url: connectionConfig,
92✔
119
        },
92✔
120
      },
92✔
121
    });
92✔
122
    await connection.$connect();
92✔
123

124
    // validate connection
92✔
125
    try {
92✔
126
      await connection.$queryRawUnsafe('SELECT 1');
92✔
127
      return connection;
92✔
128
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
92✔
129
    } catch (error: any) {
92!
130
      await connection.$disconnect();
×
131
      throw new Error(`database connection failed: ${error.message}`);
×
132
    }
×
133
  }
92✔
134

135
  private getReadOnlyRoleName(baseId: string) {
92✔
136
    return `${BASE_READ_ONLY_ROLE_PREFIX}${baseId}`;
6✔
137
  }
6✔
138

139
  async createReadOnlyRole(baseId: string) {
92✔
140
    const roleName = this.getReadOnlyRoleName(baseId);
2✔
141
    await this.prismaService
2✔
142
      .txClient()
2✔
143
      .$executeRawUnsafe(
2✔
144
        this.knex
2✔
145
          .raw(
2✔
146
            `CREATE ROLE ?? WITH NOLOGIN NOSUPERUSER NOINHERIT NOCREATEDB NOCREATEROLE NOREPLICATION`,
2✔
147
            [roleName]
2✔
148
          )
149
          .toQuery()
2✔
150
      );
151
    await this.prismaService
2✔
152
      .txClient()
2✔
153
      .$executeRawUnsafe(
2✔
154
        this.knex.raw(`GRANT USAGE ON SCHEMA ?? TO ??`, [baseId, roleName]).toQuery()
2✔
155
      );
156
    await this.prismaService
2✔
157
      .txClient()
2✔
158
      .$executeRawUnsafe(
2✔
159
        this.knex.raw(`GRANT SELECT ON ALL TABLES IN SCHEMA ?? TO ??`, [baseId, roleName]).toQuery()
2✔
160
      );
161
    await this.prismaService
2✔
162
      .txClient()
2✔
163
      .$executeRawUnsafe(
2✔
164
        this.knex
2✔
165
          .raw(`ALTER DEFAULT PRIVILEGES IN SCHEMA ?? GRANT SELECT ON TABLES TO ??`, [
2✔
166
            baseId,
2✔
167
            roleName,
2✔
168
          ])
2✔
169
          .toQuery()
2✔
170
      );
171
  }
2✔
172

173
  async dropReadOnlyRole(baseId: string) {
92✔
174
    const roleName = this.getReadOnlyRoleName(baseId);
×
175
    await this.prismaService
×
176
      .txClient()
×
177
      .$executeRawUnsafe(
×
178
        this.knex.raw(`REVOKE USAGE ON SCHEMA ?? FROM ??`, [baseId, roleName]).toQuery()
×
179
      );
180
    await this.prismaService
×
181
      .txClient()
×
182
      .$executeRawUnsafe(
×
183
        this.knex
×
184
          .raw(`REVOKE SELECT ON ALL TABLES IN SCHEMA ?? FROM ??`, [baseId, roleName])
×
185
          .toQuery()
×
186
      );
187
    await this.prismaService
×
188
      .txClient()
×
189
      .$executeRawUnsafe(
×
190
        this.knex
×
191
          .raw(`ALTER DEFAULT PRIVILEGES IN SCHEMA ?? REVOKE ALL ON TABLES FROM ??`, [
×
192
            baseId,
×
193
            roleName,
×
194
          ])
×
195
          .toQuery()
×
196
      );
197
    await this.prismaService
×
198
      .txClient()
×
199
      .$executeRawUnsafe(this.knex.raw(`DROP ROLE IF EXISTS ??`, [roleName]).toQuery());
×
200
  }
×
201

202
  async grantReadOnlyRole(baseId: string) {
92✔
203
    const roleName = this.getReadOnlyRoleName(baseId);
×
204
    await this.prismaService
×
205
      .txClient()
×
206
      .$executeRawUnsafe(
×
207
        this.knex.raw(`GRANT USAGE ON SCHEMA ?? TO ??`, [baseId, roleName]).toQuery()
×
208
      );
209
    await this.prismaService
×
210
      .txClient()
×
211
      .$executeRawUnsafe(
×
212
        this.knex.raw(`GRANT SELECT ON ALL TABLES IN SCHEMA ?? TO ??`, [baseId, roleName]).toQuery()
×
213
      );
214
    await this.prismaService
×
215
      .txClient()
×
216
      .$executeRawUnsafe(
×
217
        this.knex
×
218
          .raw(`ALTER DEFAULT PRIVILEGES IN SCHEMA ?? GRANT SELECT ON TABLES TO ??`, [
×
219
            baseId,
×
220
            roleName,
×
221
          ])
×
222
          .toQuery()
×
223
      );
224
  }
×
225

226
  private async roleExits(role: string): Promise<boolean> {
92✔
227
    const roleExists = await this.prismaService.$queryRaw<
186✔
228
      { count: bigint }[]
229
    >`SELECT count(*) FROM pg_roles WHERE rolname=${role}`;
186✔
230
    return Boolean(roleExists[0].count);
186✔
231
  }
186✔
232

233
  private async roleCheckAndCreate(baseId: string) {
92✔
234
    if (this.driver !== DriverClient.Pg) {
2!
235
      return;
×
236
    }
×
237
    const roleName = this.getReadOnlyRoleName(baseId);
2✔
238
    if (!(await this.roleExits(roleName))) {
2✔
239
      await this.createReadOnlyRole(baseId);
2✔
240
    }
2✔
241
  }
2✔
242

243
  private async setRole(prisma: Prisma.TransactionClient, baseId: string) {
92✔
244
    const roleName = this.getReadOnlyRoleName(baseId);
2✔
245
    await prisma.$executeRawUnsafe(this.knex.raw(`SET ROLE ??`, [roleName]).toQuery());
2✔
246
  }
2✔
247

248
  private async resetRole(prisma: Prisma.TransactionClient) {
92✔
249
    await prisma.$executeRawUnsafe(this.knex.raw(`RESET ROLE`).toQuery());
2✔
250
  }
1✔
251

252
  private async readonlyExecuteSql(sql: string) {
92✔
253
    return this.db?.$queryRawUnsafe(sql);
2✔
254
  }
2✔
255

256
  /**
92✔
257
   * check sql is safe
258
   * 1. role operations validation
259
   * 2. parse sql to valid table names
260
   * 3. read only role check table access
261
   */
92✔
262
  private async safeCheckSql(
92✔
263
    baseId: string,
2✔
264
    sql: string,
2✔
265
    opts?: { projectionTableDbNames?: string[]; projectionTableIds?: string[] }
2✔
266
  ) {
2✔
267
    const { projectionTableDbNames = [] } = opts ?? {};
2✔
268
    // 1. role operations keywords validation, only pg support
2✔
269
    if (this.driver == DriverClient.Pg) {
2✔
270
      validateRoleOperations(sql);
2✔
271
    }
2✔
272
    let tableNames = projectionTableDbNames;
2✔
273
    if (!projectionTableDbNames.length) {
2✔
274
      const tables = await this.prismaService.tableMeta.findMany({
1✔
275
        where: {
1✔
276
          baseId,
1✔
277
        },
1✔
278
        select: {
1✔
279
          dbTableName: true,
1✔
280
        },
1✔
281
      });
1✔
282
      tableNames = tables.map((table) => table.dbTableName);
1✔
283
    }
1✔
284
    // 2. parse sql to valid table names
2✔
285
    checkTableAccess(sql, {
2✔
286
      tableNames,
2✔
287
      database: this.driver,
2✔
288
    });
2✔
289
    // 3. read only role check table access, only pg and pg version > 14 support
2✔
290
    await this.readonlyExecuteSql(sql);
2✔
291
  }
2✔
292

293
  async executeQuerySql<T = unknown>(
92✔
294
    baseId: string,
2✔
295
    sql: string,
2✔
296
    opts?: {
2✔
297
      projectionTableDbNames?: string[];
298
      projectionTableIds?: string[];
299
    }
2✔
300
  ) {
2✔
301
    await this.safeCheckSql(baseId, sql, opts);
2✔
302
    await this.roleCheckAndCreate(baseId);
2✔
303
    return this.prismaService.$tx(async (prisma) => {
2✔
304
      try {
2✔
305
        await this.setRole(prisma, baseId);
2✔
306
        return await prisma.$queryRawUnsafe<T>(sql);
2✔
307
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
2✔
308
      } catch (error: any) {
2✔
309
        throw new BadRequestException(error?.meta?.message || error?.message);
1!
310
      } finally {
2!
311
        await this.resetRole(prisma).catch((error) => {
2✔
312
          console.log('resetRole error', error);
1✔
313
        });
1✔
314
      }
2✔
315
    });
2✔
316
  }
2✔
317
}
92✔
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