1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-03-18 00:19:49 +01:00

feat: add ip to state-service and group-service (#7120)

The add ip to two services. Despite state is being deprecated, I think
we better get it out of the way.
This commit is contained in:
Gastón Fournier 2024-05-24 09:53:46 +02:00 committed by GitHub
parent a744cdf6d8
commit 345c34a945
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 368 additions and 196 deletions

View File

@ -115,7 +115,7 @@ export default class ClientInstanceService {
if (appsToAnnounce.length > 0) { if (appsToAnnounce.length > 0) {
const events = appsToAnnounce.map((app) => ({ const events = appsToAnnounce.map((app) => ({
type: APPLICATION_CREATED, type: APPLICATION_CREATED,
createdBy: app.createdBy || SYSTEM_USER.username, createdBy: app.createdBy || SYSTEM_USER.username!,
data: app, data: app,
createdByUserId: app.createdByUserId || SYSTEM_USER.id, createdByUserId: app.createdByUserId || SYSTEM_USER.id,
})); }));

View File

@ -123,7 +123,7 @@ class StateController extends Controller {
userName, userName,
dropBeforeImport: paramToBool(drop, false), dropBeforeImport: paramToBool(drop, false),
keepExisting: paramToBool(keep, true), keepExisting: paramToBool(keep, true),
userId: req.user.id, auditUser: req.audit,
}); });
res.sendStatus(202); res.sendStatus(202);
} }

View File

@ -19,7 +19,7 @@ import {
type IUnleashOptions, type IUnleashOptions,
type IUnleashServices, type IUnleashServices,
RoleName, RoleName,
SYSTEM_USER, SYSTEM_USER_AUDIT,
} from './types'; } from './types';
import User, { type IAuditUser, type IUser } from './types/user'; import User, { type IAuditUser, type IUser } from './types/user';
@ -101,7 +101,7 @@ async function createApp(
dropBeforeImport: config.import.dropBeforeImport, dropBeforeImport: config.import.dropBeforeImport,
userName: 'import', userName: 'import',
keepExisting: config.import.keepExisting, keepExisting: config.import.keepExisting,
userId: SYSTEM_USER.id, auditUser: SYSTEM_USER_AUDIT,
}); });
} }

View File

