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

teableio / teable / 21892749536

11 Feb 2026 04:28AM UTC coverage: 64.087% (-0.05%) from 64.133%
21892749536

push

github

web-flow
[sync] feat(observability): add OTEL metrics instrumentation and dashboards (T1991) (#1230) (#2586)

Synced from teableio/teable-ee@797b30c

Co-authored-by: teable-bot <bot@teable.io>

4978 of 6515 branches covered (76.41%)

1106 of 1794 new or added lines in 83 files covered. (61.65%)

46 existing lines in 11 files now uncovered.

22942 of 35798 relevant lines covered (64.09%)

9133.46 hits per line

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

70.83
/apps/nestjs-backend/src/features/base-sql-executor/base-sql-executor.service.ts
1
import { Injectable, Logger } from '@nestjs/common';
2
import { ConfigService } from '@nestjs/config';
3
import type { IDsn } from '@teable/core';
4
import { DriverClient, HttpErrorCode, 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 { CustomHttpException } from '../../custom.exception';
9
import { BASE_READ_ONLY_ROLE_PREFIX, BASE_SCHEMA_TABLE_READ_ONLY_ROLE_NAME } from './const';
10
import { checkTableAccess, validateRoleOperations } from './utils';
11

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

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

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

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

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

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

104
  async onModuleDestroy() {
105
    await this.db?.$disconnect();
264✔
106
  }
107

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

125
    // validate connection
126
    try {
270✔
127
      await connection.$queryRawUnsafe('SELECT 1');
270✔
128
      return connection;
270✔
129
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
130
    } catch (error: any) {
131
      await connection.$disconnect();
×
132
      throw new CustomHttpException(
×
133
        `database connection failed: ${error.message}`,
134
        HttpErrorCode.VALIDATION_ERROR,
135
        {
136
          localization: {
137
            i18nKey: 'httpErrors.baseSqlExecutor.databaseConnectionFailed',
138
            context: {
139
              message: error.message,
140
            },
141
          },
142
        }
143
      );
144
    }
145
  }
146

147
  private getReadOnlyRoleName(baseId: string) {
148
    return `${BASE_READ_ONLY_ROLE_PREFIX}${baseId}`;
12✔
149
  }
150

151
  async createReadOnlyRole(baseId: string) {
152
    const roleName = this.getReadOnlyRoleName(baseId);
4✔
153
    await this.prismaService
4✔
154
      .txClient()
155
      .$executeRawUnsafe(
156
        this.knex
157
          .raw(
158
            `CREATE ROLE ?? WITH NOLOGIN NOSUPERUSER NOINHERIT NOCREATEDB NOCREATEROLE NOREPLICATION`,
159
            [roleName]
160
          )
161
          .toQuery()
162
      );
163
    await this.prismaService
4✔
164
      .txClient()
165
      .$executeRawUnsafe(
166
        this.knex.raw(`GRANT USAGE ON SCHEMA ?? TO ??`, [baseId, roleName]).toQuery()
167
      );
168
    await this.prismaService
4✔
169
      .txClient()
170
      .$executeRawUnsafe(
171
        this.knex.raw(`GRANT SELECT ON ALL TABLES IN SCHEMA ?? TO ??`, [baseId, roleName]).toQuery()
172
      );
173
    await this.prismaService
4✔
174
      .txClient()
175
      .$executeRawUnsafe(
176
        this.knex
177
          .raw(`ALTER DEFAULT PRIVILEGES IN SCHEMA ?? GRANT SELECT ON TABLES TO ??`, [
178
            baseId,
179
            roleName,
180
          ])
181
          .toQuery()
182
      );
183
  }
184

185
  async dropReadOnlyRole(baseId: string) {
186
    const roleName = this.getReadOnlyRoleName(baseId);
×
187
    await this.prismaService
×
188
      .txClient()
189
      .$executeRawUnsafe(
190
        this.knex.raw(`REVOKE USAGE ON SCHEMA ?? FROM ??`, [baseId, roleName]).toQuery()
191
      );
192
    await this.prismaService
×
193
      .txClient()
194
      .$executeRawUnsafe(
195
        this.knex
196
          .raw(`REVOKE SELECT ON ALL TABLES IN SCHEMA ?? FROM ??`, [baseId, roleName])
197
          .toQuery()
198
      );
199
    await this.prismaService
×
200
      .txClient()
201
      .$executeRawUnsafe(
202
        this.knex
203
          .raw(`ALTER DEFAULT PRIVILEGES IN SCHEMA ?? REVOKE ALL ON TABLES FROM ??`, [
204
            baseId,
205
            roleName,
206
          ])
207
          .toQuery()
208
      );
209
    await this.prismaService
×
210
      .txClient()
211
      .$executeRawUnsafe(this.knex.raw(`DROP ROLE IF EXISTS ??`, [roleName]).toQuery());
212
  }
213

214
  async grantReadOnlyRole(baseId: string) {
215
    const roleName = this.getReadOnlyRoleName(baseId);
×
216
    await this.prismaService
×
217
      .txClient()
218
      .$executeRawUnsafe(
219
        this.knex.raw(`GRANT USAGE ON SCHEMA ?? TO ??`, [baseId, roleName]).toQuery()
220
      );
221
    await this.prismaService
×
222
      .txClient()
223
      .$executeRawUnsafe(
224
        this.knex.raw(`GRANT SELECT ON ALL TABLES IN SCHEMA ?? TO ??`, [baseId, roleName]).toQuery()
225
      );
226
    await this.prismaService
×
227
      .txClient()
228
      .$executeRawUnsafe(
229
        this.knex
230
          .raw(`ALTER DEFAULT PRIVILEGES IN SCHEMA ?? GRANT SELECT ON TABLES TO ??`, [
231
            baseId,
232
            roleName,
233
          ])
234
          .toQuery()
235
      );
236
  }
237

238
  private async roleExits(role: string): Promise<boolean> {
239
    const roleExists = await this.prismaService.$queryRaw<
544✔
240
      { count: bigint }[]
241
    >`SELECT count(*) FROM pg_roles WHERE rolname=${role}`;
242
    return Boolean(roleExists[0].count);
544✔
243
  }
244

245
  private async roleCheckAndCreate(baseId: string) {
246
    if (this.driver !== DriverClient.Pg) {
4✔
247
      return;
×
248
    }
249
    const roleName = this.getReadOnlyRoleName(baseId);
4✔
250
    if (!(await this.roleExits(roleName))) {
4✔
251
      try {
4✔
252
        await this.createReadOnlyRole(baseId);
4✔
253
      } catch (error) {
254
        // Handle race condition: another concurrent request may have already created the role
NEW
255
        if (
×
256
          error instanceof Prisma.PrismaClientKnownRequestError &&
257
          (error?.meta?.code === '42710' || error?.meta?.code === '23505')
258
        ) {
NEW
259
          this.logger.warn(
×
260
            `read only role ${roleName} already exists (concurrent creation), skipping`
261
          );
NEW
262
          return;
×
263
        }
NEW
264
        throw error;
×
265
      }
266
    }
267
  }
268

269
  private async setRole(prisma: Prisma.TransactionClient, baseId: string) {
270
    const roleName = this.getReadOnlyRoleName(baseId);
4✔
271
    await prisma.$executeRawUnsafe(this.knex.raw(`SET ROLE ??`, [roleName]).toQuery());
4✔
272
  }
273

274
  private async resetRole(prisma: Prisma.TransactionClient) {
275
    await prisma.$executeRawUnsafe(this.knex.raw(`RESET ROLE`).toQuery());
4✔
276
  }
277

278
  private async readonlyExecuteSql(sql: string) {
279
    return this.db?.$queryRawUnsafe(sql);
4✔
280
  }
281

282
  /**
283
   * check sql is safe
284
   * 1. role operations validation
285
   * 2. parse sql to valid table names
286
   * 3. read only role check table access
287
   */
288
  private async safeCheckSql(
289
    baseId: string,
290
    sql: string,
291
    opts?: { projectionTableDbNames?: string[]; projectionTableIds?: string[] }
292
  ) {
293
    const { projectionTableDbNames = [] } = opts ?? {};
4✔
294
    // 1. role operations keywords validation, only pg support
295
    if (this.driver == DriverClient.Pg) {
4✔
296
      validateRoleOperations(sql);
4✔
297
    }
298
    let tableNames = projectionTableDbNames;
4✔
299
    if (!projectionTableDbNames.length) {
4✔
300
      const tables = await this.prismaService.tableMeta.findMany({
2✔
301
        where: {
302
          baseId,
303
        },
304
        select: {
305
          dbTableName: true,
306
        },
307
      });
308
      tableNames = tables.map((table) => table.dbTableName);
2✔
309
    }
310
    // 2. parse sql to valid table names
311
    checkTableAccess(sql, {
4✔
312
      tableNames,
313
      database: this.driver,
314
    });
315
    // 3. read only role check table access, only pg and pg version > 14 support
316
    try {
4✔
317
      await this.readonlyExecuteSql(sql);
4✔
318
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
319
    } catch (error: any) {
320
      throw new CustomHttpException(
×
321
        `read only check failed: ${error?.meta?.message || error?.message}`,
322
        HttpErrorCode.VALIDATION_ERROR,
323
        {
324
          localization: {
325
            i18nKey: 'httpErrors.baseSqlExecutor.readOnlyCheckFailed',
326
            context: {
327
              message: error?.meta?.message || error?.message,
328
            },
329
          },
330
        }
331
      );
332
    }
333
  }
334

335
  async executeQuerySql<T = unknown>(
336
    baseId: string,
337
    sql: string,
338
    opts?: {
339
      projectionTableDbNames?: string[];
340
      projectionTableIds?: string[];
341
    }
342
  ) {
343
    await this.safeCheckSql(baseId, sql, opts);
4✔
344
    await this.roleCheckAndCreate(baseId);
4✔
345
    return this.prismaService.$tx(async (prisma) => {
4✔
346
      try {
4✔
347
        await this.setRole(prisma, baseId);
4✔
348
        return await prisma.$queryRawUnsafe<T>(sql);
4✔
349
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
350
      } catch (error: any) {
351
        throw new CustomHttpException(
2✔
352
          `execute query sql failed: ${error?.meta?.message || error?.message}`,
353
          HttpErrorCode.VALIDATION_ERROR,
354
          {
355
            localization: {
356
              i18nKey: 'httpErrors.baseSqlExecutor.executeQuerySqlFailed',
357
              context: {
358
                message: error?.meta?.message || error?.message,
359
              },
360
            },
361
          }
362
        );
363
      } finally {
364
        await this.resetRole(prisma).catch((error) => {
4✔
365
          console.log('resetRole error', error);
2✔
366
        });
367
      }
368
    });
369
  }
370
}
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

© 2026 Coveralls, Inc