Skip to content

Commit 83a413a

Browse files
tea-artistteable-bot
andauthored
[sync] Merge pull request #1143 from teableio/fix/T1844 (#2509)
Synced from teableio/teable-ee@fc680a5 Co-authored-by: teable-bot <bot@teable.io>
1 parent e49f3ee commit 83a413a

101 files changed

Lines changed: 2938 additions & 1177 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/src/features/base/base.service.ts

Lines changed: 4 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,8 @@ import {
2626
BaseNodeResourceType,
2727
BaseDuplicateMode,
2828
UploadType,
29-
PrincipalType,
3029
} from '@teable/openapi';
31-
import { keyBy, isNumber, pick, uniq } from 'lodash';
30+
import { isNumber, keyBy, pick, uniq } from 'lodash';
3231
import { ClsService } from 'nestjs-cls';
3332
import { IThresholdConfig, ThresholdConfig } from '../../configs/threshold.config';
3433
import { CustomHttpException } from '../../custom.exception';
@@ -164,29 +163,15 @@ export class BaseService {
164163
}
165164

166165
const baseSpaceIds = uniq(baseList.map((base) => base.spaceId));
167-
const spaceCollaborators = await this.prismaService.collaborator.findMany({
168-
where: {
169-
resourceType: CollaboratorType.Space,
170-
resourceId: { in: baseSpaceIds },
171-
principalType: PrincipalType.User,
172-
},
173-
select: { resourceId: true, principalId: true, roleName: true },
174-
});
166+
const { validCreatorSet, spaceOwnerMap } =
167+
await this.collaboratorService.buildSpaceOwnerContext(baseSpaceIds);
175168

176-
const validCreatorSet = new Set(
177-
spaceCollaborators.map((c) => `${c.resourceId}:${c.principalId}`)
178-
);
179-
const spaceOwnerMap = new Map(
180-
spaceCollaborators
181-
.filter((c) => c.roleName === Role.Owner)
182-
.map((c) => [c.resourceId, c.principalId])
183-
);
184169
const allUserIds = uniq([...baseList.map((base) => base.createdBy), ...spaceOwnerMap.values()]);
185-
186170
const userList = await this.prismaService.user.findMany({
187171
where: { id: { in: allUserIds } },
188172
select: { id: true, name: true, avatar: true },
189173
});
174+
190175
const userMap = keyBy(userList, 'id');
191176

192177
return baseList.map((base) => {

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

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1015,4 +1015,38 @@ export class CollaboratorService {
10151015
avatar: item.avatar ? getPublicFullStorageUrl(item.avatar) : null,
10161016
}));
10171017
}
1018+
1019+
/**
1020+
* Build space owner context for determining display user
1021+
* When the creator is no longer in the space, falls back to space owner
1022+
*/
1023+
async buildSpaceOwnerContext(spaceIds: string[]): Promise<{
1024+
validCreatorSet: Set<string>;
1025+
spaceOwnerMap: Map<string, string>;
1026+
}> {
1027+
if (!spaceIds.length) {
1028+
return { validCreatorSet: new Set(), spaceOwnerMap: new Map() };
1029+
}
1030+
1031+
const spaceCollaborators = await this.prismaService.collaborator.findMany({
1032+
where: {
1033+
resourceType: CollaboratorType.Space,
1034+
resourceId: { in: spaceIds },
1035+
principalType: PrincipalType.User,
1036+
},
1037+
select: { resourceId: true, principalId: true, roleName: true },
1038+
});
1039+
1040+
const validCreatorSet = new Set(
1041+
spaceCollaborators.map((c) => `${c.resourceId}:${c.principalId}`)
1042+
);
1043+
1044+
const spaceOwnerMap = new Map(
1045+
spaceCollaborators
1046+
.filter((c) => c.roleName === Role.Owner)
1047+
.map((c) => [c.resourceId, c.principalId])
1048+
);
1049+
1050+
return { validCreatorSet, spaceOwnerMap };
1051+
}
10181052
}

apps/nestjs-backend/src/features/plugin/official/official-plugin-init.service.ts

Lines changed: 100 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,25 @@ import { chartConfig } from './config/chart';
1717
import { sheetFormConfig } from './config/sheet-form-view';
1818
import type { IOfficialPluginConfig } from './config/types';
1919

