mirror of
https://github.com/Unleash/unleash.git
synced 2025-08-13 13:48:59 +02:00
feat: default stickiness per project in settings (#3299)
<!-- Thanks for creating a PR! To make it easier for reviewers and everyone else to understand what your changes relate to, please add some relevant content to the headings below. Feel free to ignore or delete sections that you don't think are relevant. Thank you! ❤️ --> Introduces 2 new endpoints (behind flag `projectScopedStickiness` to set and get the setting ## About the changes <!-- Describe the changes introduced. What are they and why are they being introduced? Feel free to also add screenshots or steps to view the changes if they're visual. --> <!-- Does it close an issue? Multiple? --> Closes # <!-- (For internal contributors): Does it relate to an issue on public roadmap? --> <!-- Relates to [roadmap](https://github.com/orgs/Unleash/projects/10) item: # --> ### Important files <!-- PRs can contain a lot of changes, but not all changes are equally important. Where should a reviewer start looking to get an overview of the changes? Are any files particularly important? --> ## Discussion points <!-- Anything about the PR you'd like to discuss before it gets merged? Got any questions or doubts? --> --------- Signed-off-by: andreas-unleash <andreas@getunleash.ai>
This commit is contained in:
parent
c9d8710279
commit
de73fd3554
@ -131,6 +131,7 @@ import {
|
||||
importTogglesSchema,
|
||||
importTogglesValidateSchema,
|
||||
importTogglesValidateItemSchema,
|
||||
stickinessSchema,
|
||||
} from './spec';
|
||||
import { IServerOption } from '../types';
|
||||
import { mapValues, omitKeys } from '../util';
|
||||
@ -251,6 +252,7 @@ export const schemas = {
|
||||
stateSchema,
|
||||
strategiesSchema,
|
||||
strategySchema,
|
||||
stickinessSchema,
|
||||
tagsBulkAddSchema,
|
||||
tagSchema,
|
||||
tagsSchema,
|
||||
|
@ -129,4 +129,5 @@ export * from './project-overview-schema';
|
||||
export * from './import-toggles-validate-item-schema';
|
||||
export * from './import-toggles-validate-schema';
|
||||
export * from './import-toggles-schema';
|
||||
export * from './stickiness-schema';
|
||||
export * from './tags-bulk-add-schema';
|
||||
|
17
src/lib/openapi/spec/stickiness-schema.ts
Normal file
17
src/lib/openapi/spec/stickiness-schema.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { FromSchema } from 'json-schema-to-ts';
|
||||
|
||||
export const stickinessSchema = {
|
||||
$id: '#/components/schemas/stickinessSchema',
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
required: ['stickiness'],
|
||||
properties: {
|
||||
stickiness: {
|
||||
type: 'string',
|
||||
example: 'userId',
|
||||
},
|
||||
},
|
||||
components: {},
|
||||
} as const;
|
||||
|
||||
export type StickinessSchema = FromSchema<typeof stickinessSchema>;
|
@ -7,7 +7,7 @@ import EnvironmentsController from './environments';
|
||||
import ProjectHealthReport from './health-report';
|
||||
import ProjectService from '../../../services/project-service';
|
||||
import VariantsController from './variants';
|
||||
import { NONE } from '../../../types/permissions';
|
||||
import { NONE, UPDATE_PROJECT } from '../../../types/permissions';
|
||||
import {
|
||||
projectsSchema,
|
||||
ProjectsSchema,
|
||||
@ -17,21 +17,31 @@ import { serializeDates } from '../../../types/serialize-dates';
|
||||
import { createResponseSchema } from '../../../openapi/util/create-response-schema';
|
||||
import { IAuthRequest } from '../../unleash-types';
|
||||
import {
|
||||
emptyResponse,
|
||||
ProjectOverviewSchema,
|
||||
projectOverviewSchema,
|
||||
stickinessSchema,
|
||||
StickinessSchema,
|
||||
} from '../../../../lib/openapi';
|
||||
import { IArchivedQuery, IProjectParam } from '../../../types/model';
|
||||
import { ProjectApiTokenController } from './api-token';
|
||||
import { SettingService } from '../../../services';
|
||||
|
||||
const STICKINESS_KEY = 'stickiness';
|
||||
const DEFAULT_STICKINESS = 'default';
|
||||
|
||||
export default class ProjectApi extends Controller {
|
||||
private projectService: ProjectService;
|
||||
|
||||
private settingService: SettingService;
|
||||
|
||||
private openApiService: OpenApiService;
|
||||
|
||||
constructor(config: IUnleashConfig, services: IUnleashServices) {
|
||||
super(config);
|
||||
this.projectService = services.projectService;
|
||||
this.openApiService = services.openApiService;
|
||||
this.settingService = services.settingService;
|
||||
|
||||
this.route({
|
||||
path: '',
|
||||
@ -65,6 +75,40 @@ export default class ProjectApi extends Controller {
|
||||
],
|
||||
});
|
||||
|
||||
this.route({
|
||||
method: 'get',
|
||||
path: '/:projectId/stickiness',
|
||||
handler: this.getProjectDefaultStickiness,
|
||||
permission: NONE,
|
||||
middleware: [
|
||||
services.openApiService.validPath({
|
||||
tags: ['Projects'],
|
||||
operationId: 'getProjectDefaultStickiness',
|
||||
responses: {
|
||||
200: createResponseSchema('stickinessSchema'),
|
||||
404: emptyResponse,
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
this.route({
|
||||
method: 'post',
|
||||
path: '/:projectId/stickiness',
|
||||
handler: this.setProjectDefaultStickiness,
|
||||
permission: UPDATE_PROJECT,
|
||||
middleware: [
|
||||
services.openApiService.validPath({
|
||||
tags: ['Projects'],
|
||||
operationId: 'setProjectDefaultStickiness',
|
||||
responses: {
|
||||
200: createResponseSchema('stickinessSchema'),
|
||||
404: emptyResponse,
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
this.use('/', new ProjectFeaturesController(config, services).router);
|
||||
this.use('/', new EnvironmentsController(config, services).router);
|
||||
this.use('/', new ProjectHealthReport(config, services).router);
|
||||
@ -111,4 +155,63 @@ export default class ProjectApi extends Controller {
|
||||
serializeDates(overview),
|
||||
);
|
||||
}
|
||||
|
||||
async getProjectDefaultStickiness(
|
||||
req: IAuthRequest<IProjectParam, unknown, unknown, unknown>,
|
||||
res: Response<StickinessSchema>,
|
||||
): Promise<void> {
|
||||
if (!this.config.flagResolver.isEnabled('projectScopedStickiness')) {
|
||||
res.status(404);
|
||||
return Promise.resolve();
|
||||
}
|
||||
const { projectId } = req.params;
|
||||
const stickinessSettings = await this.settingService.get<object>(
|
||||
STICKINESS_KEY,
|
||||
{
|
||||
[projectId]: 'default',
|
||||
},
|
||||
);
|
||||
this.openApiService.respondWithValidation(
|
||||
200,
|
||||
res,
|
||||
stickinessSchema.$id,
|
||||
{ stickiness: stickinessSettings[projectId] },
|
||||
);
|
||||
}
|
||||
|
||||
async setProjectDefaultStickiness(
|
||||
req: IAuthRequest<
|
||||
IProjectParam,
|
||||
StickinessSchema,
|
||||
StickinessSchema,
|
||||
unknown
|
||||
>,
|
||||
res: Response<StickinessSchema>,
|
||||
): Promise<void> {
|
||||
if (!this.config.flagResolver.isEnabled('projectScopedStickiness')) {
|
||||
res.status(404);
|
||||
return Promise.resolve();
|
||||
}
|
||||
const { projectId } = req.params;
|
||||
const { stickiness } = req.body;
|
||||
const stickinessSettings = await this.settingService.get<{}>(
|
||||
STICKINESS_KEY,
|
||||
{
|
||||
[projectId]: DEFAULT_STICKINESS,
|
||||
},
|
||||
);
|
||||
stickinessSettings[projectId] = stickiness;
|
||||
await this.settingService.insert(
|
||||
STICKINESS_KEY,
|
||||
stickinessSettings,
|
||||
req.user.name,
|
||||
);
|
||||
|
||||
this.openApiService.respondWithValidation(
|
||||
200,
|
||||
res,
|
||||
stickinessSchema.$id,
|
||||
{ stickiness: stickiness },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -36,3 +36,27 @@ test('Should ONLY return default project', async () => {
|
||||
expect(body.projects).toHaveLength(1);
|
||||
expect(body.projects[0].id).toBe('default');
|
||||
});
|
||||
|
||||
test('Should store and retrieve default project stickiness', async () => {
|
||||
const appWithDefaultStickiness = await setupAppWithCustomConfig(db.stores, {
|
||||
experimental: {
|
||||
flags: {
|
||||
projectScopedStickiness: true,
|
||||
strictSchemaValidation: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
const reqBody = { stickiness: 'userId' };
|
||||
|
||||
await appWithDefaultStickiness.request
|
||||
.post('/api/admin/projects/default/stickiness')
|
||||
.send(reqBody)
|
||||
.expect(200);
|
||||
|
||||
const { body } = await appWithDefaultStickiness.request
|
||||
.get('/api/admin/projects/default/stickiness')
|
||||
.expect(200)
|
||||
.expect('Content-Type', /json/);
|
||||
|
||||
expect(body).toStrictEqual(reqBody);
|
||||
});
|
||||
|
@ -3504,6 +3504,19 @@ Stats are divided into current and previous **windows**.
|
||||
],
|
||||
"type": "object",
|
||||
},
|
||||
"stickinessSchema": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"stickiness": {
|
||||
"example": "userId",
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
"required": [
|
||||
"stickiness",
|
||||
],
|
||||
"type": "object",
|
||||
},
|
||||
"strategiesSchema": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
@ -7333,6 +7346,70 @@ If the provided project does not exist, the list of events will be empty.",
|
||||
],
|
||||
},
|
||||
},
|
||||
"/api/admin/projects/{projectId}/stickiness": {
|
||||
"get": {
|
||||
"operationId": "getProjectDefaultStickiness",
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
"name": "projectId",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/stickinessSchema",
|
||||
},
|
||||
},
|
||||
},
|
||||
"description": "stickinessSchema",
|
||||
},
|
||||
"404": {
|
||||
"description": "This response has no body.",
|
||||
},
|
||||
},
|
||||
"tags": [
|
||||
"Projects",
|
||||
],
|
||||
},
|
||||
"post": {
|
||||
"operationId": "setProjectDefaultStickiness",
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
"name": "projectId",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/stickinessSchema",
|
||||
},
|
||||
},
|
||||
},
|
||||
"description": "stickinessSchema",
|
||||
},
|
||||
"404": {
|
||||
"description": "This response has no body.",
|
||||
},
|
||||
},
|
||||
"tags": [
|
||||
"Projects",
|
||||
],
|
||||
},
|
||||
},
|
||||
"/api/admin/splash/{id}": {
|
||||
"post": {
|
||||
"operationId": "updateSplashSettings",
|
||||
|
Loading…
Reference in New Issue
Block a user