mirror of
https://github.com/Unleash/unleash.git
synced 2025-01-31 00:16:47 +01:00
fix: group cleanup (#4334)
This commit is contained in:
parent
95b776f4aa
commit
5de4958b0f
@ -1,16 +1,15 @@
|
||||
import { IGroupStore, IStoreGroup } from '../types/stores/group-store';
|
||||
import { Knex } from 'knex';
|
||||
import NotFoundError from '../error/notfound-error';
|
||||
import Group, {
|
||||
ICreateGroupModel,
|
||||
ICreateGroupUserModel,
|
||||
IGroup,
|
||||
IGroupModel,
|
||||
IGroupProject,
|
||||
IGroupRole,
|
||||
IGroupUser,
|
||||
IGroupUserModel,
|
||||
} from '../types/group';
|
||||
import Transaction = Knex.Transaction;
|
||||
import { Db } from './db';
|
||||
import { BadDataError, FOREIGN_KEY_VIOLATION } from '../error';
|
||||
|
||||
const T = {
|
||||
GROUPS: 'groups',
|
||||
@ -81,13 +80,23 @@ export default class GroupStore implements IGroupStore {
|
||||
return groups.map(rowToGroup);
|
||||
}
|
||||
|
||||
async update(group: IGroupModel): Promise<IGroup> {
|
||||
const rows = await this.db(T.GROUPS)
|
||||
.where({ id: group.id })
|
||||
.update(groupToRow(group))
|
||||
.returning(GROUP_COLUMNS);
|
||||
async update(group: ICreateGroupModel): Promise<IGroup> {
|
||||
try {
|
||||
const rows = await this.db(T.GROUPS)
|
||||
.where({ id: group.id })
|
||||
.update(groupToRow(group))
|
||||
.returning(GROUP_COLUMNS);
|
||||
|
||||
return rowToGroup(rows[0]);
|
||||
return rowToGroup(rows[0]);
|
||||
} catch (error) {
|
||||
if (
|
||||
error.code === FOREIGN_KEY_VIOLATION &&
|
||||
error.constraint === 'fk_group_role_id'
|
||||
) {
|
||||
throw new BadDataError(`Incorrect role id ${group.rootRole}`);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async getProjectGroupRoles(projectId: string): Promise<IGroupRole[]> {
|
||||
@ -176,10 +185,20 @@ export default class GroupStore implements IGroupStore {
|
||||
}
|
||||
|
||||
async create(group: IStoreGroup): Promise<Group> {
|
||||
const row = await this.db(T.GROUPS)
|
||||
.insert(groupToRow(group))
|
||||
.returning('*');
|
||||
return rowToGroup(row[0]);
|
||||
try {
|
||||
const row = await this.db(T.GROUPS)
|
||||
.insert(groupToRow(group))
|
||||
.returning('*');
|
||||
return rowToGroup(row[0]);
|
||||
} catch (error) {
|
||||
if (
|
||||
error.code === FOREIGN_KEY_VIOLATION &&
|
||||
error.constraint === 'fk_group_role_id'
|
||||
) {
|
||||
throw new BadDataError(`Incorrect role id ${group.rootRole}`);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async count(): Promise<number> {
|
||||
@ -190,25 +209,31 @@ export default class GroupStore implements IGroupStore {
|
||||
|
||||
async addUsersToGroup(
|
||||
groupId: number,
|
||||
users: IGroupUserModel[],
|
||||
users: ICreateGroupUserModel[],
|
||||
userName: string,
|
||||
transaction?: Transaction,
|
||||
): Promise<void> {
|
||||
const rows = (users || []).map((user) => {
|
||||
return {
|
||||
group_id: groupId,
|
||||
user_id: user.user.id,
|
||||
created_by: userName,
|
||||
};
|
||||
});
|
||||
return (transaction || this.db).batchInsert(T.GROUP_USER, rows);
|
||||
try {
|
||||
const rows = (users || []).map((user) => {
|
||||
return {
|
||||
group_id: groupId,
|
||||
user_id: user.user.id,
|
||||
created_by: userName,
|
||||
};
|
||||
});
|
||||
return await this.db.batchInsert(T.GROUP_USER, rows);
|
||||
} catch (error) {
|
||||
if (
|
||||
error.code === FOREIGN_KEY_VIOLATION &&
|
||||
error.constraint === 'group_user_user_id_fkey'
|
||||
) {
|
||||
throw new BadDataError('Incorrect user id in the users group');
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async deleteUsersFromGroup(
|
||||
deletableUsers: IGroupUser[],
|
||||
transaction?: Transaction,
|
||||
): Promise<void> {
|
||||
return (transaction || this.db)(T.GROUP_USER)
|
||||
async deleteUsersFromGroup(deletableUsers: IGroupUser[]): Promise<void> {
|
||||
return this.db(T.GROUP_USER)
|
||||
.whereIn(
|
||||
['group_id', 'user_id'],
|
||||
deletableUsers.map((user) => [user.groupId, user.userId]),
|
||||
@ -218,14 +243,12 @@ export default class GroupStore implements IGroupStore {
|
||||
|
||||
async updateGroupUsers(
|
||||
groupId: number,
|
||||
newUsers: IGroupUserModel[],
|
||||
newUsers: ICreateGroupUserModel[],
|
||||
deletableUsers: IGroupUser[],
|
||||
userName: string,
|
||||
): Promise<void> {
|
||||
await this.db.transaction(async (tx) => {
|
||||
await this.addUsersToGroup(groupId, newUsers, userName, tx);
|
||||
await this.deleteUsersFromGroup(deletableUsers, tx);
|
||||
});
|
||||
await this.addUsersToGroup(groupId, newUsers, userName);
|
||||
await this.deleteUsersFromGroup(deletableUsers);
|
||||
}
|
||||
|
||||
async getNewGroupsForExternalUser(
|
||||
|
21
src/lib/features/group/createGroupService.ts
Normal file
21
src/lib/features/group/createGroupService.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { IUnleashConfig } from '../../types';
|
||||
import { GroupService } from '../../services';
|
||||
import { Db } from '../../db/db';
|
||||
import GroupStore from '../../db/group-store';
|
||||
import { AccountStore } from '../../db/account-store';
|
||||
import EventStore from '../../db/event-store';
|
||||
|
||||
export const createGroupService = (
|
||||
db: Db,
|
||||
config: IUnleashConfig,
|
||||
): GroupService => {
|
||||
const { getLogger } = config;
|
||||
const groupStore = new GroupStore(db);
|
||||
const accountStore = new AccountStore(db, getLogger);
|
||||
const eventStore = new EventStore(db, getLogger);
|
||||
const groupService = new GroupService(
|
||||
{ groupStore, eventStore, accountStore },
|
||||
{ getLogger },
|
||||
);
|
||||
return groupService;
|
||||
};
|
@ -152,6 +152,7 @@ import {
|
||||
strategyVariantSchema,
|
||||
createStrategyVariantSchema,
|
||||
clientSegmentSchema,
|
||||
createGroupSchema,
|
||||
} from './spec';
|
||||
import { IServerOption } from '../types';
|
||||
import { mapValues, omitKeys } from '../util';
|
||||
@ -361,6 +362,7 @@ export const schemas: UnleashSchemas = {
|
||||
strategyVariantSchema,
|
||||
createStrategyVariantSchema,
|
||||
clientSegmentSchema,
|
||||
createGroupSchema,
|
||||
};
|
||||
|
||||
// Remove JSONSchema keys that would result in an invalid OpenAPI spec.
|
||||
|
43
src/lib/openapi/spec/create-group-schema.ts
Normal file
43
src/lib/openapi/spec/create-group-schema.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import { FromSchema } from 'json-schema-to-ts';
|
||||
import { groupSchema } from './group-schema';
|
||||
|
||||
export const createGroupSchema = {
|
||||
$id: '#/components/schemas/createGroupSchema',
|
||||
type: 'object',
|
||||
additionalProperties: true,
|
||||
required: ['name'],
|
||||
description: 'A detailed information about a user group',
|
||||
properties: {
|
||||
name: groupSchema.properties.name,
|
||||
description: groupSchema.properties.description,
|
||||
mappingsSSO: groupSchema.properties.mappingsSSO,
|
||||
rootRole: groupSchema.properties.rootRole,
|
||||
users: {
|
||||
type: 'array',
|
||||
description: 'A list of users belonging to this group',
|
||||
items: {
|
||||
type: 'object',
|
||||
description: 'A minimal user object',
|
||||
required: ['user'],
|
||||
properties: {
|
||||
user: {
|
||||
type: 'object',
|
||||
description: 'A minimal user object',
|
||||
required: ['id'],
|
||||
properties: {
|
||||
id: {
|
||||
description: 'The user id',
|
||||
type: 'integer',
|
||||
minimum: 0,
|
||||
example: 123,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
components: {},
|
||||
} as const;
|
||||
|
||||
export type CreateGroupSchema = FromSchema<typeof createGroupSchema>;
|
@ -5,7 +5,7 @@ import { userSchema } from './user-schema';
|
||||
export const groupSchema = {
|
||||
$id: '#/components/schemas/groupSchema',
|
||||
type: 'object',
|
||||
additionalProperties: true,
|
||||
additionalProperties: false,
|
||||
required: ['name'],
|
||||
description: 'A detailed information about a user group',
|
||||
properties: {
|
||||
|
@ -151,3 +151,4 @@ export * from './create-strategy-variant-schema';
|
||||
export * from './strategy-variant-schema';
|
||||
export * from './client-segment-schema';
|
||||
export * from './update-feature-type-lifetime-schema';
|
||||
export * from './create-group-schema';
|
||||
|
@ -1,4 +1,5 @@
|
||||
import {
|
||||
ICreateGroupModel,
|
||||
IGroup,
|
||||
IGroupModel,
|
||||
IGroupModelWithProjectRole,
|
||||
@ -81,7 +82,10 @@ export class GroupService {
|
||||
return this.mapGroupWithUsers(group, groupUsers, users);
|
||||
}
|
||||
|
||||
async createGroup(group: IGroupModel, userName: string): Promise<IGroup> {
|
||||
async createGroup(
|
||||
group: ICreateGroupModel,
|
||||
userName: string,
|
||||
): Promise<IGroup> {
|
||||
await this.validateGroup(group);
|
||||
|
||||
const newGroup = await this.groupStore.create(group);
|
||||
@ -101,7 +105,10 @@ export class GroupService {
|
||||
return newGroup;
|
||||
}
|
||||
|
||||
async updateGroup(group: IGroupModel, userName: string): Promise<IGroup> {
|
||||
async updateGroup(
|
||||
group: ICreateGroupModel,
|
||||
userName: string,
|
||||
): Promise<IGroup> {
|
||||
const preData = await this.groupStore.get(group.id);
|
||||
|
||||
await this.validateGroup(group, preData);
|
||||
@ -173,7 +180,7 @@ export class GroupService {
|
||||
}
|
||||
|
||||
async validateGroup(
|
||||
group: IGroupModel,
|
||||
group: ICreateGroupModel,
|
||||
existingGroup?: IGroup,
|
||||
): Promise<void> {
|
||||
if (!group.name) {
|
||||
|
@ -59,6 +59,7 @@ import {
|
||||
import ConfigurationRevisionService from '../features/feature-toggle/configuration-revision-service';
|
||||
import { createFeatureToggleService } from '../features';
|
||||
import EventAnnouncerService from './event-announcer-service';
|
||||
import { createGroupService } from '../features/group/createGroupService';
|
||||
|
||||
// TODO: will be moved to scheduler feature directory
|
||||
export const scheduleServices = async (
|
||||
@ -211,6 +212,8 @@ export const createServices = (
|
||||
createExportImportTogglesService(txDb, config);
|
||||
const transactionalFeatureToggleService = (txDb: Knex.Transaction) =>
|
||||
createFeatureToggleService(txDb, config);
|
||||
const transactionalGroupService = (txDb: Knex.Transaction) =>
|
||||
createGroupService(txDb, config);
|
||||
const userSplashService = new UserSplashService(stores, config);
|
||||
const openApiService = new OpenApiService(config);
|
||||
const clientSpecService = new ClientSpecService(config);
|
||||
@ -307,6 +310,7 @@ export const createServices = (
|
||||
schedulerService,
|
||||
configurationRevisionService,
|
||||
transactionalFeatureToggleService,
|
||||
transactionalGroupService,
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -33,6 +33,11 @@ export interface IGroupModel extends IGroup {
|
||||
projects?: string[];
|
||||
}
|
||||
|
||||
export interface ICreateGroupModel extends IGroup {
|
||||
users?: ICreateGroupUserModel[];
|
||||
projects?: string[];
|
||||
}
|
||||
|
||||
export interface IGroupProject {
|
||||
groupId: number;
|
||||
project: string;
|
||||
@ -44,6 +49,10 @@ export interface IGroupUserModel {
|
||||
createdBy?: string;
|
||||
}
|
||||
|
||||
export interface ICreateGroupUserModel {
|
||||
user: Pick<IUser, 'id'>;
|
||||
}
|
||||
|
||||
export interface IGroupModelWithProjectRole extends IGroupModel {
|
||||
roleId: number;
|
||||
addedAt: Date;
|
||||
|
@ -96,4 +96,5 @@ export interface IUnleashServices {
|
||||
transactionalFeatureToggleService: (
|
||||
db: Knex.Transaction,
|
||||
) => FeatureToggleService;
|
||||
transactionalGroupService: (db: Knex.Transaction) => GroupService;
|
||||
}
|
||||
|
@ -1,11 +1,11 @@
|
||||
import { Store } from './store';
|
||||
import Group, {
|
||||
ICreateGroupModel,
|
||||
ICreateGroupUserModel,
|
||||
IGroup,
|
||||
IGroupModel,
|
||||
IGroupProject,
|
||||
IGroupRole,
|
||||
IGroupUser,
|
||||
IGroupUserModel,
|
||||
} from '../group';
|
||||
|
||||
export interface IStoreGroup {
|
||||
@ -38,20 +38,20 @@ export interface IGroupStore extends Store<IGroup, number> {
|
||||
|
||||
updateGroupUsers(
|
||||
groupId: number,
|
||||
newUsers: IGroupUserModel[],
|
||||
newUsers: ICreateGroupUserModel[],
|
||||
deletableUsers: IGroupUser[],
|
||||
userName: string,
|
||||
): Promise<void>;
|
||||
|
||||
deleteUsersFromGroup(deletableUsers: IGroupUser[]): Promise<void>;
|
||||
|
||||
update(group: IGroupModel): Promise<IGroup>;
|
||||
update(group: ICreateGroupModel): Promise<IGroup>;
|
||||
|
||||
getAllUsersByGroups(groupIds: number[]): Promise<IGroupUser[]>;
|
||||
|
||||
addUsersToGroup(
|
||||
groupId: number,
|
||||
users: IGroupUserModel[],
|
||||
users: ICreateGroupUserModel[],
|
||||
userName: string,
|
||||
): Promise<void>;
|
||||
|
||||
|
@ -124,3 +124,26 @@ test('adding a root role to a group with a project role should fail', async () =
|
||||
|
||||
expect.assertions(1);
|
||||
});
|
||||
|
||||
test('adding a nonexistent role to a group should fail', async () => {
|
||||
const group = await groupStore.create({
|
||||
name: 'root_group',
|
||||
description: 'root_group',
|
||||
});
|
||||
|
||||
await expect(() => {
|
||||
return groupService.updateGroup(
|
||||
{
|
||||
id: group.id,
|
||||
name: group.name,
|
||||
users: [],
|
||||
rootRole: 100,
|
||||
createdAt: new Date(),
|
||||
createdBy: 'test',
|
||||
},
|
||||
'test',
|
||||
);
|
||||
}).rejects.toThrow(
|
||||
'Request validation failed: your request body or params contain invalid data: Incorrect role id 100',
|
||||
);
|
||||
});
|
||||
|
10
src/test/fixtures/fake-group-store.ts
vendored
10
src/test/fixtures/fake-group-store.ts
vendored
@ -1,11 +1,11 @@
|
||||
import { IGroupStore, IStoreGroup } from '../../lib/types/stores/group-store';
|
||||
import Group, {
|
||||
ICreateGroupModel,
|
||||
ICreateGroupUserModel,
|
||||
IGroup,
|
||||
IGroupModel,
|
||||
IGroupProject,
|
||||
IGroupRole,
|
||||
IGroupUser,
|
||||
IGroupUserModel,
|
||||
} from '../../lib/types/group';
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
export default class FakeGroupStore implements IGroupStore {
|
||||
@ -48,7 +48,7 @@ export default class FakeGroupStore implements IGroupStore {
|
||||
|
||||
addUsersToGroup(
|
||||
id: number,
|
||||
users: IGroupUserModel[],
|
||||
users: ICreateGroupUserModel[],
|
||||
userName: string,
|
||||
): Promise<void> {
|
||||
throw new Error('Method not implemented.');
|
||||
@ -62,13 +62,13 @@ export default class FakeGroupStore implements IGroupStore {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
update(group: IGroupModel): Promise<IGroup> {
|
||||
update(group: ICreateGroupModel): Promise<IGroup> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
updateGroupUsers(
|
||||
groupId: number,
|
||||
newUsers: IGroupUserModel[],
|
||||
newUsers: ICreateGroupUserModel[],
|
||||
deletableUsers: IGroupUser[],
|
||||
userName: string,
|
||||
): Promise<void> {
|
||||
|
Loading…
Reference in New Issue
Block a user