20+
interface IUploadResult {
21+
id: string;
22+
path: string;
23+
url: string;
24+
size: number;
25+
width?: number;
26+
height?: number;
27+
hash: string;
28+
mimetype: string;
29+
}
30+
31+
interface IPreparedPlugin {
32+
config: IOfficialPluginConfig & { secret: string; url: string };
33+
logo: IUploadResult;
34+
avatar?: IUploadResult;
35+
hashedSecret: string;
36+
maskedSecret: string;
37+
}
38+
2039
@Injectable()
2140
export class OfficialPluginInitService implements OnModuleInit {
2241
private logger = new Logger(OfficialPluginInitService.name);
@@ -30,7 +49,6 @@ export class OfficialPluginInitService implements OnModuleInit {
3049
@InjectModel('CUSTOM_KNEX') private readonly knex: Knex
3150
) {}
3251

33-
// init official plugins
3452
async onModuleInit() {
3553
const officialPlugins = [
3654
{
@@ -48,10 +66,18 @@ export class OfficialPluginInitService implements OnModuleInit {
4866
];
4967

5068
try {
69+
// Phase 1: Upload files to storage (outside transaction)
70+
const preparedPlugins: IPreparedPlugin[] = [];
71+
for (const plugin of officialPlugins) {
72+
this.logger.log(`Creating official plugin: ${plugin.name}`);
73+
const prepared = await this.preparePlugin(plugin);
74+
preparedPlugins.push(prepared);
75+
}
76+
77+
// Phase 2: Database operations (inside transaction)
5178
await this.prismaService.$tx(async () => {
52-
for (const plugin of officialPlugins) {
53-
this.logger.log(`Creating official plugin: ${plugin.name}`);
54-
await this.createOfficialPlugin(plugin);
79+
for (const prepared of preparedPlugins) {
80+
await this.savePlugin(prepared);
5581
}
5682
});
5783
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -63,21 +89,33 @@ export class OfficialPluginInitService implements OnModuleInit {
6389
this.logger.log('Official plugins initialized');
6490
}
6591

66-
async uploadStatic(id: string, filePath: string, type: UploadType) {
92+
private async uploadToStorage(
93+
id: string,
94+
filePath: string,
95+
type: UploadType
96+
): Promise<IUploadResult> {
97+
const path = join(StorageAdapter.getDir(type), id);
98+
6799
if (process.env.NODE_ENV === 'test') {
68-
return `/${join(StorageAdapter.getDir(type), id)}`;
100+
return { id, path, url: `/${path}`, size: 0, hash: '', mimetype: 'image/png' };
69101
}
102+
70103
const fileStream = createReadStream(resolve(process.cwd(), filePath));
71104
const metaReader = sharp();
72105
const sharpReader = fileStream.pipe(metaReader);
73106
const { width, height, format = 'png', size = 0 } = await sharpReader.metadata();
74-
const path = join(StorageAdapter.getDir(type), id);
75107
const bucket = StorageAdapter.getBucket(type);
76108
const mimetype = `image/${format}`;
77109
const { hash } = await this.storageAdapter.uploadFileWidthPath(bucket, path, filePath, {
78110
// eslint-disable-next-line @typescript-eslint/naming-convention
79111
'Content-Type': mimetype,
80112
});
113+
114+
return { id, path, url: `/${path}`, size, width, height, hash, mimetype };
115+
}
116+
117+
private async saveAttachment(upload: IUploadResult): Promise<void> {
118+
const { id, path, size, width, height, hash, mimetype } = upload;
81119
await this.prismaService.txClient().attachments.upsert({
82120
create: {
83121
token: id,
@@ -102,106 +140,91 @@ export class OfficialPluginInitService implements OnModuleInit {
102140
deletedTime: null,
103141
},
104142
});
105-
return `/${path}`;
106143
}
107144

108-
async createOfficialPlugin(
145+
private async preparePlugin(
109146
pluginConfig: IOfficialPluginConfig & { secret: string; url: string }
110-
) {
147+
): Promise<IPreparedPlugin> {
148+
const { id: pluginId, logoPath, avatarPath, pluginUserId, secret } = pluginConfig;
149+
150+
const logo = await this.uploadToStorage(pluginId, logoPath, UploadType.Plugin);
151+
const { hashedSecret, maskedSecret } = await generateSecret(secret);
152+
153+
let avatar: IUploadResult | undefined;
154+
if (pluginUserId && avatarPath) {
155+
avatar = await this.uploadToStorage(pluginUserId, avatarPath, UploadType.Avatar);
156+
}
157+
158+
return { config: pluginConfig, logo, avatar, hashedSecret, maskedSecret };
159+
}
160+
161+
private async savePlugin(prepared: IPreparedPlugin): Promise<void> {
162+
const { config, logo, avatar, hashedSecret, maskedSecret } = prepared;
111163
const {
112164
id: pluginId,
113165
name,
114166
description,
115167
detailDesc,
116-
logoPath,
117168
i18n,
118169
positions,
119170
helpUrl,
120-
secret,
121171
url,
122172
pluginUserId,
123-
avatarPath,
124-
} = pluginConfig;
173+
} = config;
125174

126-
const rows = await this.prismaService.txClient().plugin.count({ where: { id: pluginId } });
127-
// upload logo
128-
const logo = await this.uploadStatic(pluginId, logoPath, UploadType.Plugin);
129-
const { hashedSecret, maskedSecret } = await generateSecret(secret);
175+
// Save attachments
176+
await this.saveAttachment(logo);
177+
if (avatar) {
178+
await this.saveAttachment(avatar);
179+
}
180+
181+
// Create plugin user if needed
130182
let userId: string | undefined;
131183
if (pluginUserId) {
132184
const userEmail = getPluginEmail(pluginId);
133-
// create plugin user
134185
const user = await this.prismaService
135186
.txClient()
136187
.user.findFirst({ where: { id: pluginUserId, email: userEmail } });
137-
let avatar: string | undefined;
138-
if (avatarPath) {
139-
// upload user avatar
140-
avatar = await this.uploadStatic(pluginUserId, avatarPath, UploadType.Avatar);
141-
}
188+
142189
if (!user) {
143190
await this.userService.createSystemUser({
144191
id: pluginUserId,
145192
name,
146-
avatar,
193+
avatar: avatar?.url,
147194
email: userEmail,
148195
});
149196
}
150197
userId = pluginUserId;
151198
}
152-
if (rows > 0) {
153-
return this.prismaService.txClient().plugin.update({
154-
where: {
155-
id: pluginId,
156-
},
157-
data: {
158-
name,
159-
description,
160-
detailDesc,
161-
positions: JSON.stringify(positions),
162-
helpUrl,
163-
url,
164-
logo,
165-
status: PluginStatus.Published,
166-
i18n: JSON.stringify(i18n),
167-
secret: hashedSecret,
168-
maskedSecret,
169-
pluginUser: userId || pluginUserId,
170-
createdBy: 'system',
171-
},
199+
200+
// Create or update plugin
201+
const pluginData = {
202+
name,
203+
description,
204+
detailDesc,
205+
positions: JSON.stringify(positions),
206+
helpUrl,
207+
url,
208+
logo: logo.url,
209+
status: PluginStatus.Published,
210+
i18n: JSON.stringify(i18n),
211+
secret: hashedSecret,
212+
maskedSecret,
213+
pluginUser: userId || pluginUserId,
214+
createdBy: 'system',
215+
};
216+
217+
const exists = await this.prismaService.txClient().plugin.count({ where: { id: pluginId } });
218+
219+
if (exists > 0) {
220+
await this.prismaService.txClient().plugin.update({
221+
where: { id: pluginId },
222+
data: pluginData,
223+
});
224+
} else {
225+
await this.prismaService.txClient().plugin.create({
226+
data: { id: pluginId, ...pluginData },
172227
});
173228
}
174-
return this.prismaService.txClient().plugin.create({
175-
select: {
176-
id: true,
177-
name: true,
178-
description: true,
179-
detailDesc: true,
180-
positions: true,
181-
helpUrl: true,
182-
logo: true,
183-
url: true,
184-
status: true,
185-
i18n: true,
186-
secret: true,
187-
createdTime: true,
188-
},
189-
data: {
190-
id: pluginId,
191-
name,
192-
description,
193-
detailDesc,
194-
positions: JSON.stringify(positions),
195-
helpUrl,
196-
url,
197-
logo,
198-
status: PluginStatus.Published,
199-
i18n: JSON.stringify(i18n),
200-
secret: hashedSecret,
201-
maskedSecret,
202-
pluginUser: userId || pluginUserId,
203-
createdBy: 'system',
204-
},
205-
});
206229
}
207230
}

apps/nestjs-backend/src/features/space/space.controller.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import type {
1212
ListSpaceCollaboratorVo,
1313
IGetBaseAllVo,
1414
ITestLLMVo,
15+
ISpaceSearchVo,
1516
} from '@teable/openapi';
1617
import {
1718
createSpaceRoSchema,
@@ -39,6 +40,8 @@ import {
3940
IUpdateIntegrationRo,
4041
testLLMRoSchema,
4142
ITestLLMRo,
43+
spaceSearchRoSchema,
44+
ISpaceSearchRo,
4245
} from '@teable/openapi';
4346
import { CustomHttpException } from '../../custom.exception';
4447
import { EmitControllerEvent } from '../../event-emitter/decorators/emit-controller-event.decorator';
@@ -130,6 +133,15 @@ export class SpaceController {
130133
return await this.spaceService.getBaseListBySpaceId(spaceId);
131134
}
132135

136+
@Permissions('space|read')
137+
@Get(':spaceId/search')
138+
async search(
139+
@Param('spaceId') spaceId: string,
140+
@Query(new ZodValidationPipe(spaceSearchRoSchema)) query: ISpaceSearchRo
141+
): Promise<ISpaceSearchVo> {
142+
return await this.spaceService.search(spaceId, query);
143+
}
144+
133145
@Permissions('space|invite_link')
134146
@Patch(':spaceId/invitation/link/:invitationId')
135147
async updateInvitationLink(

0 commit comments

Comments
 (0)