mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	Merge pull request #712 from Unleash/fix-711-conflict-on-existing-tag
Make feature-toggle-store return 409
This commit is contained in:
		
						commit
						8c6bc9f118
					
				@ -3,6 +3,8 @@
 | 
			
		||||
const metricsHelper = require('../metrics-helper');
 | 
			
		||||
const { DB_TIME } = require('../events');
 | 
			
		||||
const NotFoundError = require('../error/notfound-error');
 | 
			
		||||
const FeatureHasTagError = require('../error/feature-has-tag-error');
 | 
			
		||||
const { UNIQUE_CONSTRAINT_VIOLATION } = require('../error/db-error');
 | 
			
		||||
 | 
			
		||||
const FEATURE_COLUMNS = [
 | 
			
		||||
    'name',
 | 
			
		||||
@ -240,8 +242,15 @@ class FeatureToggleStore {
 | 
			
		||||
        const stopTimer = this.timer('tagFeature');
 | 
			
		||||
        await this.db(FEATURE_TAG_TABLE)
 | 
			
		||||
            .insert(this.featureAndTagToRow(featureName, tag))
 | 
			
		||||
            .onConflict(['feature_name', 'tag_type', 'tag_value'])
 | 
			
		||||
            .ignore();
 | 
			
		||||
            .catch(err => {
 | 
			
		||||
                if (err.code === UNIQUE_CONSTRAINT_VIOLATION) {
 | 
			
		||||
                    throw new FeatureHasTagError(
 | 
			
		||||
                        `${featureName} already had the tag: [${tag.type}:${tag.value}]`,
 | 
			
		||||
                    );
 | 
			
		||||
                } else {
 | 
			
		||||
                    throw err;
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
        stopTimer();
 | 
			
		||||
        return tag;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										3
									
								
								lib/error/db-error.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								lib/error/db-error.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,3 @@
 | 
			
		||||
module.exports = {
 | 
			
		||||
    UNIQUE_CONSTRAINT_VIOLATION: '23505',
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										26
									
								
								lib/error/feature-has-tag-error.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								lib/error/feature-has-tag-error.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,26 @@
 | 
			
		||||
'use strict';
 | 
			
		||||
 | 
			
		||||
class FeatureHasTagError extends Error {
 | 
			
		||||
    constructor(message) {
 | 
			
		||||
        super();
 | 
			
		||||
        Error.captureStackTrace(this, this.constructor);
 | 
			
		||||
 | 
			
		||||
        this.name = this.constructor.name;
 | 
			
		||||
        this.message = message;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    toJSON() {
 | 
			
		||||
        const obj = {
 | 
			
		||||
            isJoi: true,
 | 
			
		||||
            name: this.constructor.name,
 | 
			
		||||
            details: [
 | 
			
		||||
                {
 | 
			
		||||
                    message: this.message,
 | 
			
		||||
                },
 | 
			
		||||
            ],
 | 
			
		||||
        };
 | 
			
		||||
        return obj;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
module.exports = FeatureHasTagError;
 | 
			
		||||
@ -42,6 +42,11 @@ const handleErrors = (res, logger, error) => {
 | 
			
		||||
                .status(400)
 | 
			
		||||
                .json(error)
 | 
			
		||||
                .end();
 | 
			
		||||
        case 'FeatureHasTagError':
 | 
			
		||||
            return res
 | 
			
		||||
                .status(409)
 | 
			
		||||
                .json(error)
 | 
			
		||||
                .end();
 | 
			
		||||
        default:
 | 
			
		||||
            logger.error('Server failed executing request', error);
 | 
			
		||||
            return res.status(500).end();
 | 
			
		||||
 | 
			
		||||
@ -101,6 +101,7 @@
 | 
			
		||||
    "eslint-config-prettier": "^6.10.1",
 | 
			
		||||
    "eslint-plugin-import": "^2.20.2",
 | 
			
		||||
    "eslint-plugin-prettier": "^3.1.3",
 | 
			
		||||
    "faker": "^5.3.1",
 | 
			
		||||
    "fetch-mock": "^9.11.0",
 | 
			
		||||
    "husky": "^4.2.3",
 | 
			
		||||
    "lint-staged": "^10.0.7",
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,7 @@
 | 
			
		||||
'use strict';
 | 
			
		||||
 | 
			
		||||
const test = require('ava');
 | 
			
		||||
const faker = require('faker');
 | 
			
		||||
const dbInit = require('../../helpers/database-init');
 | 
			
		||||
const { setupApp } = require('../../helpers/test-helper');
 | 
			
		||||
const getLogger = require('../../../fixtures/no-logger');
 | 
			
		||||
@ -338,24 +339,25 @@ test.serial(
 | 
			
		||||
test.serial('can untag feature', async t => {
 | 
			
		||||
    t.plan(1);
 | 
			
		||||
    const request = await setupApp(stores);
 | 
			
		||||
    const feature1Name = faker.helpers.slugify(faker.lorem.words(3));
 | 
			
		||||
    await request.post('/api/admin/features').send({
 | 
			
		||||
        name: 'test.feature',
 | 
			
		||||
        name: feature1Name,
 | 
			
		||||
        type: 'killswitch',
 | 
			
		||||
        enabled: true,
 | 
			
		||||
        strategies: [{ name: 'default' }],
 | 
			
		||||
    });
 | 
			
		||||
    const tag = { value: 'TeamGreen', type: 'simple' };
 | 
			
		||||
    const tag = { value: faker.lorem.word(), type: 'simple' };
 | 
			
		||||
    await request
 | 
			
		||||
        .post('/api/admin/features/test.feature/tags')
 | 
			
		||||
        .post(`/api/admin/features/${feature1Name}/tags`)
 | 
			
		||||
        .send(tag)
 | 
			
		||||
        .expect(201);
 | 
			
		||||
    await request
 | 
			
		||||
        .delete(
 | 
			
		||||
            `/api/admin/features/test.feature/tags/${tag.type}/${tag.value}`,
 | 
			
		||||
            `/api/admin/features/${feature1Name}/tags/${tag.type}/${tag.value}`,
 | 
			
		||||
        )
 | 
			
		||||
        .expect(200);
 | 
			
		||||
    return request
 | 
			
		||||
        .get('/api/admin/features/test.feature/tags')
 | 
			
		||||
        .get(`/api/admin/features/${feature1Name}/tags`)
 | 
			
		||||
        .expect('Content-Type', /json/)
 | 
			
		||||
        .expect(200)
 | 
			
		||||
        .expect(res => {
 | 
			
		||||
@ -366,76 +368,86 @@ test.serial('can untag feature', async t => {
 | 
			
		||||
test.serial('Can get features tagged by tag', async t => {
 | 
			
		||||
    t.plan(2);
 | 
			
		||||
    const request = await setupApp(stores);
 | 
			
		||||
    const feature1Name = faker.helpers.slugify(faker.lorem.words(3));
 | 
			
		||||
    const feature2Name = faker.helpers.slugify(faker.lorem.words(3));
 | 
			
		||||
    await request.post('/api/admin/features').send({
 | 
			
		||||
        name: 'test.feature',
 | 
			
		||||
        name: feature1Name,
 | 
			
		||||
        type: 'killswitch',
 | 
			
		||||
        enabled: true,
 | 
			
		||||
        strategies: [{ name: 'default' }],
 | 
			
		||||
    });
 | 
			
		||||
    await request.post('/api/admin/features').send({
 | 
			
		||||
        name: 'test.feature2',
 | 
			
		||||
        name: feature2Name,
 | 
			
		||||
        type: 'killswitch',
 | 
			
		||||
        enabled: true,
 | 
			
		||||
        strategies: [{ name: 'default' }],
 | 
			
		||||
    });
 | 
			
		||||
    const tag = { value: 'Crazy', type: 'simple' };
 | 
			
		||||
    const tag = { value: faker.lorem.word(), type: 'simple' };
 | 
			
		||||
    await request
 | 
			
		||||
        .post('/api/admin/features/test.feature/tags')
 | 
			
		||||
        .post(`/api/admin/features/${feature1Name}/tags`)
 | 
			
		||||
        .send(tag)
 | 
			
		||||
        .expect(201);
 | 
			
		||||
    return request
 | 
			
		||||
        .get('/api/admin/features?tag=simple:Crazy')
 | 
			
		||||
        .get(`/api/admin/features?tag=${tag.type}:${tag.value}`)
 | 
			
		||||
        .expect('Content-Type', /json/)
 | 
			
		||||
        .expect(200)
 | 
			
		||||
        .expect(res => {
 | 
			
		||||
            t.is(res.body.features.length, 1);
 | 
			
		||||
            t.is(res.body.features[0].name, 'test.feature');
 | 
			
		||||
            t.is(res.body.features[0].name, feature1Name);
 | 
			
		||||
        });
 | 
			
		||||
});
 | 
			
		||||
test.serial('Can query for multiple tags using OR', async t => {
 | 
			
		||||
    t.plan(2);
 | 
			
		||||
    t.plan(3);
 | 
			
		||||
    const request = await setupApp(stores);
 | 
			
		||||
    const feature1Name = faker.helpers.slugify(faker.lorem.words(3));
 | 
			
		||||
    const feature2Name = faker.helpers.slugify(faker.lorem.words(3));
 | 
			
		||||
    await request.post('/api/admin/features').send({
 | 
			
		||||
        name: 'test.feature',
 | 
			
		||||
        name: feature1Name,
 | 
			
		||||
        type: 'killswitch',
 | 
			
		||||
        enabled: true,
 | 
			
		||||
        strategies: [{ name: 'default' }],
 | 
			
		||||
    });
 | 
			
		||||
    await request.post('/api/admin/features').send({
 | 
			
		||||
        name: 'test.feature2',
 | 
			
		||||
        name: feature2Name,
 | 
			
		||||
        type: 'killswitch',
 | 
			
		||||
        enabled: true,
 | 
			
		||||
        strategies: [{ name: 'default' }],
 | 
			
		||||
    });
 | 
			
		||||
    const tag = { value: 'Crazy', type: 'simple' };
 | 
			
		||||
    const tag2 = { value: 'tagb', type: 'simple' };
 | 
			
		||||
    const tag = { value: faker.name.firstName(), type: 'simple' };
 | 
			
		||||
    const tag2 = { value: faker.name.firstName(), type: 'simple' };
 | 
			
		||||
    await request
 | 
			
		||||
        .post('/api/admin/features/test.feature/tags')
 | 
			
		||||
        .post(`/api/admin/features/${feature1Name}/tags`)
 | 
			
		||||
        .send(tag)
 | 
			
		||||
        .expect(201);
 | 
			
		||||
    await request
 | 
			
		||||
        .post('/api/admin/features/test.feature2/tags')
 | 
			
		||||
        .post(`/api/admin/features/${feature2Name}/tags`)
 | 
			
		||||
        .send(tag2)
 | 
			
		||||
        .expect(201);
 | 
			
		||||
    return request
 | 
			
		||||
        .get('/api/admin/features?tag[]=simple:Crazy&tag[]=simple:tagb')
 | 
			
		||||
        .get(
 | 
			
		||||
            `/api/admin/features?tag[]=${tag.type}:${tag.value}&tag[]=${tag2.type}:${tag2.value}`,
 | 
			
		||||
        )
 | 
			
		||||
        .expect('Content-Type', /json/)
 | 
			
		||||
        .expect(200)
 | 
			
		||||
        .expect(res => {
 | 
			
		||||
            t.is(res.body.features.length, 2);
 | 
			
		||||
            t.is(res.body.features[0].name, 'test.feature');
 | 
			
		||||
            t.true(res.body.features.some(f => f.name === feature1Name));
 | 
			
		||||
            t.true(res.body.features.some(f => f.name === feature2Name));
 | 
			
		||||
        });
 | 
			
		||||
});
 | 
			
		||||
test.serial('Querying with multiple filters ANDs the filters', async t => {
 | 
			
		||||
    const request = await setupApp(stores);
 | 
			
		||||
    const feature1Name = `test.${faker.helpers.slugify(faker.hacker.phrase())}`;
 | 
			
		||||
    const feature2Name = faker.helpers.slugify(faker.lorem.words());
 | 
			
		||||
 | 
			
		||||
    await request.post('/api/admin/features').send({
 | 
			
		||||
        name: 'test.feature',
 | 
			
		||||
        name: feature1Name,
 | 
			
		||||
        type: 'killswitch',
 | 
			
		||||
        enabled: true,
 | 
			
		||||
        strategies: [{ name: 'default' }],
 | 
			
		||||
    });
 | 
			
		||||
    await request.post('/api/admin/features').send({
 | 
			
		||||
        name: 'test.feature2',
 | 
			
		||||
        name: feature2Name,
 | 
			
		||||
        type: 'killswitch',
 | 
			
		||||
        enabled: true,
 | 
			
		||||
        strategies: [{ name: 'default' }],
 | 
			
		||||
@ -446,14 +458,14 @@ test.serial('Querying with multiple filters ANDs the filters', async t => {
 | 
			
		||||
        enabled: true,
 | 
			
		||||
        strategies: [{ name: 'default' }],
 | 
			
		||||
    });
 | 
			
		||||
    const tag = { value: 'Crazy', type: 'simple' };
 | 
			
		||||
    const tag2 = { value: 'tagb', type: 'simple' };
 | 
			
		||||
    const tag = { value: faker.lorem.word(), type: 'simple' };
 | 
			
		||||
    const tag2 = { value: faker.name.firstName(), type: 'simple' };
 | 
			
		||||
    await request
 | 
			
		||||
        .post('/api/admin/features/test.feature/tags')
 | 
			
		||||
        .post(`/api/admin/features/${feature1Name}/tags`)
 | 
			
		||||
        .send(tag)
 | 
			
		||||
        .expect(201);
 | 
			
		||||
    await request
 | 
			
		||||
        .post('/api/admin/features/test.feature2/tags')
 | 
			
		||||
        .post(`/api/admin/features/${feature2Name}/tags`)
 | 
			
		||||
        .send(tag2)
 | 
			
		||||
        .expect(201);
 | 
			
		||||
    await request
 | 
			
		||||
@ -461,16 +473,48 @@ test.serial('Querying with multiple filters ANDs the filters', async t => {
 | 
			
		||||
        .send(tag)
 | 
			
		||||
        .expect(201);
 | 
			
		||||
    await request
 | 
			
		||||
        .get('/api/admin/features?tag=simple:Crazy')
 | 
			
		||||
        .get(`/api/admin/features?tag=${tag.type}:${tag.value}`)
 | 
			
		||||
        .expect('Content-Type', /json/)
 | 
			
		||||
        .expect(200)
 | 
			
		||||
        .expect(res => t.is(res.body.features.length, 2));
 | 
			
		||||
    await request
 | 
			
		||||
        .get('/api/admin/features?namePrefix=test&tag=simple:Crazy')
 | 
			
		||||
        .get(`/api/admin/features?namePrefix=test&tag=${tag.type}:${tag.value}`)
 | 
			
		||||
        .expect('Content-Type', /json/)
 | 
			
		||||
        .expect(200)
 | 
			
		||||
        .expect(res => {
 | 
			
		||||
            t.is(res.body.features.length, 1);
 | 
			
		||||
            t.is(res.body.features[0].name, 'test.feature');
 | 
			
		||||
            t.is(res.body.features[0].name, feature1Name);
 | 
			
		||||
        });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test.serial(
 | 
			
		||||
    'Tagging a feature with a tag it already has should return 409',
 | 
			
		||||
    async t => {
 | 
			
		||||
        const request = await setupApp(stores);
 | 
			
		||||
        const feature1Name = `test.${faker.helpers.slugify(
 | 
			
		||||
            faker.lorem.words(3),
 | 
			
		||||
        )}`;
 | 
			
		||||
        await request.post('/api/admin/features').send({
 | 
			
		||||
            name: feature1Name,
 | 
			
		||||
            type: 'killswitch',
 | 
			
		||||
            enabled: true,
 | 
			
		||||
            strategies: [{ name: 'default' }],
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        const tag = { value: faker.lorem.word(), type: 'simple' };
 | 
			
		||||
        await request
 | 
			
		||||
            .post(`/api/admin/features/${feature1Name}/tags`)
 | 
			
		||||
            .send(tag)
 | 
			
		||||
            .expect(201);
 | 
			
		||||
        return request
 | 
			
		||||
            .post(`/api/admin/features/${feature1Name}/tags`)
 | 
			
		||||
            .send(tag)
 | 
			
		||||
            .expect(409)
 | 
			
		||||
            .expect(res => {
 | 
			
		||||
                t.is(
 | 
			
		||||
                    res.body.details[0].message,
 | 
			
		||||
                    `${feature1Name} already had the tag: [${tag.type}:${tag.value}]`,
 | 
			
		||||
                );
 | 
			
		||||
            });
 | 
			
		||||
    },
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
@ -2284,6 +2284,11 @@ eyes@0.1.x:
 | 
			
		||||
  resolved "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz"
 | 
			
		||||
  integrity sha1-Ys8SAjTGg3hdkCNIqADvPgzCC8A=
 | 
			
		||||
 | 
			
		||||
faker@^5.3.1:
 | 
			
		||||
  version "5.3.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/faker/-/faker-5.3.1.tgz#67f8f5c170b97a76b875389e0e8b9155da7b4853"
 | 
			
		||||
  integrity sha512-sVdoApX/awJHO9DZHZsHVaJBNFiJW0n3lPs0q/nFxp/Mtya1dr2sCMktST3mdxNMHjkvKTTMAW488E+jH1eSbg==
 | 
			
		||||
 | 
			
		||||
fast-deep-equal@^3.1.1:
 | 
			
		||||
  version "3.1.3"
 | 
			
		||||
  resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz"
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user