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

teableio / teable / 15039567037

15 May 2025 07:49AM UTC coverage: 80.625% (-0.4%) from 81.043%
15039567037

push

github

web-flow
feat: base chat UI (#1524)

* feat: add base sql executor services

* feat: chatbot ui

* feat: chat loading

* feat: full chat ui

* fix: build error

* feat: support add context for messages

* feat: add reason message part render

* fix: reason part expand

* chore: update pnpm lock

* feat: preview html

* feat: memo ai markdown block

* chore: update pnpm lock

* feat: ai message add consumed credits and time

* fix: meta content is empty display

* fix: import url

* chore: update pnpm-lock.yaml

* fix: display token usage

* fix: magic ai icons

* feat: chat model only display model name

* fix: missing import

* feat: message rerender

* Update apps/nestjs-backend/src/features/base-sql-executor/utils.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Boris <boris2code@outlook.com>

* chore: lint code

* chore: update i18n

---------

Signed-off-by: Boris <boris2code@outlook.com>
Co-authored-by: tea artist <artist@teable.io>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

7869 of 8373 branches covered (93.98%)

53 of 297 new or added lines in 4 files covered. (17.85%)

5 existing lines in 2 files now uncovered.

37339 of 46312 relevant lines covered (80.62%)

1762.75 hits per line

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

0.0
/apps/nestjs-backend/src/features/base-sql-executor/base-sql-executor.service.ts
NEW
1
import { BadRequestException, Injectable } from '@nestjs/common';
×
2
import { ConfigService } from '@nestjs/config';
3
import type { IDsn } from '@teable/core';
4
import { DriverClient, getRandomString, parseDsn } from '@teable/core';
5
import type { Prisma } from '@teable/db-main-prisma';
6
import { PrismaService } from '@teable/db-main-prisma';
7
import knex, { Knex } from 'knex';
8
import { InjectModel } from 'nest-knexjs';
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()
NEW
13
export class BaseSqlExecutorService {
×
NEW
14
  private db?: Knex;
×
NEW
15
  private readonly dsn: IDsn;
×
NEW
16
  readonly driver: DriverClient;
×
NEW
17
  private hasPgReadAllDataRole?: boolean;
×
18

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

NEW
28
  private getDatabaseUrl() {
×
NEW
29
    return this.configService.getOrThrow<string>('PRISMA_DATABASE_URL');
×
NEW
30
  }
×
31

NEW
32
  private async getReadOnlyDatabaseConnectionConfig(): Promise<
×
33
    Knex.PgConnectionConfig | undefined
NEW
34
  > {
×
NEW
35
    if (this.driver === DriverClient.Sqlite) {
×
NEW
36
      return;
×
NEW
37
    }
×
NEW
38
    if (!this.hasPgReadAllDataRole) {
×
NEW
39
      return;
×
NEW
40
    }
×
NEW
41
    const isExistReadOnlyRole = await this.roleExits(BASE_SCHEMA_TABLE_READ_ONLY_ROLE_NAME);
×
NEW
42
    if (!isExistReadOnlyRole) {
×
NEW
43
      await this.prismaService.$tx(async (prisma) => {
×
NEW
44
        await prisma.$executeRawUnsafe(
×
NEW
45
          this.knex
×
NEW
46
            .raw(
×
NEW
47
              `CREATE ROLE ?? WITH LOGIN PASSWORD ? NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION`,
×
NEW
48
              [BASE_SCHEMA_TABLE_READ_ONLY_ROLE_NAME, this.dsn.pass]
×
49
            )
NEW
50
            .toQuery()
×
51
        );
NEW
52
        await prisma.$executeRawUnsafe(
×
NEW
53
          this.knex
×
NEW
54
            .raw(`GRANT pg_read_all_data TO ??`, [BASE_SCHEMA_TABLE_READ_ONLY_ROLE_NAME])
×
NEW
55
            .toQuery()
×
56
        );
NEW
57
      });
×
NEW
58
    }
×
NEW
59
    return {
×
NEW
60
      ...this.dsn,
×
NEW
61
      database: this.dsn.db,
×
NEW
62
      password: this.dsn.pass,
×
NEW
63
      query_timeout: 10000,
×
NEW
64
      user: BASE_SCHEMA_TABLE_READ_ONLY_ROLE_NAME,
×
NEW
65
    };
×
NEW
66
  }
×
67

NEW
68
  async onModuleInit() {
×
NEW
69
    // if pg_read_all_data role not exist, no need to create read only role
×
NEW
70
    this.hasPgReadAllDataRole = await this.roleExits('pg_read_all_data');
×
NEW
71
    if (!this.hasPgReadAllDataRole) {
×
NEW
72
      return;
×
NEW
73
    }
×
NEW
74
    this.db = await this.createConnection();
×
NEW
75
  }
×
76

NEW
77
  async onModuleDestroy() {
×
NEW
78
    await this.db?.destroy();
×
NEW
79
  }
×
80

NEW
81
  private async createConnection(): Promise<Knex | undefined> {
×
NEW
82
    if (this.db) {
×
NEW
83
      return this.db;
×
NEW
84
    }
×
NEW
85
    const connectionConfig = await this.getReadOnlyDatabaseConnectionConfig();
×
NEW
86
    if (!connectionConfig) {
×
NEW
87
      return;
×
NEW
88
    }
×
NEW
89
    const connection = knex({
×
NEW
90
      client: this.driver,
×
NEW
91
      connection: connectionConfig,
×
NEW
92
    });
×
93

NEW
94
    // validate connection
×
NEW
95
    try {
×
NEW
96
      await connection.raw('SELECT 1');
×
NEW
97
      return connection;
×
NEW
98
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
×
NEW
99
    } catch (error: any) {
×
NEW
100
      await connection.destroy();
×
NEW
101
      throw new Error(`database connection failed: ${error.message}`);
×
NEW
102
    }
×
NEW
103
  }
×
104

NEW
105
  private getReadOnlyRoleName(baseId: string) {
×
NEW
106
    return `${BASE_READ_ONLY_ROLE_PREFIX}${baseId}`;
×
NEW
107
  }
×
108

NEW
109
  async createReadOnlyRole(baseId: string) {
×
NEW
110
    const roleName = this.getReadOnlyRoleName(baseId);
×
NEW
111
    await this.prismaService
×
NEW
112
      .txClient()
×
NEW
113
      .$executeRawUnsafe(
×
NEW
114
        this.knex
×
NEW
115
          .raw(
×
NEW
116
            `CREATE ROLE ?? WITH NOLOGIN PASSWORD ? NOSUPERUSER NOINHERIT NOCREATEDB NOCREATEROLE NOREPLICATION`,
×
NEW
117
            [roleName, getRandomString(16).toLocaleLowerCase()]
×
118
          )
NEW
119
          .toQuery()
×
120
      );
NEW
121
    await this.prismaService
×
NEW
122
      .txClient()
×
NEW
123
      .$executeRawUnsafe(
×
NEW
124
        this.knex.raw(`GRANT USAGE ON SCHEMA ?? TO ??`, [baseId, roleName]).toQuery()
×
125
      );
NEW
126
    await this.prismaService
×
NEW
127
      .txClient()
×
NEW
128
      .$executeRawUnsafe(
×
NEW
129
        this.knex.raw(`GRANT SELECT ON ALL TABLES IN SCHEMA ?? TO ??`, [baseId, roleName]).toQuery()
×
130
      );
NEW
131
    await this.prismaService
×
NEW
132
      .txClient()
×
NEW
133
      .$executeRawUnsafe(
×
NEW
134
        this.knex
×
NEW
135
          .raw(`ALTER DEFAULT PRIVILEGES IN SCHEMA ?? GRANT SELECT ON TABLES TO ??`, [
×
NEW
136
            baseId,
×
NEW
137
            roleName,
×
NEW
138
          ])
×
NEW
139
          .toQuery()
×
140
      );
NEW
141
  }
×
142

NEW
143
  async dropReadOnlyRole(baseId: string) {
×
NEW
144
    const roleName = this.getReadOnlyRoleName(baseId);
×
NEW
145
    await this.prismaService
×
NEW
146
      .txClient()
×
NEW
147
      .$executeRawUnsafe(
×
NEW
148
        this.knex.raw(`REVOKE USAGE ON SCHEMA ?? FROM ??`, [baseId, roleName]).toQuery()
×
149
      );
NEW
150
    await this.prismaService
×
NEW
151
      .txClient()
×
NEW
152
      .$executeRawUnsafe(
×
NEW
153
        this.knex
×
NEW
154
          .raw(`REVOKE SELECT ON ALL TABLES IN SCHEMA ?? FROM ??`, [baseId, roleName])
×
NEW
155
          .toQuery()
×
156
      );
NEW
157
    await this.prismaService
×
NEW
158
      .txClient()
×
NEW
159
      .$executeRawUnsafe(
×
NEW
160
        this.knex
×
NEW
161
          .raw(`ALTER DEFAULT PRIVILEGES IN SCHEMA ?? REVOKE ALL ON TABLES FROM ??`, [
×
NEW
162
            baseId,
×
NEW
163
            roleName,
×
NEW
164
          ])
×
NEW
165
          .toQuery()
×
166
      );
NEW
167
    await this.prismaService
×
NEW
168
      .txClient()
×
NEW
169
      .$executeRawUnsafe(this.knex.raw(`DROP ROLE IF EXISTS ??`, [roleName]).toQuery());
×
NEW
170
  }
×
171

NEW
172
  async grantReadOnlyRole(baseId: string) {
×
NEW
173
    const roleName = this.getReadOnlyRoleName(baseId);
×
NEW
174
    await this.prismaService
×
NEW
175
      .txClient()
×
NEW
176
      .$executeRawUnsafe(
×
NEW
177
        this.knex.raw(`GRANT USAGE ON SCHEMA ?? TO ??`, [baseId, roleName]).toQuery()
×
178
      );
NEW
179
    await this.prismaService
×
NEW
180
      .txClient()
×
NEW
181
      .$executeRawUnsafe(
×
NEW
182
        this.knex.raw(`GRANT SELECT ON ALL TABLES IN SCHEMA ?? TO ??`, [baseId, roleName]).toQuery()
×
183
      );
NEW
184
    await this.prismaService
×
NEW
185
      .txClient()
×
NEW
186
      .$executeRawUnsafe(
×
NEW
187
        this.knex
×
NEW
188
          .raw(`ALTER DEFAULT PRIVILEGES IN SCHEMA ?? GRANT SELECT ON TABLES TO ??`, [
×
NEW
189
            baseId,
×
NEW
190
            roleName,
×
NEW
191
          ])
×
NEW
192
          .toQuery()
×
193
      );
NEW
194
  }
×
195

NEW
196
  private async roleExits(role: string): Promise<boolean> {
×
NEW
197
    const roleExists = await this.prismaService.$queryRaw<
×
198
      { count: bigint }[]
NEW
199
    >`SELECT count(*) FROM pg_roles WHERE rolname=${role}`;
×
NEW
200
    return Boolean(roleExists[0].count);
×
NEW
201
  }
×
202

NEW
203
  private async roleCheckAndCreate(baseId: string) {
×
NEW
204
    if (this.driver !== DriverClient.Pg) {
×
NEW
205
      return;
×
NEW
206
    }
×
NEW
207
    const roleName = this.getReadOnlyRoleName(baseId);
×
NEW
208
    if (!(await this.roleExits(roleName))) {
×
NEW
209
      await this.createReadOnlyRole(baseId);
×
NEW
210
    }
×
NEW
211
  }
×
212

NEW
213
  private async setRole(prisma: Prisma.TransactionClient, baseId: string) {
×
NEW
214
    const roleName = this.getReadOnlyRoleName(baseId);
×
NEW
215
    await prisma.$executeRawUnsafe(this.knex.raw(`SET ROLE ??`, [roleName]).toQuery());
×
NEW
216
  }
×
217

NEW
218
  private async resetRole(prisma: Prisma.TransactionClient) {
×
NEW
219
    await prisma.$executeRawUnsafe(this.knex.raw(`RESET ROLE`).toQuery());
×
NEW
220
  }
×
221

NEW
222
  private async readonlyExecuteSql(sql: string) {
×
NEW
223
    return this.db?.raw(sql);
×
NEW
224
  }
×
225

NEW
226
  /**
×
227
   * check sql is safe
228
   * 1. role operations validation
229
   * 2. parse sql to valid table names
230
   * 3. read only role check table access
NEW
231
   */
×
NEW
232
  private async safeCheckSql(
×
NEW
233
    baseId: string,
×
NEW
234
    sql: string,
×
NEW
235
    opts?: { projectionTableDbNames?: string[]; projectionTableIds?: string[] }
×
NEW
236
  ) {
×
NEW
237
    const { projectionTableDbNames = [] } = opts ?? {};
×
NEW
238
    // 1. role operations keywords validation, only pg support
×
NEW
239
    if (this.driver == DriverClient.Pg) {
×
NEW
240
      validateRoleOperations(sql);
×
NEW
241
    }
×
NEW
242
    let tableNames = projectionTableDbNames;
×
NEW
243
    if (!projectionTableDbNames.length) {
×
NEW
244
      const tables = await this.prismaService.tableMeta.findMany({
×
NEW
245
        where: {
×
NEW
246
          baseId,
×
NEW
247
        },
×
NEW
248
        select: {
×
NEW
249
          dbTableName: true,
×
NEW
250
        },
×
NEW
251
      });
×
NEW
252
      tableNames = tables.map((table) => table.dbTableName);
×
NEW
253
    }
×
NEW
254
    // 2. parse sql to valid table names
×
NEW
255
    checkTableAccess(sql, {
×
NEW
256
      tableNames,
×
NEW
257
      database: this.driver,
×
NEW
258
    });
×
NEW
259
    // 3. read only role check table access, only pg and pg version > 14 support
×
NEW
260
    await this.readonlyExecuteSql(sql);
×
NEW
261
  }
×
262

NEW
263
  async executeQuerySql<T = unknown>(
×
NEW
264
    baseId: string,
×
NEW
265
    sql: string,
×
NEW
266
    opts?: {
×
267
      projectionTableDbNames?: string[];
268
      projectionTableIds?: string[];
NEW
269
    }
×
NEW
270
  ) {
×
NEW
271
    await this.safeCheckSql(baseId, sql, opts);
×
NEW
272
    await this.roleCheckAndCreate(baseId);
×
NEW
273
    return this.prismaService.$tx(async (prisma) => {
×
NEW
274
      try {
×
NEW
275
        await this.setRole(prisma, baseId);
×
NEW
276
        return await prisma.$queryRawUnsafe<T>(sql);
×
NEW
277
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
×
NEW
278
      } catch (error: any) {
×
NEW
279
        throw new BadRequestException(error?.meta?.message || error?.message);
×
NEW
280
      } finally {
×
NEW
281
        await this.resetRole(prisma).catch((error) => {
×
NEW
282
          console.log('resetRole error', error);
×
NEW
283
        });
×
NEW
284
      }
×
NEW
285
    });
×
NEW
286
  }
×
NEW
287
}
×
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