@ -21,6 +21,8 @@ import {
GROUP_CREATED, GROUP_CREATED,
GROUP_USER_ADDED, GROUP_USER_ADDED,
GROUP_USER_REMOVED, GROUP_USER_REMOVED,
GroupUserAdded,
GroupUserRemoved,
type IBaseEvent, type IBaseEvent,
} from '../types/events'; } from '../types/events';
import NameExistsError from '../error/name-exists-error'; import NameExistsError from '../error/name-exists-error';
@ -235,6 +237,7 @@ export class GroupService {
return this.groupStore.getProjectGroupRoles(projectId); return this.groupStore.getProjectGroupRoles(projectId);
} }
/** @deprecated use syncExternalGroupsWithAudit */
async syncExternalGroups( async syncExternalGroups(
userId: number, userId: number,
externalGroups: string[], externalGroups: string[],
@ -286,6 +289,52 @@ export class GroupService {
} }
} }
async syncExternalGroupsWithAudit(
userId: number,
externalGroups: string[],
auditUser: IAuditUser,
): Promise<void> {
if (Array.isArray(externalGroups)) {
const newGroups = await this.groupStore.getNewGroupsForExternalUser(
userId,
externalGroups,
);
await this.groupStore.addUserToGroups(
userId,
newGroups.map((g) => g.id),
auditUser.username,
);
const oldGroups = await this.groupStore.getOldGroupsForExternalUser(
userId,
externalGroups,
);
await this.groupStore.deleteUsersFromGroup(oldGroups);
const events: IBaseEvent[] = [];
for (const group of newGroups) {
events.push(
new GroupUserAdded({
userId,
groupId: group.id,
auditUser,
}),
);
}
for (const group of oldGroups) {
events.push(
new GroupUserRemoved({
userId,
groupId: group.groupId,
auditUser,
}),
);
}
await this.eventService.storeEvents(events);
}
}
private mapGroupWithUsers( private mapGroupWithUsers(
group: IGroup, group: IGroup,
allGroupUsers: IGroupUser[], allGroupUsers: IGroupUser[],

View File

@ -14,7 +14,7 @@ import {
import { GLOBAL_ENV } from '../types/environment'; import { GLOBAL_ENV } from '../types/environment';
import variantsExportV3 from '../../test/examples/variantsexport_v3.json'; import variantsExportV3 from '../../test/examples/variantsexport_v3.json';
import EventService from '../features/events/event-service'; import EventService from '../features/events/event-service';
import { SYSTEM_USER_ID } from '../types'; import { SYSTEM_USER_AUDIT } from '../types';
import { EventEmitter } from 'stream'; import { EventEmitter } from 'stream';
const oldExportExample = require('./state-service-export-v1.json'); const oldExportExample = require('./state-service-export-v1.json');
const TESTUSERID = 3333; const TESTUSERID = 3333;
@ -103,7 +103,7 @@ test('should import a feature', async () => {
], ],
}; };
await stateService.import({ userId: SYSTEM_USER_ID, data }); await stateService.import({ auditUser: SYSTEM_USER_AUDIT, data });
const events = await stores.eventStore.getEvents(); const events = await stores.eventStore.getEvents();
expect(events).toHaveLength(1); expect(events).toHaveLength(1);
@ -130,7 +130,7 @@ test('should not import an existing feature', async () => {
await stateService.import({ await stateService.import({
data, data,
keepExisting: true, keepExisting: true,
userId: SYSTEM_USER_ID, auditUser: SYSTEM_USER_AUDIT,
}); });
const events = await stores.eventStore.getEvents(); const events = await stores.eventStore.getEvents();
@ -157,7 +157,7 @@ test('should not keep existing feature if drop-before-import', async () => {
data, data,
keepExisting: true, keepExisting: true,
dropBeforeImport: true, dropBeforeImport: true,
userId: SYSTEM_USER_ID, auditUser: SYSTEM_USER_AUDIT,
}); });
const events = await stores.eventStore.getEvents(); const events = await stores.eventStore.getEvents();
@ -182,7 +182,7 @@ test('should drop feature before import if specified', async () => {
await stateService.import({ await stateService.import({
data, data,
dropBeforeImport: true, dropBeforeImport: true,
userId: SYSTEM_USER_ID, auditUser: SYSTEM_USER_AUDIT,
}); });
const events = await stores.eventStore.getEvents(); const events = await stores.eventStore.getEvents();
@ -204,7 +204,7 @@ test('should import a strategy', async () => {
], ],
}; };
await stateService.import({ userId: SYSTEM_USER_ID, data }); await stateService.import({ auditUser: SYSTEM_USER_AUDIT, data });
const events = await stores.eventStore.getEvents(); const events = await stores.eventStore.getEvents();
expect(events).toHaveLength(1); expect(events).toHaveLength(1);
@ -228,7 +228,7 @@ test('should not import an existing strategy', async () => {
await stateService.import({ await stateService.import({
data, data,
userId: SYSTEM_USER_ID, auditUser: SYSTEM_USER_AUDIT,
keepExisting: true, keepExisting: true,
}); });
@ -250,7 +250,7 @@ test('should drop strategies before import if specified', async () => {
await stateService.import({ await stateService.import({
data, data,
userId: SYSTEM_USER_ID, auditUser: SYSTEM_USER_AUDIT,
dropBeforeImport: true, dropBeforeImport: true,
}); });
@ -268,7 +268,7 @@ test('should drop neither features nor strategies when neither is imported', asy
await stateService.import({ await stateService.import({
data, data,
userId: SYSTEM_USER_ID, auditUser: SYSTEM_USER_AUDIT,
dropBeforeImport: true, dropBeforeImport: true,
}); });
@ -286,11 +286,11 @@ test('should not accept gibberish', async () => {
const data2 = '{somerandomtext/'; const data2 = '{somerandomtext/';
await expect(async () => await expect(async () =>
stateService.import({ userId: SYSTEM_USER_ID, data: data1 }), stateService.import({ auditUser: SYSTEM_USER_AUDIT, data: data1 }),
).rejects.toThrow(); ).rejects.toThrow();
await expect(async () => await expect(async () =>
stateService.import({ userId: SYSTEM_USER_ID, data: data2 }), stateService.import({ auditUser: SYSTEM_USER_AUDIT, data: data2 }),
).rejects.toThrow(); ).rejects.toThrow();
}); });
@ -386,7 +386,7 @@ test('should import a tag and tag type', async () => {
tags: [{ type: 'simple', value: 'test' }], tags: [{ type: 'simple', value: 'test' }],
}; };
await stateService.import({ userId: SYSTEM_USER_ID, data }); await stateService.import({ auditUser: SYSTEM_USER_AUDIT, data });
const events = await stores.eventStore.getEvents(); const events = await stores.eventStore.getEvents();
expect(events).toHaveLength(2); expect(events).toHaveLength(2);
@ -423,7 +423,7 @@ test('Should not import an existing tag', async () => {
); );
await stateService.import({ await stateService.import({
data, data,
userId: SYSTEM_USER_ID, auditUser: SYSTEM_USER_AUDIT,
keepExisting: true, keepExisting: true,
}); });
const events = await stores.eventStore.getEvents(); const events = await stores.eventStore.getEvents();
@ -460,7 +460,7 @@ test('Should not keep existing tags if drop-before-import', async () => {
}; };
await stateService.import({ await stateService.import({
data, data,
userId: SYSTEM_USER_ID, auditUser: SYSTEM_USER_AUDIT,
dropBeforeImport: true, dropBeforeImport: true,
}); });
const tagTypes = await stores.tagTypeStore.getAll(); const tagTypes = await stores.tagTypeStore.getAll();
@ -570,7 +570,7 @@ test('should import a project', async () => {
], ],
}; };
await stateService.import({ userId: SYSTEM_USER_ID, data }); await stateService.import({ auditUser: SYSTEM_USER_AUDIT, data });
const events = await stores.eventStore.getEvents(); const events = await stores.eventStore.getEvents();
expect(events).toHaveLength(1); expect(events).toHaveLength(1);
@ -595,13 +595,13 @@ test('Should not import an existing project', async () => {
await stateService.import({ await stateService.import({
data, data,
userId: SYSTEM_USER_ID, auditUser: SYSTEM_USER_AUDIT,
keepExisting: true, keepExisting: true,
}); });
const events = await stores.eventStore.getEvents(); const events = await stores.eventStore.getEvents();
expect(events).toHaveLength(0); expect(events).toHaveLength(0);
await stateService.import({ userId: SYSTEM_USER_ID, data }); await stateService.import({ auditUser: SYSTEM_USER_AUDIT, data });
}); });
test('Should drop projects before import if specified', async () => { test('Should drop projects before import if specified', async () => {
@ -624,7 +624,7 @@ test('Should drop projects before import if specified', async () => {
}); });
await stateService.import({ await stateService.import({
data, data,
userId: SYSTEM_USER_ID, auditUser: SYSTEM_USER_AUDIT,
dropBeforeImport: true, dropBeforeImport: true,
}); });
const hasProject = await stores.projectStore.hasProject('fancy'); const hasProject = await stores.projectStore.hasProject('fancy');
@ -773,8 +773,7 @@ test('featureStrategies can keep existing', async () => {
const exported = await stateService.export({}); const exported = await stateService.export({});
await stateService.import({ await stateService.import({
data: exported, data: exported,
userId: SYSTEM_USER_ID, auditUser: SYSTEM_USER_AUDIT,
userName: 'testing',
keepExisting: true, keepExisting: true,
}); });
expect(await stores.featureStrategiesStore.getAll()).toHaveLength(1); expect(await stores.featureStrategiesStore.getAll()).toHaveLength(1);
@ -830,8 +829,7 @@ test('featureStrategies should not keep existing if dropBeforeImport', async ()
exported.featureStrategies = []; exported.featureStrategies = [];
await stateService.import({ await stateService.import({
data: exported, data: exported,
userId: SYSTEM_USER_ID, auditUser: SYSTEM_USER_AUDIT,
userName: 'testing',
keepExisting: true, keepExisting: true,
dropBeforeImport: true, dropBeforeImport: true,
}); });
@ -842,9 +840,8 @@ test('Import v1 and exporting v2 should work', async () => {
const { stateService } = getSetup(); const { stateService } = getSetup();
await stateService.import({ await stateService.import({
data: oldExportExample, data: oldExportExample,
userId: SYSTEM_USER_ID, auditUser: SYSTEM_USER_AUDIT,
dropBeforeImport: true, dropBeforeImport: true,
userName: 'testing',
}); });
const exported = await stateService.export({}); const exported = await stateService.export({});
const strategiesCount = oldExportExample.features.reduce( const strategiesCount = oldExportExample.features.reduce(
@ -879,8 +876,7 @@ test('Importing states with deprecated strategies should keep their deprecated s
}; };
await stateService.import({ await stateService.import({
data: deprecatedStrategyExample, data: deprecatedStrategyExample,
userId: SYSTEM_USER_ID, auditUser: SYSTEM_USER_AUDIT,
userName: 'strategy-importer',
dropBeforeImport: true, dropBeforeImport: true,
keepExisting: false, keepExisting: false,
}); });
@ -894,9 +890,8 @@ test('Exporting a deprecated strategy and then importing it should keep correct
await stateService.import({ await stateService.import({
data: variantsExportV3, data: variantsExportV3,
keepExisting: false, keepExisting: false,
userId: SYSTEM_USER_ID, auditUser: SYSTEM_USER_AUDIT,
dropBeforeImport: true, dropBeforeImport: true,
userName: 'strategy importer',
}); });
const rolloutRandom = await stores.strategyStore.get( const rolloutRandom = await stores.strategyStore.get(
'gradualRolloutRandom', 'gradualRolloutRandom',

View File

@ -1,19 +1,19 @@
import { stateSchema } from './state-schema'; import { stateSchema } from './state-schema';
import { import {
DROP_ENVIRONMENTS, DropEnvironmentsEvent,
DROP_FEATURE_TAGS, DropFeaturesEvent,
DROP_FEATURES, DropFeatureTagsEvent,
DROP_PROJECTS, DropProjectsEvent,
DROP_STRATEGIES, DropStrategiesEvent,
DROP_TAG_TYPES, DropTagsEvent,
DROP_TAGS, DropTagTypesEvent,
ENVIRONMENT_IMPORT, EnvironmentImport,
FEATURE_IMPORT, FeatureImport,
FEATURE_TAG_IMPORT, FeatureTagImport,
PROJECT_IMPORT, ProjectImport,
STRATEGY_IMPORT, StrategyImport,
TAG_IMPORT, TagImport,
TAG_TYPE_IMPORT, TagTypeImport,
} from '../types/events'; } from '../types/events';
import { filterEqual, filterExisting, parseFile, readFile } from './state-util'; import { filterEqual, filterExisting, parseFile, readFile } from './state-util';
@ -53,6 +53,7 @@ import { GLOBAL_ENV } from '../types/environment';
import type { ISegmentStore } from '../features/segment/segment-store-type'; import type { ISegmentStore } from '../features/segment/segment-store-type';
import type { PartialSome } from '../types/partial'; import type { PartialSome } from '../types/partial';
import type EventService from '../features/events/event-service'; import type EventService from '../features/events/event-service';
import type { IAuditUser } from '../server-impl';
export interface IBackupOption { export interface IBackupOption {
includeFeatureToggles: boolean; includeFeatureToggles: boolean;
@ -117,8 +118,7 @@ export default class StateService {
async importFile({ async importFile({
file, file,
dropBeforeImport = false, dropBeforeImport = false,
userName = 'import-user', auditUser,
userId,
keepExisting = true, keepExisting = true,
}: IImportFile): Promise<void> { }: IImportFile): Promise<void> {
return readFile(file) return readFile(file)
@ -126,10 +126,9 @@ export default class StateService {
.then((data) => .then((data) =>
this.import({ this.import({
data, data,
userName, auditUser,
dropBeforeImport, dropBeforeImport,
keepExisting, keepExisting,
userId,
}), }),
); );
} }
@ -169,8 +168,7 @@ export default class StateService {
async import({ async import({
data, data,
userName = 'importUser', auditUser,
userId,
dropBeforeImport = false, dropBeforeImport = false,
keepExisting = true, keepExisting = true,
}: IImportData): Promise<void> { }: IImportData): Promise<void> {
@ -186,10 +184,9 @@ export default class StateService {
if (importData.environments) { if (importData.environments) {
importedEnvironments = await this.importEnvironments({ importedEnvironments = await this.importEnvironments({
environments: data.environments, environments: data.environments,
userName,
dropBeforeImport, dropBeforeImport,
keepExisting, keepExisting,
userId, auditUser,
}); });
} }
@ -197,10 +194,9 @@ export default class StateService {
await this.importProjects({ await this.importProjects({
projects: data.projects, projects: data.projects,
importedEnvironments, importedEnvironments,
userName,
dropBeforeImport, dropBeforeImport,
keepExisting, keepExisting,
userId, auditUser,
}); });
} }
@ -217,11 +213,10 @@ export default class StateService {
await this.importFeatures({ await this.importFeatures({
features, features,
userName,
dropBeforeImport, dropBeforeImport,
keepExisting, keepExisting,
featureEnvironments, featureEnvironments,
userId, auditUser,
}); });
if (featureEnvironments) { if (featureEnvironments) {
@ -240,10 +235,9 @@ export default class StateService {
if (importData.strategies) { if (importData.strategies) {
await this.importStrategies({ await this.importStrategies({
strategies: data.strategies, strategies: data.strategies,
userName,
dropBeforeImport, dropBeforeImport,
keepExisting, keepExisting,
userId, auditUser,
}); });
} }
@ -263,18 +257,16 @@ export default class StateService {
tagValue: t.tagValue || t.value, tagValue: t.tagValue || t.value,
tagType: t.tagType || t.type, tagType: t.tagType || t.type,
})) || [], })) || [],
userName,
dropBeforeImport, dropBeforeImport,
keepExisting, keepExisting,
userId, auditUser,
}); });
} }
if (importData.segments) { if (importData.segments) {
await this.importSegments( await this.importSegments(
data.segments, data.segments,
userName, auditUser,
userId,
dropBeforeImport, dropBeforeImport,
); );
} }
@ -365,14 +357,12 @@ export default class StateService {
}; };
} }
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
async importFeatures({ async importFeatures({
features, features,
userName,
userId,
dropBeforeImport, dropBeforeImport,
keepExisting, keepExisting,
featureEnvironments, featureEnvironments,
auditUser,
}): Promise<void> { }): Promise<void> {
this.logger.info(`Importing ${features.length} feature flags`); this.logger.info(`Importing ${features.length} feature flags`);
const oldToggles = dropBeforeImport const oldToggles = dropBeforeImport
@ -382,12 +372,9 @@ export default class StateService {
if (dropBeforeImport) { if (dropBeforeImport) {
this.logger.info('Dropping existing feature flags'); this.logger.info('Dropping existing feature flags');
await this.toggleStore.deleteAll(); await this.toggleStore.deleteAll();
await this.eventService.storeEvent({ await this.eventService.storeEvent(
type: DROP_FEATURES, new DropFeaturesEvent({ auditUser }),
createdBy: userName, );
createdByUserId: userId,
data: { name: 'all-features' },
});
} }
await Promise.all( await Promise.all(
@ -396,7 +383,7 @@ export default class StateService {
.filter(filterEqual(oldToggles)) .filter(filterEqual(oldToggles))
.map(async (feature) => { .map(async (feature) => {
await this.toggleStore.create(feature.project, { await this.toggleStore.create(feature.project, {
createdByUserId: userId, createdByUserId: auditUser.id,
...feature, ...feature,
}); });
await this.featureEnvironmentStore.connectFeatureToEnvironmentsForProject( await this.featureEnvironmentStore.connectFeatureToEnvironmentsForProject(
@ -404,12 +391,12 @@ export default class StateService {
feature.project, feature.project,
this.enabledIn(feature.name, featureEnvironments), this.enabledIn(feature.name, featureEnvironments),
); );
await this.eventService.storeEvent({ await this.eventService.storeEvent(
type: FEATURE_IMPORT, new FeatureImport({
createdByUserId: userId, feature,
createdBy: userName, auditUser,
data: feature, }),
}); );
}), }),
); );
} }
@ -417,10 +404,9 @@ export default class StateService {
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
async importStrategies({ async importStrategies({
strategies, strategies,
userName,
userId,
dropBeforeImport, dropBeforeImport,
keepExisting, keepExisting,
auditUser,
}): Promise<void> { }): Promise<void> {
this.logger.info(`Importing ${strategies.length} strategies`); this.logger.info(`Importing ${strategies.length} strategies`);
const oldStrategies = dropBeforeImport const oldStrategies = dropBeforeImport
@ -430,12 +416,9 @@ export default class StateService {
if (dropBeforeImport) { if (dropBeforeImport) {
this.logger.info('Dropping existing strategies'); this.logger.info('Dropping existing strategies');
await this.strategyStore.dropCustomStrategies(); await this.strategyStore.dropCustomStrategies();
await this.eventService.storeEvent({ await this.eventService.storeEvent(
type: DROP_STRATEGIES, new DropStrategiesEvent({ auditUser }),
createdBy: userName, );
createdByUserId: userId,
data: { name: 'all-strategies' },
});
} }
await Promise.all( await Promise.all(
@ -444,12 +427,9 @@ export default class StateService {
.filter(filterEqual(oldStrategies)) .filter(filterEqual(oldStrategies))
.map((strategy) => .map((strategy) =>
this.strategyStore.importStrategy(strategy).then(() => { this.strategyStore.importStrategy(strategy).then(() => {
this.eventService.storeEvent({ this.eventService.storeEvent(
type: STRATEGY_IMPORT, new StrategyImport({ strategy, auditUser }),
createdBy: userName, );
createdByUserId: userId,
data: strategy,
});
}), }),
), ),
); );
@ -458,8 +438,7 @@ export default class StateService {
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
async importEnvironments({ async importEnvironments({
environments, environments,
userName, auditUser,
userId,
dropBeforeImport, dropBeforeImport,
keepExisting, keepExisting,
}): Promise<IEnvironment[]> { }): Promise<IEnvironment[]> {
@ -470,12 +449,9 @@ export default class StateService {
if (dropBeforeImport) { if (dropBeforeImport) {
this.logger.info('Dropping existing environments'); this.logger.info('Dropping existing environments');
await this.environmentStore.deleteAll(); await this.environmentStore.deleteAll();
await this.eventService.storeEvent({ await this.eventService.storeEvent(
type: DROP_ENVIRONMENTS, new DropEnvironmentsEvent({ auditUser }),
createdBy: userName, );
createdByUserId: userId,
data: { name: 'all-environments' },
});
} }
const envsImport = environments.filter((env) => const envsImport = environments.filter((env) =>
keepExisting ? !oldEnvs.some((old) => old.name === env.name) : true, keepExisting ? !oldEnvs.some((old) => old.name === env.name) : true,
@ -484,12 +460,9 @@ export default class StateService {
if (envsImport.length > 0) { if (envsImport.length > 0) {
importedEnvs = importedEnvs =
await this.environmentStore.importEnvironments(envsImport); await this.environmentStore.importEnvironments(envsImport);
const importedEnvironmentEvents = importedEnvs.map((env) => ({ const importedEnvironmentEvents = importedEnvs.map(
type: ENVIRONMENT_IMPORT, (env) => new EnvironmentImport({ auditUser, env }),
createdBy: userName, );
createdByUserId: userId,
data: env,
}));
await this.eventService.storeEvents(importedEnvironmentEvents); await this.eventService.storeEvents(importedEnvironmentEvents);
} }
return importedEnvs; return importedEnvs;
@ -499,8 +472,7 @@ export default class StateService {
async importProjects({ async importProjects({
projects, projects,
importedEnvironments, importedEnvironments,
userName, auditUser,
userId,
dropBeforeImport, dropBeforeImport,
keepExisting, keepExisting,
}): Promise<void> { }): Promise<void> {
@ -511,12 +483,9 @@ export default class StateService {
if (dropBeforeImport) { if (dropBeforeImport) {
this.logger.info('Dropping existing projects'); this.logger.info('Dropping existing projects');
await this.projectStore.deleteAll(); await this.projectStore.deleteAll();
await this.eventService.storeEvent({ await this.eventService.storeEvent(
type: DROP_PROJECTS, new DropProjectsEvent({ auditUser }),
createdBy: userName, );
createdByUserId: userId,
data: { name: 'all-projects' },
});
} }
const projectsToImport = projects.filter((project) => const projectsToImport = projects.filter((project) =>
keepExisting keepExisting
@ -528,12 +497,9 @@ export default class StateService {
projectsToImport, projectsToImport,
importedEnvironments, importedEnvironments,
); );
const importedProjectEvents = importedProjects.map((project) => ({ const importedProjectEvents = importedProjects.map(
type: PROJECT_IMPORT, (project) => new ProjectImport({ project, auditUser }),
createdBy: userName, );
createdByUserId: userId,
data: project,
}));
await this.eventService.storeEvents(importedProjectEvents); await this.eventService.storeEvents(importedProjectEvents);
} }
} }
@ -543,8 +509,7 @@ export default class StateService {
tagTypes, tagTypes,
tags, tags,
featureTags, featureTags,
userName, auditUser,
userId,
dropBeforeImport, dropBeforeImport,
keepExisting, keepExisting,
}): Promise<void> { }): Promise<void> {
@ -566,40 +531,23 @@ export default class StateService {
await this.tagStore.deleteAll(); await this.tagStore.deleteAll();
await this.tagTypeStore.deleteAll(); await this.tagTypeStore.deleteAll();
await this.eventService.storeEvents([ await this.eventService.storeEvents([
{ new DropFeatureTagsEvent({ auditUser }),
type: DROP_FEATURE_TAGS, new DropTagsEvent({ auditUser }),
createdBy: userName, new DropTagTypesEvent({ auditUser }),
createdByUserId: userId,
data: { name: 'all-feature-tags' },
},
{
type: DROP_TAGS,
createdBy: userName,
createdByUserId: userId,
data: { name: 'all-tags' },
},
{
type: DROP_TAG_TYPES,
createdBy: userName,
createdByUserId: userId,
data: { name: 'all-tag-types' },
},
]); ]);
} }
await this.importTagTypes( await this.importTagTypes(
tagTypes, tagTypes,
keepExisting, keepExisting,
oldTagTypes, oldTagTypes,
userName, auditUser,
userId,
); );
await this.importTags(tags, keepExisting, oldTags, userName, userId); await this.importTags(tags, keepExisting, oldTags, auditUser);
await this.importFeatureTags( await this.importFeatureTags(
featureTags, featureTags,
keepExisting, keepExisting,
oldFeatureTags, oldFeatureTags,
userName, auditUser,
userId,
); );
} }
@ -615,8 +563,7 @@ export default class StateService {
featureTags: IFeatureTag[], featureTags: IFeatureTag[],
keepExisting: boolean, keepExisting: boolean,
oldFeatureTags: IFeatureTag[], oldFeatureTags: IFeatureTag[],
userName: string, auditUser: IAuditUser,
userId: number,
): Promise<void> { ): Promise<void> {
const featureTagsToInsert = featureTags const featureTagsToInsert = featureTags
.filter((tag) => .filter((tag) =>
@ -627,18 +574,15 @@ export default class StateService {
: true, : true,
) )
.map((tag) => ({ .map((tag) => ({
createdByUserId: userId, createdByUserId: auditUser.id,
...tag, ...tag,
})); }));
if (featureTagsToInsert.length > 0) { if (featureTagsToInsert.length > 0) {
const importedFeatureTags = const importedFeatureTags =
await this.featureTagStore.tagFeatures(featureTagsToInsert); await this.featureTagStore.tagFeatures(featureTagsToInsert);
const importedFeatureTagEvents = importedFeatureTags.map((tag) => ({ const importedFeatureTagEvents = importedFeatureTags.map(
type: FEATURE_TAG_IMPORT, (featureTag) => new FeatureTagImport({ featureTag, auditUser }),
createdBy: userName, );
createdByUserId: userId,
data: tag,
}));
await this.eventService.storeEvents(importedFeatureTagEvents); await this.eventService.storeEvents(importedFeatureTagEvents);
} }
} }
@ -650,8 +594,7 @@ export default class StateService {
tags: ITag[], tags: ITag[],
keepExisting: boolean, keepExisting: boolean,
oldTags: ITag[], oldTags: ITag[],
userName: string, auditUser: IAuditUser,
userId: number,
): Promise<void> { ): Promise<void> {
const tagsToInsert = tags.filter((tag) => const tagsToInsert = tags.filter((tag) =>
keepExisting keepExisting
@ -660,12 +603,9 @@ export default class StateService {
); );
if (tagsToInsert.length > 0) { if (tagsToInsert.length > 0) {
const importedTags = await this.tagStore.bulkImport(tagsToInsert); const importedTags = await this.tagStore.bulkImport(tagsToInsert);
const importedTagEvents = importedTags.map((tag) => ({ const importedTagEvents = importedTags.map(
type: TAG_IMPORT, (tag) => new TagImport({ tag, auditUser }),
createdBy: userName, );
createdByUserId: userId,
data: tag,
}));
await this.eventService.storeEvents(importedTagEvents); await this.eventService.storeEvents(importedTagEvents);
} }
} }
@ -674,8 +614,7 @@ export default class StateService {
tagTypes: ITagType[], tagTypes: ITagType[],
keepExisting: boolean, keepExisting: boolean,
oldTagTypes: ITagType[], oldTagTypes: ITagType[],
userName: string, auditUser: IAuditUser,
userId: number,
): Promise<void> { ): Promise<void> {
const tagTypesToInsert = tagTypes.filter((tagType) => const tagTypesToInsert = tagTypes.filter((tagType) =>
keepExisting keepExisting
@ -685,20 +624,16 @@ export default class StateService {
if (tagTypesToInsert.length > 0) { if (tagTypesToInsert.length > 0) {
const importedTagTypes = const importedTagTypes =
await this.tagTypeStore.bulkImport(tagTypesToInsert); await this.tagTypeStore.bulkImport(tagTypesToInsert);
const importedTagTypeEvents = importedTagTypes.map((tagType) => ({ const importedTagTypeEvents = importedTagTypes.map(
type: TAG_TYPE_IMPORT, (tagType) => new TagTypeImport({ tagType, auditUser }),
createdBy: userName, );
createdByUserId: userId,
data: tagType,
}));
await this.eventService.storeEvents(importedTagTypeEvents); await this.eventService.storeEvents(importedTagTypeEvents);
} }
} }
async importSegments( async importSegments(
segments: PartialSome<ISegment, 'id'>[], segments: PartialSome<ISegment, 'id'>[],
userName: string, auditUser: IAuditUser,
userId: number,
dropBeforeImport: boolean, dropBeforeImport: boolean,
): Promise<void> { ): Promise<void> {
if (dropBeforeImport) { if (dropBeforeImport) {
@ -707,7 +642,9 @@ export default class StateService {
await Promise.all( await Promise.all(
segments.map((segment) => segments.map((segment) =>
this.segmentStore.create(segment, { username: userName }), this.segmentStore.create(segment, {
username: auditUser.username,
}),
), ),
); );
} }

View File

@ -1,7 +1,16 @@
import type { FeatureToggle, IStrategyConfig, ITag, IVariant } from './model'; import type {
FeatureToggle,
IEnvironment,
IProject,
IStrategyConfig,
ITag,
IVariant,
} from './model';
import type { IApiToken } from './models/api-token'; import type { IApiToken } from './models/api-token';
import type { IAuditUser, IUserWithRootRole } from './user'; import type { IAuditUser, IUserWithRootRole } from './user';
import type { FeatureLifecycleCompletedSchema } from '../openapi'; import type { FeatureLifecycleCompletedSchema } from '../openapi';
import type { ITagType } from '../features/tag-type/tag-type-store-type';
import type { IFeatureAndTag } from './stores/feature-tag-store';
export const APPLICATION_CREATED = 'application-created' as const; export const APPLICATION_CREATED = 'application-created' as const;
@ -356,7 +365,8 @@ export interface IBaseEvent {
tags?: ITag[]; tags?: ITag[];
} }
export interface IEvent extends IBaseEvent { // This represents the read model for events
export interface IEvent extends Omit<IBaseEvent, 'ip'> {
id: number; id: number;
createdAt: Date; createdAt: Date;
} }
@ -366,7 +376,7 @@ export interface IEventList {
events: IEvent[]; events: IEvent[];
} }
class BaseEvent implements IBaseEvent { export class BaseEvent implements IBaseEvent {
readonly type: IEventType; readonly type: IEventType;
readonly createdBy: string; readonly createdBy: string;
@ -633,6 +643,83 @@ export class FeatureCreatedEvent extends BaseEvent {
} }
} }
export class ProjectImport extends BaseEvent {
readonly data: IProject;
constructor(p: {
project: IProject;
auditUser: IAuditUser;
}) {
super(PROJECT_IMPORT, p.auditUser);
this.data = p.project;
}
}
export class FeatureImport extends BaseEvent {
readonly data: any;
constructor(p: {
feature: any;
auditUser: IAuditUser;
}) {
super(FEATURE_IMPORT, p.auditUser);
this.data = p.feature;
}
}
export class StrategyImport extends BaseEvent {
readonly data: any;
constructor(p: {
strategy: any;
auditUser: IAuditUser;
}) {
super(STRATEGY_IMPORT, p.auditUser);
this.data = p.strategy;
}
}
export class EnvironmentImport extends BaseEvent {
readonly data: IEnvironment;
constructor(p: {
env: IEnvironment;
auditUser: IAuditUser;
}) {
super(ENVIRONMENT_IMPORT, p.auditUser);
this.data = p.env;
}
}
export class TagTypeImport extends BaseEvent {
readonly data: ITagType;
constructor(p: {
tagType: ITagType;
auditUser: IAuditUser;
}) {
super(TAG_TYPE_IMPORT, p.auditUser);
this.data = p.tagType;
}
}
export class TagImport extends BaseEvent {
readonly data: ITag;
constructor(p: {
tag: ITag;
auditUser: IAuditUser;
}) {
super(TAG_IMPORT, p.auditUser);
this.data = p.tag;
}
}
export class FeatureTagImport extends BaseEvent {
readonly data: IFeatureAndTag;
constructor(p: {
featureTag: IFeatureAndTag;
auditUser: IAuditUser;
}) {
super(FEATURE_TAG_IMPORT, p.auditUser);
this.data = p.featureTag;
}
}
export class FeatureCompletedEvent extends BaseEvent { export class FeatureCompletedEvent extends BaseEvent {
readonly featureName: string; readonly featureName: string;
readonly data: FeatureLifecycleCompletedSchema; readonly data: FeatureLifecycleCompletedSchema;
@ -1202,6 +1289,36 @@ export class ProjectAccessGroupRolesUpdated extends BaseEvent {
} }
} }
export class GroupUserRemoved extends BaseEvent {
readonly preData: any;
constructor(p: {
userId: number;
groupId: number;
auditUser: IAuditUser;
}) {
super(GROUP_USER_REMOVED, p.auditUser);
this.preData = {
groupId: p.groupId,
userId: p.userId,
};
}
}
export class GroupUserAdded extends BaseEvent {
readonly data: any;
constructor(p: {
userId: number;
groupId: number;
auditUser: IAuditUser;
}) {
super(GROUP_USER_ADDED, p.auditUser);
this.data = {
groupId: p.groupId,
userId: p.userId,
};
}
}
export class ProjectAccessUserRolesDeleted extends BaseEvent { export class ProjectAccessUserRolesDeleted extends BaseEvent {
readonly project: string; readonly project: string;
@ -1558,6 +1675,83 @@ export class FeaturesExportedEvent extends BaseEvent {
} }
} }
export class DropProjectsEvent extends BaseEvent {
readonly data: any;
constructor(eventData: {
auditUser: IAuditUser;
}) {
super(DROP_PROJECTS, eventData.auditUser);
this.data = { name: 'all-projects' };
}
}
export class DropFeaturesEvent extends BaseEvent {
readonly data: any;
constructor(eventData: {
auditUser: IAuditUser;
}) {
super(DROP_FEATURES, eventData.auditUser);
this.data = { name: 'all-features' };
}
}
export class DropStrategiesEvent extends BaseEvent {
readonly data: any;
constructor(eventData: {
auditUser: IAuditUser;
}) {
super(DROP_STRATEGIES, eventData.auditUser);
this.data = { name: 'all-strategies' };
}
}
export class DropEnvironmentsEvent extends BaseEvent {
readonly data: any;
constructor(eventData: {
auditUser: IAuditUser;
}) {
super(DROP_ENVIRONMENTS, eventData.auditUser);
this.data = { name: 'all-environments' };
}
}
export class DropFeatureTagsEvent extends BaseEvent {
readonly data: any;
constructor(eventData: {
auditUser: IAuditUser;
}) {
super(DROP_FEATURE_TAGS, eventData.auditUser);
this.data = { name: 'all-feature-tags' };
}
}
export class DropTagsEvent extends BaseEvent {
readonly data: any;
constructor(eventData: {
auditUser: IAuditUser;
}) {
super(DROP_TAGS, eventData.auditUser);
this.data = { name: 'all-tags' };
}
}
export class DropTagTypesEvent extends BaseEvent {
readonly data: any;
constructor(eventData: {
auditUser: IAuditUser;
}) {
super(DROP_TAG_TYPES, eventData.auditUser);
this.data = { name: 'all-tag-types' };
}
}
export class RoleCreatedEvent extends BaseEvent { export class RoleCreatedEvent extends BaseEvent {
readonly data: any; readonly data: any;

View File

@ -1,7 +1,7 @@
import type { ITagType } from '../features/tag-type/tag-type-store-type'; import type { ITagType } from '../features/tag-type/tag-type-store-type';
import type { LogProvider } from '../logger'; import type { LogProvider } from '../logger';
import type { IRole } from './stores/access-store'; import type { IRole } from './stores/access-store';
import type { IUser } from './user'; import type { IAuditUser, IUser } from './user';
import type { ALL_OPERATORS } from '../util'; import type { ALL_OPERATORS } from '../util';
import type { IProjectStats } from '../features/project/project-service'; import type { IProjectStats } from '../features/project/project-service';
import type { CreateFeatureStrategySchema } from '../openapi'; import type { CreateFeatureStrategySchema } from '../openapi';
@ -479,8 +479,11 @@ export interface IImportFile extends ImportCommon {
interface ImportCommon { interface ImportCommon {
dropBeforeImport?: boolean; dropBeforeImport?: boolean;
keepExisting?: boolean; keepExisting?: boolean;
/** @deprecated should use auditUser instead */
userName?: string; userName?: string;
userId: number; /** @deprecated should use auditUser instead */
userId?: number;
auditUser: IAuditUser;
} }
export interface IImportData extends ImportCommon { export interface IImportData extends ImportCommon {

View File

@ -206,8 +206,7 @@ test('Can roundtrip. I.e. export and then import', async () => {
data, data,
dropBeforeImport: true, dropBeforeImport: true,
keepExisting: false, keepExisting: false,
userName: 'export-tester', auditUser: SYSTEM_USER_AUDIT,
userId: -9999,
}); });
}); });
@ -270,8 +269,7 @@ test('Roundtrip with tags works', async () => {
data, data,
dropBeforeImport: true, dropBeforeImport: true,
keepExisting: false, keepExisting: false,
userName: 'export-tester', auditUser: SYSTEM_USER_AUDIT,
userId: -9999,
}); });
const f = await app.services.featureTagService.listTags(featureName); const f = await app.services.featureTagService.listTags(featureName);
@ -341,8 +339,7 @@ test('Roundtrip with strategies in multiple environments works', async () => {
data, data,
dropBeforeImport: true, dropBeforeImport: true,
keepExisting: false, keepExisting: false,
userName: 'export-tester', auditUser: SYSTEM_USER_AUDIT,
userId: -9999,
}); });
const f = await app.services.featureToggleServiceV2.getFeature({ const f = await app.services.featureToggleServiceV2.getFeature({
featureName, featureName,
@ -423,8 +420,7 @@ test(`should not delete api_tokens on import when drop-flag is set`, async () =>
data, data,
dropBeforeImport: true, dropBeforeImport: true,
keepExisting: false, keepExisting: false,
userName: userName, auditUser: SYSTEM_USER_AUDIT,
userId: -9999,
}); });
const apiTokens = await app.services.apiTokenService.getAllTokens(); const apiTokens = await app.services.apiTokenService.getAllTokens();

View File

@ -4,7 +4,7 @@ import StateService from '../../../lib/services/state-service';
import oldFormat from '../../examples/variantsexport_v3.json'; import oldFormat from '../../examples/variantsexport_v3.json';
import { WeightType } from '../../../lib/types/model'; import { WeightType } from '../../../lib/types/model';
import { EventService } from '../../../lib/services'; import { EventService } from '../../../lib/services';
import type { IUnleashStores } from '../../../lib/types'; import { SYSTEM_USER_AUDIT, type IUnleashStores } from '../../../lib/types';
let stores: IUnleashStores; let stores: IUnleashStores;
let db: ITestDb; let db: ITestDb;
@ -143,7 +143,7 @@ test('Should import variants from old format and convert to new format (per envi
data: oldFormat, data: oldFormat,
keepExisting: false, keepExisting: false,
dropBeforeImport: true, dropBeforeImport: true,
userId: -9999, auditUser: SYSTEM_USER_AUDIT,
}); });
const featureEnvironments = await stores.featureEnvironmentStore.getAll(); const featureEnvironments = await stores.featureEnvironmentStore.getAll();
expect(featureEnvironments).toHaveLength(6); // There are 3 environments enabled and 2 features expect(featureEnvironments).toHaveLength(6); // There are 3 environments enabled and 2 features
@ -158,14 +158,14 @@ test('Should import variants in new format (per environment)', async () => {
data: oldFormat, data: oldFormat,
keepExisting: false, keepExisting: false,
dropBeforeImport: true, dropBeforeImport: true,
userId: -9999, auditUser: SYSTEM_USER_AUDIT,
}); });
const exportedJson = await stateService.export({}); const exportedJson = await stateService.export({});
await stateService.import({ await stateService.import({
data: exportedJson, data: exportedJson,
keepExisting: false, keepExisting: false,
dropBeforeImport: true, dropBeforeImport: true,
userId: -9999, auditUser: SYSTEM_USER_AUDIT,
}); });
const featureEnvironments = await stores.featureEnvironmentStore.getAll(); const featureEnvironments = await stores.featureEnvironmentStore.getAll();
expect(featureEnvironments).toHaveLength(6); // 3 environments, 2 features === 6 rows expect(featureEnvironments).toHaveLength(6); // 3 environments, 2 features === 6 rows
@ -190,10 +190,9 @@ test('Importing states with deprecated strategies should keep their deprecated s
}; };
await stateService.import({ await stateService.import({
data: deprecatedStrategyExample, data: deprecatedStrategyExample,
userName: 'strategy-importer',
dropBeforeImport: true, dropBeforeImport: true,
keepExisting: false, keepExisting: false,
userId: -9999, auditUser: SYSTEM_USER_AUDIT,
}); });
const deprecatedStrategy = const deprecatedStrategy =
await stores.strategyStore.get('deprecatedstrat'); await stores.strategyStore.get('deprecatedstrat');
@ -205,8 +204,7 @@ test('Exporting a deprecated strategy and then importing it should keep correct
data: oldFormat, data: oldFormat,
keepExisting: false, keepExisting: false,
dropBeforeImport: true, dropBeforeImport: true,
userName: 'strategy importer', auditUser: SYSTEM_USER_AUDIT,
userId: -9999,
}); });
const rolloutRandom = await stores.strategyStore.get( const rolloutRandom = await stores.strategyStore.get(
'gradualRolloutRandom', 'gradualRolloutRandom',