Skip to content

Commit d8ff726

Browse files
authored
Merge pull request #216 from import-ai/refactor/namespaces
fix(namespaces): fix member count
2 parents b80a01c + 210769d commit d8ff726

File tree

5 files changed

+48
-18
lines changed

5 files changed

+48
-18
lines changed

src/i18n/en/namespace.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"workspaceNotFound": "Workspace not found",
66
"rootResourceNotFound": "Root resource not found",
77
"namespaceConflict": "Namespace conflict",
8-
"userNotOwner": "Current user is not owner of this namespace"
8+
"userNotOwner": "Current user is not owner of this namespace",
9+
"noOwnerAfterwards": "The namespace requires at least one owner"
910
}
1011
}

src/i18n/zh/namespace.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"workspaceNotFound": "工作空间未找到",
66
"rootResourceNotFound": "根资源未找到",
77
"namespaceConflict": "命名空间冲突",
8-
"userNotOwner": "当前用户不是该命名空间的所有者"
8+
"userNotOwner": "当前用户不是该命名空间的所有者",
9+
"noOwnerAfterwards": "需要保留至少一个所有者"
910
}
1011
}

src/namespaces/namespaces.controller.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,12 @@ export class NamespacesController {
3535
return await this.namespacesService.listMembers(namespaceId);
3636
}
3737

38+
@Get(':namespaceId/members/count')
39+
async countMembers(@Param('namespaceId') namespaceId: string) {
40+
const count = await this.namespacesService.countMembers(namespaceId);
41+
return { count };
42+
}
43+
3844
@NamespaceOwner()
3945
@Get(':namespaceId/members/:userId')
4046
async getMemberByUserId(

src/namespaces/namespaces.e2e-spec.ts

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -204,19 +204,13 @@ describe('NamespacesController (e2e)', () => {
204204

205205
const tempNamespaceId = tempNamespace.body.id;
206206

207-
// Test updating role (this will test the endpoint but may fail due to business logic)
208-
// The actual implementation might prevent changing the last owner's role
207+
// Should fail because user is the only owner in the namespace.
209208
await client
210209
.patch(
211210
`/api/v1/namespaces/${tempNamespaceId}/members/${client.user.id}`,
212211
)
213212
.send({ role: NamespaceRole.MEMBER })
214-
.expect(HttpStatus.OK);
215-
216-
// Members are not allowed to delete the namespace.
217-
await client
218-
.delete(`/api/v1/namespaces/${tempNamespaceId}`)
219-
.expect(HttpStatus.FORBIDDEN);
213+
.expect(HttpStatus.UNPROCESSABLE_ENTITY);
220214
});
221215

222216
it('should fail for non-existent member', async () => {
@@ -256,17 +250,16 @@ describe('NamespacesController (e2e)', () => {
256250

257251
const tempNamespaceId = tempNamespace.body.id;
258252

259-
// For this test, we'll try to remove the owner (should work but may have business logic constraints)
253+
// Should fail because user is the only owner in the namespace.
260254
await client
261255
.delete(
262256
`/api/v1/namespaces/${tempNamespaceId}/members/${client.user.id}`,
263257
)
264-
.expect(HttpStatus.OK);
258+
.expect(HttpStatus.UNPROCESSABLE_ENTITY);
265259

266-
// Verify member was removed by checking if they can still access the namespace
267260
await client
268261
.delete(`/api/v1/namespaces/${tempNamespaceId}`)
269-
.expect(HttpStatus.FORBIDDEN);
262+
.expect(HttpStatus.OK);
270263
});
271264
});
272265

src/namespaces/namespaces.service.ts

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,16 @@ export class NamespacesService {
3939
private readonly i18n: I18nService,
4040
) {}
4141

42+
private async hasOwner(
43+
namespaceId: string,
44+
entityManager: EntityManager,
45+
): Promise<boolean> {
46+
const count = await entityManager.count(NamespaceMember, {
47+
where: { namespaceId, role: NamespaceRole.OWNER },
48+
});
49+
return count > 0;
50+
}
51+
4252
async getPrivateRootId(userId: string, namespaceId: string): Promise<string> {
4353
const member = await this.namespaceMemberRepository.findOne({
4454
where: {
@@ -307,10 +317,17 @@ export class NamespacesService {
307317
userId: string,
308318
role: NamespaceRole,
309319
) {
310-
await this.namespaceMemberRepository.update(
311-
{ namespaceId, userId },
312-
{ role },
313-
);
320+
await this.dataSource.transaction(async (manager) => {
321+
await manager.update(NamespaceMember, { namespaceId, userId }, { role });
322+
const hasOwner = await this.hasOwner(namespaceId, manager);
323+
if (!hasOwner) {
324+
throw new AppException(
325+
this.i18n.t('namespace.errors.noOwnerAfterwards'),
326+
'NO_OWNER_AFTERWARDS',
327+
HttpStatus.UNPROCESSABLE_ENTITY,
328+
);
329+
}
330+
});
314331
}
315332

316333
async listNamespaces(userId: string): Promise<Namespace[]> {
@@ -329,6 +346,10 @@ export class NamespacesService {
329346
});
330347
}
331348

349+
async countMembers(namespaceId: string): Promise<number> {
350+
return await this.namespaceMemberRepository.countBy({ namespaceId });
351+
}
352+
332353
async listMembers(namespaceId: string): Promise<NamespaceMemberDto[]> {
333354
const members = await this.namespaceMemberRepository.find({
334355
where: { namespaceId },
@@ -399,6 +420,14 @@ export class NamespacesService {
399420
});
400421
// Delete namespace member record
401422
await manager.softDelete(NamespaceMember, { id: member.id });
423+
const hasOwner = await this.hasOwner(namespaceId, manager);
424+
if (!hasOwner) {
425+
throw new AppException(
426+
this.i18n.t('namespace.errors.noOwnerAfterwards'),
427+
'NO_OWNER_AFTERWARDS',
428+
HttpStatus.UNPROCESSABLE_ENTITY,
429+
);
430+
}
402431
});
403432
}
404433

0 commit comments

Comments
 (0)