Skip to content

Commit 46dda60

Browse files
github-actions[bot]caoxing9boris-wJocky-Teablehammond-lj
committed
[sync] feat: grid selection statistic chip (T3813)
Synced from teableio/teable-ee@a996401 Co-authored-by: Aries X <caoxing9@gmail.com> Co-authored-by: Boris <boris2code@outlook.com> Co-authored-by: Jocky-Teable <jocky@teable.ai> Co-authored-by: Jun Lu <hammond@teable.io> Co-authored-by: SkyHuang <sky.huang.fe@gmail.com> Co-authored-by: Uno <uno@teable.ai> Co-authored-by: nichenqin <nichenqin@hotmail.com>
1 parent bda82ee commit 46dda60

109 files changed

Lines changed: 3485 additions & 292 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/nestjs-backend/scripts/validate-dual-db-cutover.mjs

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -162,16 +162,24 @@ const main = async () => {
162162
]);
163163

164164
const schemaDiff = diffSets(sourceSchemas, targetSchemas);
165-
const schemaTableCountDiffs = compareCountMaps(sourceSchemaTableCounts, targetSchemaTableCounts);
165+
const schemaTableCountDiffs = compareCountMaps(
166+
sourceSchemaTableCounts,
167+
targetSchemaTableCounts
168+
);
166169

167-
const [sourceDataCounts, targetDataCounts, sourceMetaCounts, targetMetaCounts, undoFunctionExists] =
168-
await Promise.all([
169-
getTableCounts(sourceClient, DATA_PLANE_TABLES),
170-
getTableCounts(dataClient, DATA_PLANE_TABLES),
171-
getTableCounts(sourceClient, META_PLANE_TABLES),
172-
getTableCounts(metaClient, META_PLANE_TABLES),
173-
getFunctionExists(dataClient, '__teable_capture_undo_row'),
174-
]);
170+
const [
171+
sourceDataCounts,
172+
targetDataCounts,
173+
sourceMetaCounts,
174+
targetMetaCounts,
175+
undoFunctionExists,
176+
] = await Promise.all([
177+
getTableCounts(sourceClient, DATA_PLANE_TABLES),
178+
getTableCounts(dataClient, DATA_PLANE_TABLES),
179+
getTableCounts(sourceClient, META_PLANE_TABLES),
180+
getTableCounts(metaClient, META_PLANE_TABLES),
181+
getFunctionExists(dataClient, '__teable_capture_undo_row'),
182+
]);
175183

176184
const dataCountDiffs = compareCountMaps(sourceDataCounts, targetDataCounts);
177185
const metaCountDiffs = compareCountMaps(sourceMetaCounts, targetMetaCounts);

