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

Strategy sort order endpoint (#1855)

* strategy sort order endpoint

Co-authored-by: Fredrik Strand Oseberg <fredrik.no@gmail.com>

* feat: add e2e test for happy path

* add tests to features strategies order

Co-authored-by: Fredrik Strand Oseberg <fredrik.no@gmail.com>

* feat: add tests for sort-order

* fix: update snapshot

* fix: lint

Co-authored-by: Fredrik Strand Oseberg <fredrik.no@gmail.com>
This commit is contained in:
Tymoteusz Czech 2022-07-26 14:16:30 +02:00 committed by GitHub
parent 5f8b88aa0b
commit 25fdefc4d1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 490 additions and 2 deletions

View File

@ -385,6 +385,12 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
throw new NotFoundError(`Could not find strategy with id: ${id}`);
}
async updateSortOrder(id: string, sortOrder: number): Promise<void> {
await this.db<IFeatureStrategiesTable>(T.featureStrategies)
.where({ id })
.update({ sort_order: sortOrder });
}
async updateStrategy(
id: string,
updates: Partial<IFeatureStrategy>,

View File

@ -62,8 +62,8 @@ import { patchesSchema } from './spec/patches-schema';
import { patchSchema } from './spec/patch-schema';
import { permissionSchema } from './spec/permission-schema';
import { playgroundFeatureSchema } from './spec/playground-feature-schema';
import { playgroundResponseSchema } from './spec/playground-response-schema';
import { playgroundRequestSchema } from './spec/playground-request-schema';
import { playgroundResponseSchema } from './spec/playground-response-schema';
import { projectEnvironmentSchema } from './spec/project-environment-schema';
import { projectSchema } from './spec/project-schema';
import { projectsSchema } from './spec/projects-schema';
@ -71,6 +71,7 @@ import { resetPasswordSchema } from './spec/reset-password-schema';
import { roleSchema } from './spec/role-schema';
import { sdkContextSchema } from './spec/sdk-context-schema';
import { segmentSchema } from './spec/segment-schema';
import { setStrategySortOrderSchema } from './spec/set-strategy-sort-order-schema';
import { sortOrderSchema } from './spec/sort-order-schema';
import { splashSchema } from './spec/splash-schema';
import { stateSchema } from './spec/state-schema';
@ -171,8 +172,8 @@ export const schemas = {
patchSchema,
permissionSchema,
playgroundFeatureSchema,
playgroundResponseSchema,
playgroundRequestSchema,
playgroundResponseSchema,
projectEnvironmentSchema,
projectSchema,
projectsSchema,
@ -180,6 +181,7 @@ export const schemas = {
roleSchema,
sdkContextSchema,
segmentSchema,
setStrategySortOrderSchema,
sortOrderSchema,
splashSchema,
stateSchema,

View File

@ -0,0 +1,69 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`setStrategySortOrderSchema missing id 1`] = `
Object {
"errors": Array [
Object {
"instancePath": "/0",
"keyword": "required",
"message": "must have required property 'id'",
"params": Object {
"missingProperty": "id",
},
"schemaPath": "#/items/required",
},
],
"schema": "#/components/schemas/setStrategySortOrderSchema",
}
`;
exports[`setStrategySortOrderSchema missing sortOrder 1`] = `
Object {
"errors": Array [
Object {
"instancePath": "/0",
"keyword": "required",
"message": "must have required property 'sortOrder'",
"params": Object {
"missingProperty": "sortOrder",
},
"schemaPath": "#/items/required",
},
],
"schema": "#/components/schemas/setStrategySortOrderSchema",
}
`;
exports[`setStrategySortOrderSchema no additional parameters 1`] = `
Object {
"errors": Array [
Object {
"instancePath": "/1",
"keyword": "additionalProperties",
"message": "must NOT have additional properties",
"params": Object {
"additionalProperty": "extra",
},
"schemaPath": "#/items/additionalProperties",
},
],
"schema": "#/components/schemas/setStrategySortOrderSchema",
}
`;
exports[`setStrategySortOrderSchema wrong sortOrder type 1`] = `
Object {
"errors": Array [
Object {
"instancePath": "/0/sortOrder",
"keyword": "type",
"message": "must be number",
"params": Object {
"type": "number",
},
"schemaPath": "#/items/properties/sortOrder/type",
},
],
"schema": "#/components/schemas/setStrategySortOrderSchema",
}
`;

View File

@ -0,0 +1,49 @@
import { validateSchema } from '../validate';
import { SetStrategySortOrderSchema } from './set-strategy-sort-order-schema';
test('setStrategySortOrderSchema', () => {
const data: SetStrategySortOrderSchema = [
{ id: 'strategy-1', sortOrder: 1 },
{ id: 'strategy-2', sortOrder: 2 },
{ id: 'strategy-3', sortOrder: 3 },
];
expect(
validateSchema('#/components/schemas/setStrategySortOrderSchema', data),
).toBeUndefined();
});
test('setStrategySortOrderSchema missing sortOrder', () => {
expect(
validateSchema('#/components/schemas/setStrategySortOrderSchema', [
{ id: 'strategy-1' },
]),
).toMatchSnapshot();
});
test('setStrategySortOrderSchema missing id', () => {
expect(
validateSchema('#/components/schemas/setStrategySortOrderSchema', [
{ sortOrder: 123 },
{ sortOrder: 7 },
]),
).toMatchSnapshot();
});
test('setStrategySortOrderSchema wrong sortOrder type', () => {
expect(
validateSchema('#/components/schemas/setStrategySortOrderSchema', [
{ id: 'strategy-1', sortOrder: 'test' },
]),
).toMatchSnapshot();
});
test('setStrategySortOrderSchema no additional parameters', () => {
expect(
validateSchema('#/components/schemas/setStrategySortOrderSchema', [
{ id: 'strategy-1', sortOrder: 1 },
{ id: 'strategy-2', sortOrder: 2, extra: 'test' },
{ id: 'strategy-3', sortOrder: 3 },
]),
).toMatchSnapshot();
});

View File

@ -0,0 +1,24 @@
import { FromSchema } from 'json-schema-to-ts';
export const setStrategySortOrderSchema = {
$id: '#/components/schemas/setStrategySortOrderSchema',
type: 'array',
items: {
type: 'object',
additionalProperties: false,
required: ['id', 'sortOrder'],
properties: {
id: {
type: 'string',
},
sortOrder: {
type: 'number',
},
},
},
components: {},
} as const;
export type SetStrategySortOrderSchema = FromSchema<
typeof setStrategySortOrderSchema
>;

View File

@ -36,6 +36,8 @@ import { OpenApiService } from '../../../services/openapi-service';
import { createRequestSchema } from '../../../openapi/util/create-request-schema';
import { createResponseSchema } from '../../../openapi/util/create-response-schema';
import { FeatureEnvironmentSchema } from '../../../openapi/spec/feature-environment-schema';
import { SetStrategySortOrderSchema } from '../../../openapi/spec/set-strategy-sort-order-schema';
import { emptyResponse } from '../../../openapi/util/standard-responses';
interface FeatureStrategyParams {
@ -182,6 +184,25 @@ export default class ProjectFeaturesController extends Controller {
],
});
this.route({
method: 'post',
path: `${PATH_STRATEGIES}/set-sort-order`,
handler: this.setStrategiesSortOrder,
permission: UPDATE_FEATURE_STRATEGY,
middleware: [
openApiService.validPath({
tags: ['admin'],
operationId: 'setStrategySortOrder',
requestBody: createRequestSchema(
'setStrategySortOrderSchema',
),
responses: {
200: emptyResponse,
},
}),
],
});
this.route({
method: 'put',
path: PATH_STRATEGY,
@ -217,6 +238,7 @@ export default class ProjectFeaturesController extends Controller {
}),
],
});
this.route({
method: 'delete',
path: PATH_STRATEGY,
@ -556,6 +578,24 @@ export default class ProjectFeaturesController extends Controller {
res.status(200).json(featureStrategies);
}
async setStrategiesSortOrder(
req: Request<
FeatureStrategyParams,
any,
SetStrategySortOrderSchema,
any
>,
res: Response,
): Promise<void> {
const { featureName } = req.params;
await this.featureService.updateStrategiesSortOrder(
featureName,
req.body,
);
res.status(200).send();
}
async updateFeatureStrategy(
req: IAuthRequest<StrategyIdParams, any, UpdateFeatureStrategySchema>,
res: Response<FeatureStrategySchema>,

View File

@ -70,6 +70,7 @@ import {
import { IContextFieldStore } from 'lib/types/stores/context-field-store';
import { Saved, Unsaved } from '../types/saved';
import { SegmentService } from './segment-service';
import { SetStrategySortOrderSchema } from 'lib/openapi/spec/set-strategy-sort-order-schema';
interface IFeatureContext {
featureName: string;
@ -282,6 +283,17 @@ class FeatureToggleService {
};
}
async updateStrategiesSortOrder(
featureName: string,
sortOrders: SetStrategySortOrderSchema,
): Promise<Saved<any>> {
await Promise.all(
sortOrders.map(({ id, sortOrder }) => {
this.featureStrategiesStore.updateSortOrder(id, sortOrder);
}),
);
}
async createStrategy(
strategyConfig: Unsaved<IStrategyConfig>,
context: IFeatureStrategyContext,

View File

@ -51,4 +51,5 @@ export interface IFeatureStrategiesStore
newProjectId: string,
): Promise<void>;
getStrategiesBySegment(segmentId: number): Promise<IFeatureStrategy[]>;
updateSortOrder(id: string, sortOrder: number): Promise<void>;
}

View File

@ -2276,3 +2276,210 @@ test('should allow long parameter values', async () => {
.send(strategy)
.expect(200);
});
test('should change strategy sort order when payload is valid', async () => {
const toggle = { name: uuidv4(), impressionData: false };
await app.request
.post('/api/admin/projects/default/features')
.send({
name: toggle.name,
})
.expect(201);
const { body: strategyOne } = await app.request
.post(
`/api/admin/projects/default/features/${toggle.name}/environments/default/strategies`,
)
.send({
name: 'default',
parameters: {
userId: 'string',
},
})
.expect(200);
const { body: strategyTwo } = await app.request
.post(
`/api/admin/projects/default/features/${toggle.name}/environments/default/strategies`,
)
.send({
name: 'gradualrollout',
parameters: {
userId: 'string',
},
})
.expect(200);
const { body: strategies } = await app.request.get(
`/api/admin/projects/default/features/${toggle.name}/environments/default/strategies`,
);
expect(strategies[0].sortOrder).toBe(9999);
expect(strategies[0].id).toBe(strategyOne.id);
expect(strategies[1].sortOrder).toBe(9999);
expect(strategies[1].id).toBe(strategyTwo.id);
await app.request
.post(
`/api/admin/projects/default/features/${toggle.name}/environments/default/strategies/set-sort-order`,
)
.send([
{
id: strategyOne.id,
sortOrder: 2,
},
{
id: strategyTwo.id,
sortOrder: 1,
},
])
.expect(200);
const { body: strategiesOrdered } = await app.request.get(
`/api/admin/projects/default/features/${toggle.name}/environments/default/strategies`,
);
expect(strategiesOrdered[0].sortOrder).toBe(1);
expect(strategiesOrdered[0].id).toBe(strategyTwo.id);
expect(strategiesOrdered[1].sortOrder).toBe(2);
expect(strategiesOrdered[1].id).toBe(strategyOne.id);
});
test('should reject set sort order request when payload is invalid', async () => {
const toggle = { name: uuidv4(), impressionData: false };
await app.request
.post(
`/api/admin/projects/default/features/${toggle.name}/environments/default/strategies/set-sort-order`,
)
.send([
{
id: '213141',
},
{
id: '341123',
},
])
.expect(400);
});
test('should return strategies in correct order when new strategies are added', async () => {
const toggle = { name: uuidv4(), impressionData: false };
await app.request
.post('/api/admin/projects/default/features')
.send({
name: toggle.name,
})
.expect(201);
const { body: strategyOne } = await app.request
.post(
`/api/admin/projects/default/features/${toggle.name}/environments/default/strategies`,
)
.send({
name: 'default',
parameters: {
userId: 'string',
},
})
.expect(200);
const { body: strategyTwo } = await app.request
.post(
`/api/admin/projects/default/features/${toggle.name}/environments/default/strategies`,
)
.send({
name: 'gradualrollout',
parameters: {
userId: 'string',
},
})
.expect(200);
const { body: strategies } = await app.request.get(
`/api/admin/projects/default/features/${toggle.name}/environments/default/strategies`,
);
expect(strategies[0].sortOrder).toBe(9999);
expect(strategies[0].id).toBe(strategyOne.id);
expect(strategies[1].sortOrder).toBe(9999);
expect(strategies[1].id).toBe(strategyTwo.id);
await app.request
.post(
`/api/admin/projects/default/features/${toggle.name}/environments/default/strategies/set-sort-order`,
)
.send([
{
id: strategyOne.id,
sortOrder: 2,
},
{
id: strategyTwo.id,
sortOrder: 1,
},
])
.expect(200);
const { body: strategyThree } = await app.request
.post(
`/api/admin/projects/default/features/${toggle.name}/environments/default/strategies`,
)
.send({
name: 'gradualrollout',
parameters: {
userId: 'string',
},
})
.expect(200);
const { body: strategyFour } = await app.request
.post(
`/api/admin/projects/default/features/${toggle.name}/environments/default/strategies`,
)
.send({
name: 'gradualrollout',
parameters: {
userId: 'string',
},
})
.expect(200);
const { body: strategiesOrdered } = await app.request.get(
`/api/admin/projects/default/features/${toggle.name}/environments/default/strategies`,
);
expect(strategiesOrdered[0].sortOrder).toBe(1);
expect(strategiesOrdered[0].id).toBe(strategyTwo.id);
expect(strategiesOrdered[1].sortOrder).toBe(2);
expect(strategiesOrdered[1].id).toBe(strategyOne.id);
expect(strategiesOrdered[2].id).toBe(strategyThree.id);
expect(strategiesOrdered[3].id).toBe(strategyFour.id);
await app.request
.post(
`/api/admin/projects/default/features/${toggle.name}/environments/default/strategies/set-sort-order`,
)
.send([
{
id: strategyFour.id,
sortOrder: 0,
},
])
.expect(200);
const { body: strategiesReOrdered } = await app.request.get(
`/api/admin/projects/default/features/${toggle.name}/environments/default/strategies`,
);
// This block checks the order of the strategies retrieved from the endpoint. After partial update, the order should
// change because the new element received a lower sort order than the previous objects.
expect(strategiesReOrdered[0].sortOrder).toBe(0);
expect(strategiesReOrdered[0].id).toBe(strategyFour.id);
expect(strategiesReOrdered[1].sortOrder).toBe(1);
expect(strategiesReOrdered[1].id).toBe(strategyTwo.id);
expect(strategiesReOrdered[2].sortOrder).toBe(2);
expect(strategiesReOrdered[2].id).toBe(strategyOne.id);
expect(strategiesReOrdered[3].sortOrder).toBe(9999);
expect(strategiesReOrdered[3].id).toBe(strategyThree.id);
});

View File

@ -2161,6 +2161,25 @@ Object {
],
"type": "object",
},
"setStrategySortOrderSchema": Object {
"items": Object {
"additionalProperties": false,
"properties": Object {
"id": Object {
"type": "string",
},
"sortOrder": Object {
"type": "number",
},
},
"required": Array [
"id",
"sortOrder",
],
"type": "object",
},
"type": "array",
},
"sortOrderSchema": Object {
"additionalProperties": Object {
"type": "number",
@ -4677,6 +4696,56 @@ If the provided project does not exist, the list of events will be empty.",
],
},
},
"/api/admin/projects/{projectId}/features/{featureName}/environments/{environment}/strategies/set-sort-order": Object {
"post": Object {
"operationId": "setStrategySortOrder",
"parameters": Array [
Object {
"in": "path",
"name": "projectId",
"required": true,
"schema": Object {
"type": "string",
},
},
Object {
"in": "path",
"name": "featureName",
"required": true,
"schema": Object {
"type": "string",
},
},
Object {
"in": "path",
"name": "environment",
"required": true,
"schema": Object {
"type": "string",
},
},
],
"requestBody": Object {
"content": Object {
"application/json": Object {
"schema": Object {
"$ref": "#/components/schemas/setStrategySortOrderSchema",
},
},
},
"description": "setStrategySortOrderSchema",
"required": true,
},
"responses": Object {
"200": Object {
"description": "This response has no body.",
},
},
"tags": Array [
"admin",
],
},
},
"/api/admin/projects/{projectId}/features/{featureName}/environments/{environment}/strategies/{strategyId}": Object {
"delete": Object {
"operationId": "deleteFeatureStrategy",

View File

@ -73,6 +73,15 @@ export default class FakeFeatureStrategiesStore
this.featureStrategies = [];
}
// FIXME: implement
async updateSortOrder(id: string, sortOrder: number): Promise<void> {
const found = this.featureStrategies.find((item) => item.id === id);
if (found) {
found.sortOrder = sortOrder;
}
}
destroy(): void {
throw new Error('Method not implemented.');
}