1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-06-04 01:18:20 +02:00

Merge pull request #1095 from Unleash/feat/splash

create splash migration store and service file
This commit is contained in:
Youssef Khedher 2021-11-23 20:26:42 +01:00 committed by GitHub
commit 67f7969b84
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 488 additions and 1 deletions

View File

@ -29,6 +29,7 @@ import EnvironmentStore from './environment-store';
import FeatureTagStore from './feature-tag-store';
import { FeatureEnvironmentStore } from './feature-environment-store';
import { ClientMetricsStoreV2 } from './client-metrics-store-v2';
import UserSplashStore from './user-splash-store';
export const createStores = (
config: IUnleashConfig,
@ -85,6 +86,7 @@ export const createStores = (
eventBus,
getLogger,
),
userSplashStore: new UserSplashStore(db, eventBus, getLogger),
};
};

View File

@ -0,0 +1,105 @@
import { Knex } from 'knex';
import { EventEmitter } from 'events';
import { LogProvider, Logger } from '../logger';
import {
IUserSplash,
IUserSplashKey,
IUserSplashStore,
} from '../types/stores/user-splash-store';
const COLUMNS = ['user_id', 'splash_id', 'seen'];
const TABLE = 'user_splash';
interface IUserSplashTable {
seen?: boolean;
splash_id: string;
user_id: number;
}
const fieldToRow = (fields: IUserSplash): IUserSplashTable => ({
seen: fields.seen,
splash_id: fields.splashId,
user_id: fields.userId,
});
const rowToField = (row: IUserSplashTable): IUserSplash => ({
seen: row.seen,
splashId: row.splash_id,
userId: row.user_id,
});
export default class UserSplashStore implements IUserSplashStore {
private db: Knex;
private logger: Logger;
constructor(db: Knex, eventBus: EventEmitter, getLogger: LogProvider) {
this.db = db;
this.logger = getLogger('user-splash-store.ts');
}
async getAllUserSplashs(userId: number): Promise<IUserSplash[]> {
const userSplash = await this.db
.table<IUserSplashTable>(TABLE)
.select()
.where({ user_id: userId });
return userSplash.map(rowToField);
}
async getSplash(userId: number, splashId: string): Promise<IUserSplash> {
const userSplash = await this.db
.table<IUserSplashTable>(TABLE)
.select()
.where({ user_id: userId, splash_id: splashId })
.first();
return rowToField(userSplash);
}
async updateSplash(splash: IUserSplash): Promise<IUserSplash> {
const insertedSplash = await this.db
.table<IUserSplashTable>(TABLE)
.insert(fieldToRow(splash))
.onConflict(['user_id', 'splash_id'])
.merge()
.returning(COLUMNS);
return rowToField(insertedSplash[0]);
}
async delete({ userId, splashId }: IUserSplashKey): Promise<void> {
await this.db(TABLE)
.where({ user_id: userId, splash_id: splashId })
.del();
}
async deleteAll(): Promise<void> {
await this.db(TABLE).del();
}
destroy(): void {}
async exists({ userId, splashId }: IUserSplashKey): Promise<boolean> {
const result = await this.db.raw(
`SELECT EXISTS (SELECT 1 FROM ${TABLE} WHERE user_id = ? AND splash_id = ?) AS present`,
[userId, splashId],
);
const { present } = result.rows[0];
return present;
}
async get({ userId, splashId }: IUserSplashKey): Promise<IUserSplash> {
return this.getSplash(userId, splashId);
}
async getAll(): Promise<IUserSplash[]> {
const userSplashs = await this.db
.table<IUserSplashTable>(TABLE)
.select();
return userSplashs.map(rowToField);
}
}
module.exports = UserSplashStore;

View File

