mirror of
https://github.com/Unleash/unleash.git
synced 2025-04-24 01:18:01 +02: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 { IGroupStore, IStoreGroup } from '../types/stores/group-store';
|
||||||
import { Knex } from 'knex';
|
|
||||||
import NotFoundError from '../error/notfound-error';
|
import NotFoundError from '../error/notfound-error';
|
||||||
import Group, {
|
import Group, {
|
||||||
|
ICreateGroupModel,
|
||||||
|
ICreateGroupUserModel,
|
||||||
IGroup,
|
IGroup,
|
||||||
IGroupModel,
|
|
||||||
IGroupProject,
|
IGroupProject,
|
||||||
IGroupRole,
|
IGroupRole,
|
||||||
IGroupUser,
|
IGroupUser,
|
||||||
IGroupUserModel,
|
|
||||||
} from '../types/group';
|
} from '../types/group';
|
||||||
import Transaction = Knex.Transaction;
|
|
||||||
import { Db } from './db';
|
import { Db } from './db';
|
||||||
|
import { BadDataError, FOREIGN_KEY_VIOLATION } from '../error';
|
||||||
|
|
||||||
const T = {
|
const T = {
|
||||||
GROUPS: 'groups',
|
GROUPS: 'groups',
|
||||||
@ -81,13 +80,23 @@ export default class GroupStore implements IGroupStore {
|
|||||||
return groups.map(rowToGroup);
|
return groups.map(rowToGroup);
|
||||||
}
|
}
|
||||||
|
|
||||||
async update(group: IGroupModel): Promise<IGroup> {
|
async update(group: ICreateGroupModel): Promise<IGroup> {
|
||||||
const rows = await this.db(T.GROUPS)
|
try {
|
||||||
.where({ id: group.id })
|
const rows = await this.db(T.GROUPS)
|
||||||
.update(groupToRow(group))
|
.where({ id: group.id })
|
||||||
.returning(GROUP_COLUMNS);
|
.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[]> {
|
async getProjectGroupRoles(projectId: string): Promise<IGroupRole[]> {
|
||||||
@ -176,10 +185,20 @@ export default class GroupStore implements IGroupStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async create(group: IStoreGroup): Promise<Group> {
|
async create(group: IStoreGroup): Promise<Group> {
|
||||||
const row = await this.db(T.GROUPS)
|
try {
|
||||||
.insert(groupToRow(group))
|
const row = await this.db(T.GROUPS)
|
||||||
.returning('*');
|
.insert(groupToRow(group))
|
||||||
return rowToGroup(row[0]);
|
.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> {
|
async count(): Promise<number> {
|
||||||
@ -190,25 +209,31 @@ export default class GroupStore implements IGroupStore {
|
|||||||
|
|
||||||
async addUsersToGroup(
|
async addUsersToGroup(
|
||||||
groupId: number,
|
groupId: number,
|
||||||
users: IGroupUserModel[],
|
users: ICreateGroupUserModel[],
|
||||||
userName: string,
|
userName: string,
|
||||||
transaction?: Transaction,
|
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const rows = (users || []).map((user) => {
|
try {
|
||||||
return {
|
const rows = (users || []).map((user) => {
|
||||||
group_id: groupId,
|
return {
|
||||||
user_id: user.user.id,
|
group_id: groupId,
|
||||||
created_by: userName,
|
user_id: user.user.id,
|
||||||
};
|
created_by: userName,
|
||||||
});
|
};
|
||||||
return (transaction || this.db).batchInsert(T.GROUP_USER, rows);
|
});
|
||||||
|
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(
|
async deleteUsersFromGroup(deletableUsers: IGroupUser[]): Promise<void> {
|
||||||
deletableUsers: IGroupUser[],
|
return this.db(T.GROUP_USER)
|
||||||
transaction?: Transaction,
|
|
||||||
): Promise<void> {
|
|
||||||
return (transaction || this.db)(T.GROUP_USER)
|
|
||||||
.whereIn(
|
.whereIn(
|
||||||
['group_id', 'user_id'],
|
['group_id', 'user_id'],
|
||||||
deletableUsers.map((user) => [user.groupId, user.userId]),
|
deletableUsers.map((user) => [user.groupId, user.userId]),
|
||||||
@ -218,14 +243,12 @@ export default class GroupStore implements IGroupStore {
|
|||||||
|
|
||||||
async updateGroupUsers(
|
async updateGroupUsers(
|
||||||
groupId: number,
|
groupId: number,
|
||||||
newUsers: IGroupUserModel[],
|
newUsers: ICreateGroupUserModel[],
|
||||||
deletableUsers: IGroupUser[],
|
deletableUsers: IGroupUser[],
|
||||||
userName: string,
|
userName: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await this.db.transaction(async (tx) => {
|
await this.addUsersToGroup(groupId, newUsers, userName);
|
||||||
await this.addUsersToGroup(groupId, newUsers, userName, tx);
|
await this.deleteUsersFromGroup(deletableUsers);
|
||||||
await this.deleteUsersFromGroup(deletableUsers, tx);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getNewGroupsForExternalUser(
|
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,
|
strategyVariantSchema,
|
||||||
createStrategyVariantSchema,
|
createStrategyVariantSchema,
|
||||||
clientSegmentSchema,
|
clientSegmentSchema,
|
||||||
|
createGroupSchema,
|
||||||
} from './spec';
|
} from './spec';
|
||||||
import { IServerOption } from '../types';
|
import { IServerOption } from '../types';
|
||||||
import { mapValues, omitKeys } from '../util';
|
import { mapValues, omitKeys } from '../util';
|
||||||
@ -361,6 +362,7 @@ export const schemas: UnleashSchemas = {
|
|||||||
strategyVariantSchema,
|
strategyVariantSchema,
|
||||||
createStrategyVariantSchema,
|
createStrategyVariantSchema,
|
||||||
clientSegmentSchema,
|
clientSegmentSchema,
|
||||||
|
createGroupSchema,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Remove JSONSchema keys that would result in an invalid OpenAPI spec.
|
// 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 = {
|
export const groupSchema = {
|
||||||
$id: '#/components/schemas/groupSchema',
|
$id: '#/components/schemas/groupSchema',
|
||||||
type: 'object',
|
type: 'object',
|
||||||
additionalProperties: true,
|
additionalProperties: false,
|
||||||
required: ['name'],
|
required: ['name'],
|
||||||
description: 'A detailed information about a user group',
|
description: 'A detailed information about a user group',
|
||||||
properties: {
|
properties: {
|
||||||
|
@ -151,3 +151,4 @@ export * from './create-strategy-variant-schema';
|
|||||||
export * from './strategy-variant-schema';
|
export * from './strategy-variant-schema';
|
||||||
export * from './client-segment-schema';
|
export * from './client-segment-schema';
|
||||||
export * from './update-feature-type-lifetime-schema';
|
export * from './update-feature-type-lifetime-schema';
|
||||||
|
export * from './create-group-schema';
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
|
ICreateGroupModel,
|
||||||
IGroup,
|
IGroup,
|
||||||
IGroupModel,
|
IGroupModel,
|
||||||
IGroupModelWithProjectRole,
|
IGroupModelWithProjectRole,
|
||||||
@ -81,7 +82,10 @@ export class GroupService {
|
|||||||
return this.mapGroupWithUsers(group, groupUsers, users);
|
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);
|
await this.validateGroup(group);
|
||||||
|
|
||||||
const newGroup = await this.groupStore.create(group);
|
const newGroup = await this.groupStore.create(group);
|
||||||
@ -101,7 +105,10 @@ export class GroupService {
|
|||||||
return newGroup;
|
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);
|
const preData = await this.groupStore.get(group.id);
|
||||||
|
|
||||||
await this.validateGroup(group, preData);
|
await this.validateGroup(group, preData);
|
||||||
@ -173,7 +180,7 @@ export class GroupService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async validateGroup(
|
async validateGroup(
|
||||||
group: IGroupModel,
|
group: ICreateGroupModel,
|
||||||
existingGroup?: IGroup,
|
existingGroup?: IGroup,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (!group.name) {
|
if (!group.name) {
|
||||||
|
@ -59,6 +59,7 @@ import {
|
|||||||
import ConfigurationRevisionService from '../features/feature-toggle/configuration-revision-service';
|
import ConfigurationRevisionService from '../features/feature-toggle/configuration-revision-service';
|
||||||
import { createFeatureToggleService } from '../features';
|
import { createFeatureToggleService } from '../features';
|
||||||
import EventAnnouncerService from './event-announcer-service';
|
import EventAnnouncerService from './event-announcer-service';
|
||||||
|
import { createGroupService } from '../features/group/createGroupService';
|
||||||
|
|
||||||
// TODO: will be moved to scheduler feature directory
|
// TODO: will be moved to scheduler feature directory
|
||||||
export const scheduleServices = async (
|
export const scheduleServices = async (
|
||||||
@ -211,6 +212,8 @@ export const createServices = (
|
|||||||
createExportImportTogglesService(txDb, config);
|
createExportImportTogglesService(txDb, config);
|
||||||
const transactionalFeatureToggleService = (txDb: Knex.Transaction) =>
|
const transactionalFeatureToggleService = (txDb: Knex.Transaction) =>
|
||||||
createFeatureToggleService(txDb, config);
|
createFeatureToggleService(txDb, config);
|
||||||
|
const transactionalGroupService = (txDb: Knex.Transaction) =>
|
||||||
|
createGroupService(txDb, config);
|
||||||
const userSplashService = new UserSplashService(stores, config);
|
const userSplashService = new UserSplashService(stores, config);
|
||||||
const openApiService = new OpenApiService(config);
|
const openApiService = new OpenApiService(config);
|
||||||
const clientSpecService = new ClientSpecService(config);
|
const clientSpecService = new ClientSpecService(config);
|
||||||
@ -307,6 +310,7 @@ export const createServices = (
|
|||||||
schedulerService,
|
schedulerService,
|
||||||
configurationRevisionService,
|
configurationRevisionService,
|
||||||
transactionalFeatureToggleService,
|
transactionalFeatureToggleService,
|
||||||
|
transactionalGroupService,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -33,6 +33,11 @@ export interface IGroupModel extends IGroup {
|
|||||||
projects?: string[];
|
projects?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ICreateGroupModel extends IGroup {
|
||||||
|
users?: ICreateGroupUserModel[];
|
||||||
|
projects?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface IGroupProject {
|
export interface IGroupProject {
|
||||||
groupId: number;
|
groupId: number;
|
||||||
project: string;
|
project: string;
|
||||||
@ -44,6 +49,10 @@ export interface IGroupUserModel {
|
|||||||
createdBy?: string;
|
createdBy?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ICreateGroupUserModel {
|
||||||
|
user: Pick<IUser, 'id'>;
|
||||||
|
}
|
||||||
|
|
||||||
export interface IGroupModelWithProjectRole extends IGroupModel {
|
export interface IGroupModelWithProjectRole extends IGroupModel {
|
||||||
roleId: number;
|
roleId: number;
|
||||||
addedAt: Date;
|
addedAt: Date;
|
||||||
|
@ -96,4 +96,5 @@ export interface IUnleashServices {
|
|||||||
transactionalFeatureToggleService: (
|
transactionalFeatureToggleService: (
|
||||||
db: Knex.Transaction,
|
db: Knex.Transaction,
|
||||||
) => FeatureToggleService;
|
) => FeatureToggleService;
|
||||||
|
transactionalGroupService: (db: Knex.Transaction) => GroupService;
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import { Store } from './store';
|
import { Store } from './store';
|
||||||
import Group, {
|
import Group, {
|
||||||
|
ICreateGroupModel,
|
||||||
|
ICreateGroupUserModel,
|
||||||
IGroup,
|
IGroup,
|
||||||
IGroupModel,
|
|
||||||
IGroupProject,
|
IGroupProject,
|
||||||
IGroupRole,
|
IGroupRole,
|
||||||
IGroupUser,
|
IGroupUser,
|
||||||
IGroupUserModel,
|
|
||||||
} from '../group';
|
} from '../group';
|
||||||
|
|
||||||
export interface IStoreGroup {
|
export interface IStoreGroup {
|
||||||
@ -38,20 +38,20 @@ export interface IGroupStore extends Store<IGroup, number> {
|
|||||||
|
|
||||||
updateGroupUsers(
|
updateGroupUsers(
|
||||||
groupId: number,
|
groupId: number,
|
||||||
newUsers: IGroupUserModel[],
|
newUsers: ICreateGroupUserModel[],
|
||||||
deletableUsers: IGroupUser[],
|
deletableUsers: IGroupUser[],
|
||||||
userName: string,
|
userName: string,
|
||||||
): Promise<void>;
|
): Promise<void>;
|
||||||
|
|
||||||
deleteUsersFromGroup(deletableUsers: IGroupUser[]): Promise<void>;
|
deleteUsersFromGroup(deletableUsers: IGroupUser[]): Promise<void>;
|
||||||
|
|
||||||
update(group: IGroupModel): Promise<IGroup>;
|
update(group: ICreateGroupModel): Promise<IGroup>;
|
||||||
|
|
||||||
getAllUsersByGroups(groupIds: number[]): Promise<IGroupUser[]>;
|
getAllUsersByGroups(groupIds: number[]): Promise<IGroupUser[]>;
|
||||||
|
|
||||||
addUsersToGroup(
|
addUsersToGroup(
|
||||||
groupId: number,
|
groupId: number,
|
||||||
users: IGroupUserModel[],
|
users: ICreateGroupUserModel[],
|
||||||
userName: string,
|
userName: string,
|
||||||
): Promise<void>;
|
): Promise<void>;
|
||||||
|
|
||||||
|
@ -124,3 +124,26 @@ test('adding a root role to a group with a project role should fail', async () =
|
|||||||
|
|
||||||
expect.assertions(1);
|
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 { IGroupStore, IStoreGroup } from '../../lib/types/stores/group-store';
|
||||||
import Group, {
|
import Group, {
|
||||||
|
ICreateGroupModel,
|
||||||
|
ICreateGroupUserModel,
|
||||||
IGroup,
|
IGroup,
|
||||||
IGroupModel,
|
|
||||||
IGroupProject,
|
IGroupProject,
|
||||||
IGroupRole,
|
IGroupRole,
|
||||||
IGroupUser,
|
IGroupUser,
|
||||||
IGroupUserModel,
|
|
||||||
} from '../../lib/types/group';
|
} from '../../lib/types/group';
|
||||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
export default class FakeGroupStore implements IGroupStore {
|
export default class FakeGroupStore implements IGroupStore {
|
||||||
@ -48,7 +48,7 @@ export default class FakeGroupStore implements IGroupStore {
|
|||||||
|
|
||||||
addUsersToGroup(
|
addUsersToGroup(
|
||||||
id: number,
|
id: number,
|
||||||
users: IGroupUserModel[],
|
users: ICreateGroupUserModel[],
|
||||||
userName: string,
|
userName: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
throw new Error('Method not implemented.');
|
throw new Error('Method not implemented.');
|
||||||
@ -62,13 +62,13 @@ export default class FakeGroupStore implements IGroupStore {
|
|||||||
throw new Error('Method not implemented.');
|
throw new Error('Method not implemented.');
|
||||||
}
|
}
|
||||||
|
|
||||||
update(group: IGroupModel): Promise<IGroup> {
|
update(group: ICreateGroupModel): Promise<IGroup> {
|
||||||
throw new Error('Method not implemented.');
|
throw new Error('Method not implemented.');
|
||||||
}
|
}
|
||||||
|
|
||||||
updateGroupUsers(
|
updateGroupUsers(
|
||||||
groupId: number,
|
groupId: number,
|
||||||
newUsers: IGroupUserModel[],
|
newUsers: ICreateGroupUserModel[],
|
||||||
deletableUsers: IGroupUser[],
|
deletableUsers: IGroupUser[],
|
||||||
userName: string,
|
userName: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
Loading…
Reference in New Issue
Block a user