apps/nestjs-backend/src/configs/threshold.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ export const thresholdConfig = registerAs('threshold', () => ({
3636
jitter: Number(process.env.BACKEND_DB_DEADLOCK_JITTER ?? 1.0),
3737
},
3838
baseNodeMaxFolderDepth: Number(process.env.BASE_NODE_MAX_FOLDER_DEPTH ?? 2),
39+
maxOwnedSpaceCount: Number(process.env.MAX_SPACE_OWNER_COUNT ?? 10),
3940
changeEmailSendCodeMailRate: Number(process.env.BACKEND_CHANGE_EMAIL_SEND_CODE_MAIL_RATE ?? 30),
4041
resetPasswordSendMailRate: Number(process.env.BACKEND_RESET_PASSWORD_SEND_MAIL_RATE ?? 30),
4142
signupVerificationSendCodeMailRate: Number(

apps/nestjs-backend/src/features/base/base.service.spec.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ describe('BaseService', () => {
5757
return {
5858
service: new BaseService(
5959
prismaService as never,
60+
{} as never,
61+
{} as never,
6062
cls as never,
6163
{} as never,
6264
{} as never,

apps/nestjs-backend/src/features/base/base.service.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ import {
77
Role,
88
generateTemplateId,
99
} from '@teable/core';
10-
import { PrismaService, ProvisionState } from '@teable/db-main-prisma';
1110
import { DataPrismaService } from '@teable/db-data-prisma';
11+
import { PrismaService, ProvisionState } from '@teable/db-main-prisma';
1212
import type {
1313
IBaseErdVo,
1414
ICreateBaseFromTemplateRo,
@@ -34,6 +34,7 @@ import { IThresholdConfig, ThresholdConfig } from '../../configs/threshold.confi
3434
import { CustomHttpException } from '../../custom.exception';
3535
import { InjectDbProvider } from '../../db-provider/db.provider';
3636
import { IDbProvider } from '../../db-provider/db.provider.interface';
37+
import { DataDbClientManager } from '../../global/data-db-client-manager.service';
3738
import type { IClsStore } from '../../types/cls';
3839
import { getMaxLevelRole } from '../../utils/get-max-level-role';
3940
import { updateOrder } from '../../utils/update-order';
@@ -56,6 +57,7 @@ export class BaseService {
5657
constructor(
5758
private readonly prismaService: PrismaService,
5859
private readonly dataPrismaService: DataPrismaService,
60+
private readonly dataDbClientManager: DataDbClientManager,
5961
private readonly cls: ClsService<IClsStore>,
6062
private readonly collaboratorService: CollaboratorService,
6163
private readonly baseDuplicateService: BaseDuplicateService,
@@ -250,8 +252,9 @@ export class BaseService {
250252
try {
251253
const sqlList = this.dbProvider.createSchema(base.id);
252254
if (sqlList) {
255+
const dataPrisma = await this.dataDbClientManager.dataPrismaForSpace(spaceId);
253256
for (const sql of sqlList) {
254-
await this.dataPrismaService.$executeRawUnsafe(sql);
257+
await dataPrisma.$executeRawUnsafe(sql);
255258
}
256259
}
257260

apps/nestjs-backend/src/features/collaborator/collaborator.service.ts

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { Knex } from 'knex';
2222
import { difference, keyBy, map } from 'lodash';
2323
import { InjectModel } from 'nest-knexjs';
2424
import { ClsService } from 'nestjs-cls';
25+
import { ThresholdConfig, IThresholdConfig } from '../../configs/threshold.config';
2526
import { CustomHttpException } from '../../custom.exception';
2627
import { InjectDbProvider } from '../../db-provider/db.provider';
2728
import { IDbProvider } from '../../db-provider/db.provider.interface';
@@ -43,7 +44,8 @@ export class CollaboratorService {
4344
private readonly cls: ClsService<IClsStore>,
4445
private readonly eventEmitterService: EventEmitterService,
4546
@InjectModel('CUSTOM_KNEX') private readonly knex: Knex,
46-
@InjectDbProvider() private readonly dbProvider: IDbProvider
47+
@InjectDbProvider() private readonly dbProvider: IDbProvider,
48+
@ThresholdConfig() private readonly thresholdConfig: IThresholdConfig
4749
) {}
4850

4951
async createSpaceCollaborator({
@@ -82,6 +84,20 @@ export class CollaboratorService {
8284
}
8385
);
8486
}
87+
if (role === Role.Owner) {
88+
const userIds = collaborators
89+
.filter((c) => c.principalType === PrincipalType.User)
90+
.map((c) => c.principalId);
91+
if (userIds.length > 0) {
92+
const countMap = await this.countUserOwnedSpaces(userIds);
93+
for (const uid of userIds) {
94+
await this.validateOwnedSpaceLimit(
95+
countMap.get(uid) ?? 0,
96+
uid !== currentUserId ? uid : undefined
97+
);
98+
}
99+
}
100+
}
85101
// if has exist base collaborator, then delete it
86102
const bases = await this.prismaService.txClient().base.findMany({
87103
where: {
@@ -548,6 +564,61 @@ export class CollaboratorService {
548564
return collaborators.length === 1 && collaborators[0].principal_id === userId;
549565
}
550566

567+
async countUserOwnedSpaces(userId: string): Promise<number>;
568+
async countUserOwnedSpaces(userIds: string[]): Promise<Map<string, number>>;
569+
async countUserOwnedSpaces(
570+
userIdOrIds: string | string[]
571+
): Promise<number | Map<string, number>> {
572+
const isSingle = typeof userIdOrIds === 'string';
573+
const userIds = isSingle ? [userIdOrIds] : userIdOrIds;
574+
if (userIds.length === 0) return isSingle ? 0 : new Map();
575+
const builder = this.knex('collaborator')
576+
.join('space', 'collaborator.resource_id', 'space.id')
577+
.whereIn('collaborator.principal_id', userIds)
578+
.where('collaborator.principal_type', PrincipalType.User)
579+
.where('collaborator.resource_type', CollaboratorType.Space)
580+
.where('collaborator.role_name', Role.Owner)
581+
.whereNull('space.deleted_time')
582+
.groupBy('collaborator.principal_id')
583+
.select('collaborator.principal_id as user_id')
584+
.count('* as count');
585+
const result = await this.prismaService
586+
.txClient()
587+
.$queryRawUnsafe<{ user_id: string; count: number }[]>(builder.toQuery());
588+
if (isSingle) {
589+
return Number(result[0]?.count ?? 0);
590+
}
591+
const countMap = new Map<string, number>();
592+
for (const row of result) {
593+
countMap.set(row.user_id, Number(row.count));
594+
}
595+
return countMap;
596+
}
597+
598+
async validateOwnedSpaceLimit(currentCount: number, userId?: string): Promise<void> {
599+
const maxCount = this.thresholdConfig.maxOwnedSpaceCount;
600+
if (maxCount <= 0 || currentCount < maxCount) return;
601+
602+
const userName = userId
603+
? await this.prismaService.user
604+
.findUnique({ where: { id: userId }, select: { name: true, email: true } })
605+
.then((user) => (user ? `${user.name} (${user.email})` : undefined))
606+
: undefined;
607+
608+
throw new CustomHttpException(
609+
`Owned space limit exceeded, max: ${maxCount}${userName ? `, user: ${userName}` : ''}`,
610+
HttpErrorCode.VALIDATION_ERROR,
611+
{
612+
localization: {
613+
i18nKey: userId
614+
? 'httpErrors.space.ownedSpaceLimitExceededOther'
615+
: 'httpErrors.space.ownedSpaceLimitExceeded',
616+
context: userId ? { max: maxCount, name: userName } : { max: maxCount },
617+
},
618+
}
619+
);
620+
}
621+
551622
async deleteCollaborator({
552623
resourceId,
553624
resourceType,
@@ -659,6 +730,19 @@ export class CollaboratorService {
659730
);
660731
}
661732

733+
if (
734+
role === Role.Owner &&
735+
resourceType === CollaboratorType.Space &&
736+
targetColl.roleName !== Role.Owner &&
737+
principalType === PrincipalType.User
738+
) {
739+
const count = await this.countUserOwnedSpaces(principalId);
740+
await this.validateOwnedSpaceLimit(
741+
count,
742+
principalId !== currentUserId ? principalId : undefined
743+
);
744+
}
745+
662746
const res = await this.prismaService.txClient().collaborator.updateMany({
663747
where: {
664748
resourceId: resourceId,

apps/nestjs-backend/src/features/integrity/foreign-key.service.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,9 @@ export class ForeignKeyIntegrityService {
8888
.toQuery();
8989

9090
try {
91-
const invalidRefs =
92-
await this.dataPrismaService.txClient().$queryRawUnsafe<{ count: bigint }[]>(invalidQuery);
91+
const invalidRefs = await this.dataPrismaService
92+
.txClient()
93+
.$queryRawUnsafe<{ count: bigint }[]>(invalidQuery);
9394
const refCount = Number(invalidRefs[0]?.count || 0);
9495

9596
if (refCount > 0) {

apps/nestjs-backend/src/features/integrity/link-integrity.service.ts

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -348,10 +348,9 @@ export class LinkIntegrityService {
348348

349349
let canCheckLinks = false;
350350
const tableExistsSql = this.dbProvider.checkTableExist(options.fkHostTableName);
351-
const tableExists =
352-
await this.dataPrismaService.txClient().$queryRawUnsafe<{ exists: boolean }[]>(
353-
tableExistsSql
354-
);
351+
const tableExists = await this.dataPrismaService
352+
.txClient()
353+
.$queryRawUnsafe<{ exists: boolean }[]>(tableExistsSql);
355354
const hostTableExists = tableExists[0].exists;
356355

357356
if (!hostTableExists) {
@@ -538,11 +537,7 @@ export class LinkIntegrityService {
538537

539538
if (hostAlreadyExists) {
540539
const [selfKeyExists, foreignKeyExists, orderColumnExists] = await Promise.all([
541-
this.dbProvider.checkColumnExist(
542-
options.fkHostTableName,
543-
options.selfKeyName,
544-
dataPrisma
545-
),
540+
this.dbProvider.checkColumnExist(options.fkHostTableName, options.selfKeyName, dataPrisma),
546541
this.dbProvider.checkColumnExist(
547542
options.fkHostTableName,
548543
options.foreignKeyName,

apps/nestjs-backend/src/features/oauth/oauth.service.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -333,7 +333,11 @@ export class OAuthService {
333333
homepage: c.homepage,
334334
scopes: c.scopes,
335335
lastUsedTime: lastUsedTimeMap[c.clientId]?.lastUsedTime,
336-
createdUser: userMap[c.createdBy],
336+
createdUser:
337+
userMap[c.createdBy] ??
338+
(c.createdBy === 'system'
339+
? { name: 'System', email: 'system@teable.ai' }
340+
: { name: 'Unknown', email: '' }),
337341
})
338342
);
339343
}

apps/nestjs-backend/src/features/record/computed/services/computed-dependency-collector.service.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -152,9 +152,9 @@ export class ComputedDependencyCollectorService {
152152
): Promise<string[]> {
153153
const dbTable = await this.getDbTableName(tableId, ctx);
154154
const { schema, table } = this.splitDbTableName(dbTable);
155-
const qb = (schema ? this.dataKnex.withSchema(schema) : this.dataKnex).select('__id').from(
156-
table
157-
);
155+
const qb = (schema ? this.dataKnex.withSchema(schema) : this.dataKnex)
156+
.select('__id')
157+
.from(table);
158158
const rows = await this.dataPrismaService
159159
.txClient()
160160
.$queryRawUnsafe<Array<{ __id: string }>>(qb.toQuery());

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

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2226,10 +2226,9 @@ export class RecordService {
22262226

22272227
this.logger.debug('getSearchHitIndex query: %s', searchQuery);
22282228

2229-
const result =
2230-
await this.dataPrismaService
2231-
.txClient()
2232-
.$queryRawUnsafe<{ __id: string; fieldId: string }[]>(searchQuery);
2229+
const result = await this.dataPrismaService
2230+
.txClient()
2231+
.$queryRawUnsafe<{ __id: string; fieldId: string }[]>(searchQuery);
22332232

22342233
if (!result.length) {
22352234
return null;
@@ -2600,7 +2599,9 @@ export class RecordService {
26002599
const rowCountSql = qb.count({ count: '*' });
26012600
const sql = rowCountSql.toQuery();
26022601
this.logger.debug('getRowCountSql: %s', sql);
2603-
const result = await this.dataPrismaService.txClient().$queryRawUnsafe<{ count?: number }[]>(sql);
2602+
const result = await this.dataPrismaService
2603+
.txClient()
2604+
.$queryRawUnsafe<{ count?: number }[]>(sql);
26042605
return Number(result[0].count);
26052606
}
26062607

@@ -2710,10 +2711,9 @@ export class RecordService {
27102711
);
27112712

27122713
try {
2713-
const result =
2714-
await this.dataPrismaService.txClient().$queryRawUnsafe<{ [key: string]: unknown; __c: number }[]>(
2715-
groupSql
2716-
);
2714+
const result = await this.dataPrismaService
2715+
.txClient()
2716+
.$queryRawUnsafe<{ [key: string]: unknown; __c: number }[]>(groupSql);
27172717
const pointsResult = await this.groupDbCollection2GroupPoints(
27182718
result,
27192719
groupFields,

0 commit comments

Comments
 (0)