@ -21,6 +21,7 @@ import ApiTokenController from './api-token-controller';
import UserAdminController from './user-admin';
import EmailController from './email';
import UserFeedbackController from './user-feedback-controller';
import UserSplashController from './user-splash-controller';
import ProjectApi from './project';
import { EnvironmentsController } from './environments-controller';
@ -92,6 +93,10 @@ class AdminApi extends Controller {
'/environments',
new EnvironmentsController(config, services).router,
);
this.app.use(
'/splash',
new UserSplashController(config, services).router,
);
}
index(req, res) {

View File

@ -0,0 +1,49 @@
import { Response } from 'express';
import Controller from '../controller';
import { Logger } from '../../logger';
import { IUnleashConfig } from '../../types/option';
import { IUnleashServices } from '../../types/services';
import UserSplashService from '../../services/user-splash-service';
import { IAuthRequest } from '../unleash-types';
interface ISplashBody {
seen: boolean;
splashId: string;
}
class UserSplashController extends Controller {
private logger: Logger;
private userSplashService: UserSplashService;
constructor(
config: IUnleashConfig,
{ userSplashService }: Pick<IUnleashServices, 'userSplashService'>,
) {
super(config);
this.logger = config.getLogger('splash-controller.ts');
this.userSplashService = userSplashService;
this.post('/:id', this.updateSplashSettings);
}
private async updateSplashSettings(
req: IAuthRequest<any, any, ISplashBody, any>,
res: Response,
): Promise<void> {
const { user } = req;
const { id } = req.params;
const splash = {
splashId: id,
userId: user.id,
seen: true,
};
const updated = await this.userSplashService.updateSplash(splash);
res.json(updated);
}
}
module.exports = UserSplashController;
export default UserSplashController;

View File

@ -7,6 +7,7 @@ import { IUnleashServices } from '../../types/services';
import UserService from '../../services/user-service';
import SessionService from '../../services/session-service';
import UserFeedbackService from '../../services/user-feedback-service';
import UserSplashService from '../../services/user-splash-service';
interface IChangeUserRequest {
password: string;
@ -22,6 +23,8 @@ class UserController extends Controller {
private sessionService: SessionService;
private userSplashService: UserSplashService;
constructor(
config: IUnleashConfig,
{
@ -29,12 +32,14 @@ class UserController extends Controller {
userService,
sessionService,
userFeedbackService,
userSplashService,
}: Pick<
IUnleashServices,
| 'accessService'
| 'userService'
| 'sessionService'
| 'userFeedbackService'
| 'userSplashService'
>,
) {
super(config);
@ -42,6 +47,7 @@ class UserController extends Controller {
this.userService = userService;
this.sessionService = sessionService;
this.userFeedbackService = userFeedbackService;
this.userSplashService = userSplashService;
this.get('/', this.getUser);
this.post('/change-password', this.updateUserPass);
@ -57,11 +63,15 @@ class UserController extends Controller {
const feedback = await this.userFeedbackService.getAllUserFeedback(
user,
);
const splash = await this.userSplashService.getAllUserSplashs(user);
// TODO: remove this line after we remove it from db.
delete user.permissions;
return res.status(200).json({ user, permissions, feedback }).end();
return res
.status(200)
.json({ user, permissions, feedback, splash })
.end();
}
async updateUserPass(

View File

@ -27,6 +27,7 @@ import FeatureToggleService from './feature-toggle-service';
import EnvironmentService from './environment-service';
import FeatureTagService from './feature-tag-service';
import ProjectHealthService from './project-health-service';
import UserSplashService from './user-splash-service';
export const createServices = (
stores: IUnleashStores,
@ -72,6 +73,7 @@ export const createServices = (
accessService,
featureToggleServiceV2,
);
const userSplashService = new UserSplashService(stores, config);
return {
accessService,
@ -100,6 +102,7 @@ export const createServices = (
userFeedbackService,
featureTagService,
projectHealthService,
userSplashService,
};
};

View File

@ -0,0 +1,54 @@
import { Logger } from '../logger';
import { IUnleashStores } from '../types/stores';
import { IUnleashConfig } from '../types/option';
import User from '../types/user';
import {
IUserSplash,
IUserSplashStore,
} from '../types/stores/user-splash-store';
export default class UserSplashService {
private userSplashStore: IUserSplashStore;
private logger: Logger;
constructor(
{ userSplashStore }: Pick<IUnleashStores, 'userSplashStore'>,
{ getLogger }: Pick<IUnleashConfig, 'getLogger'>,
) {
this.userSplashStore = userSplashStore;
this.logger = getLogger('services/user-splash-service.js');
}
async getAllUserSplashs(user: User): Promise<Object> {
if (user.isAPI) {
return [];
}
try {
const splashs = (
await this.userSplashStore.getAllUserSplashs(user.id)
).reduce(
(splashObject, splash) => ({
...splashObject,
[splash.splashId]: splash.seen,
}),
{},
);
return splashs;
} catch (err) {
this.logger.error(err);
return {};
}
}
async getSplash(user_id: number, splash_id: string): Promise<IUserSplash> {
return this.userSplashStore.getSplash(user_id, splash_id);
}
async updateSplash(splash: IUserSplash): Promise<IUserSplash> {
return this.userSplashStore.updateSplash(splash);
}
}
module.exports = UserSplashService;

View File

@ -23,6 +23,7 @@ import EnvironmentService from '../services/environment-service';
import FeatureTagService from '../services/feature-tag-service';
import ProjectHealthService from '../services/project-health-service';
import ClientMetricsServiceV2 from '../services/client-metrics/client-metrics-service-v2';
import UserSplashService from '../services/user-splash-service';
export interface IUnleashServices {
accessService: AccessService;
@ -51,4 +52,5 @@ export interface IUnleashServices {
userFeedbackService: UserFeedbackService;
userService: UserService;
versionService: VersionService;
userSplashService: UserSplashService;
}

View File

@ -23,6 +23,7 @@ import { IFeatureStrategiesStore } from './stores/feature-strategies-store';
import { IEnvironmentStore } from './stores/environment-store';
import { IFeatureToggleClientStore } from './stores/feature-toggle-client-store';
import { IClientMetricsStoreV2 } from './stores/client-metrics-store-v2';
import { IUserSplashStore } from './stores/user-splash-store';
export interface IUnleashStores {
accessStore: IAccessStore;
@ -50,4 +51,5 @@ export interface IUnleashStores {
tagTypeStore: ITagTypeStore;
userFeedbackStore: IUserFeedbackStore;
userStore: IUserStore;
userSplashStore: IUserSplashStore;
}

View File

@ -0,0 +1,18 @@
import { Store } from './store';
export interface IUserSplash {
seen: boolean;
splashId: string;
userId: number;
}
export interface IUserSplashKey {
userId: number;
splashId: string;
}
export interface IUserSplashStore extends Store<IUserSplash, IUserSplashKey> {
getAllUserSplashs(userId: number): Promise<IUserSplash[]>;
getSplash(userId: number, splashId: string): Promise<IUserSplash>;
updateSplash(splash: IUserSplash): Promise<IUserSplash>;
}

View File

@ -0,0 +1,25 @@
'use strict';
exports.up = function (db, cb) {
db.runSql(
`
CREATE TABLE IF NOT EXISTS user_splash
(user_id INTEGER NOT NULL references users (id) ON DELETE CASCADE,
splash_id TEXT,
seen BOOLEAN NOT NULL DEFAULT false,
PRIMARY KEY (user_id, splash_id));
CREATE INDEX user_splash_user_id_idx ON user_splash (user_id);
`,
cb,
);
};
exports.down = function (db, cb) {
db.runSql(
`
DROP INDEX user_splash_user_id_idx;
DROP TABLE user_splash;
`,
cb,
);
};

View File

@ -0,0 +1,10 @@
exports.up = function (db, cb) {
db.runSql(
`INSERT INTO user_splash(splash_id, user_id, seen) SELECT 'environment', u.id, false FROM users u`,
cb,
);
};
exports.down = function (db, cb) {
db.runSql('DELETE FROM user_splash', cb);
};

View File

@ -0,0 +1,68 @@
import { Application, NextFunction, Request, Response } from 'express';
import { setupAppWithCustomAuth } from '../../helpers/test-helper';
import dbInit from '../../helpers/database-init';
import getLogger from '../../../fixtures/no-logger';
import { IUnleashConfig } from '../../../../lib/types/option';
import { IUnleashServices } from '../../../../lib/types/services';
let stores;
let db;
let app;
beforeAll(async () => {
db = await dbInit('splash_api_serial', getLogger);
stores = db.stores;
const email = 'custom-user@mail.com';
const preHook = (
application: Application,
config: IUnleashConfig,
{ userService }: IUnleashServices,
) => {
application.use(
'/api/admin/',
async (req: Request, res: Response, next: NextFunction) => {
// @ts-ignore
req.user = await userService.loginUserWithoutPassword(
email,
true,
);
next();
},
);
};
app = await setupAppWithCustomAuth(stores, preHook);
});
afterAll(async () => {
await app.destroy();
await db.destroy();
});
test('it updates splash for user', async () => {
expect.assertions(1);
return app.request
.post('/api/admin/splash/environment')
.set('Content-Type', 'application/json')
.expect('Content-Type', /json/)
.expect(200)
.expect((res) => {
expect(res.body.seen).toBe(true);
});
});
test('it retrieves splash for user', async () => {
expect.assertions(1);
return app.request
.get('/api/admin/user')
.set('Content-Type', 'application/json')
.expect('Content-Type', /json/)
.expect(200)
.expect((res) => {
expect(res.body.splash).toStrictEqual({ environment: true });
});
});

View File

@ -0,0 +1,82 @@
import { IUserSplashStore } from 'lib/types/stores/user-splash-store';
import { IUserStore } from 'lib/types/stores/user-store';
import dbInit from '../helpers/database-init';
import getLogger from '../../fixtures/no-logger';
let stores;
let db;
let userSplashStore: IUserSplashStore;
let userStore: IUserStore;
let currentUser;
beforeAll(async () => {
db = await dbInit('user_splash_store', getLogger);
stores = db.stores;
userSplashStore = stores.userSplashStore;
userStore = stores.userStore;
currentUser = await userStore.upsert({ email: 'me.feedback@mail.com' });
});
afterAll(async () => {
await db.destroy();
});
afterEach(async () => {
await userSplashStore.deleteAll();
});
test('should create userSplash', async () => {
await userSplashStore.updateSplash({
splashId: 'some-id',
userId: currentUser.id,
seen: false,
});
const userSplashs = await userSplashStore.getAllUserSplashs(currentUser.id);
expect(userSplashs).toHaveLength(1);
expect(userSplashs[0].splashId).toBe('some-id');
});
test('should get userSplash', async () => {
await userSplashStore.updateSplash({
splashId: 'some-id',
userId: currentUser.id,
seen: false,
});
const userSplash = await userSplashStore.getSplash(
currentUser.id,
'some-id',
);
expect(userSplash.splashId).toBe('some-id');
});
test('should exists', async () => {
await userSplashStore.updateSplash({
splashId: 'some-id-3',
userId: currentUser.id,
seen: false,
});
const exists = await userSplashStore.exists({
userId: currentUser.id,
splashId: 'some-id-3',
});
expect(exists).toBe(true);
});
test('should not exists', async () => {
const exists = await userSplashStore.exists({
userId: currentUser.id,
splashId: 'some-id-not-here',
});
expect(exists).toBe(false);
});
test('should get all userSplashs', async () => {
await userSplashStore.updateSplash({
splashId: 'some-id-2',
userId: currentUser.id,
seen: false,
});
const userSplashs = await userSplashStore.getAll();
expect(userSplashs).toHaveLength(1);
expect(userSplashs[0].splashId).toBe('some-id-2');
});

View File

@ -0,0 +1,50 @@
import {
IUserSplashKey,
IUserSplash,
IUserSplashStore,
} from '../../lib/types/stores/user-splash-store';
export default class FakeUserSplashStore implements IUserSplashStore {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
getAllUserSplashs(userId: number): Promise<IUserSplash[]> {
return Promise.resolve([]);
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
getSplash(userId: number, splashId: string): Promise<IUserSplash> {
return Promise.resolve(undefined);
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
updateSplash(splash: IUserSplash): Promise<IUserSplash> {
return Promise.resolve(undefined);
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
exists(key: IUserSplashKey): Promise<boolean> {
return Promise.resolve(false);
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
get(key: IUserSplashKey): Promise<IUserSplash> {
return Promise.resolve(undefined);
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
getAll(): Promise<IUserSplash[]> {
return Promise.resolve([]);
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
delete(key: IUserSplashKey): Promise<void> {
return Promise.resolve(undefined);
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
deleteAll(): Promise<void> {
return Promise.resolve(undefined);
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
destroy(): void {}
}

View File

@ -24,6 +24,7 @@ import FakeFeatureTypeStore from './fake-feature-type-store';
import FakeResetTokenStore from './fake-reset-token-store';
import FakeFeatureToggleClientStore from './fake-feature-toggle-client-store';
import FakeClientMetricsStoreV2 from './fake-client-metrics-store-v2';
import FakeUserSplashStore from './fake-user-splash-store';
const createStores: () => IUnleashStores = () => {
const db = {
@ -59,6 +60,7 @@ const createStores: () => IUnleashStores = () => {
featureTypeStore: new FakeFeatureTypeStore(),
resetTokenStore: new FakeResetTokenStore(),
sessionStore: new FakeSessionStore(),
userSplashStore: new FakeUserSplashStore(),
};
};