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:
parent
5f8b88aa0b
commit
25fdefc4d1
@ -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>,
|
||||
|
@ -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,
|
||||
|
@ -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",
|
||||
}
|
||||
`;
|
49
src/lib/openapi/spec/set-strategy-sort-order-schema.test.ts
Normal file
49
src/lib/openapi/spec/set-strategy-sort-order-schema.test.ts
Normal 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();
|
||||
});
|
24
src/lib/openapi/spec/set-strategy-sort-order-schema.ts
Normal file
24
src/lib/openapi/spec/set-strategy-sort-order-schema.ts
Normal 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
|
||||
>;
|
@ -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>,
|
||||
|
@ -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,
|
||||
|
@ -51,4 +51,5 @@ export interface IFeatureStrategiesStore
|
||||
newProjectId: string,
|
||||
): Promise<void>;
|
||||
getStrategiesBySegment(segmentId: number): Promise<IFeatureStrategy[]>;
|
||||
updateSortOrder(id: string, sortOrder: number): Promise<void>;
|
||||
}
|
||||
|
@ -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);
|
||||
});
|
||||
|
@ -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",
|
||||
|
@ -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.');
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user