mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	fix: reject duplicate segment names (#1475)
* fix: reject duplicate segment names * refactor: remove unnecessary comment * refactor: improve validateName logic with existsByName * fix: removed unused NotFoundError import
This commit is contained in:
		
							parent
							
								
									a96cb0cc6c
								
							
						
					
					
						commit
						794327f8e0
					
				@ -170,6 +170,15 @@ export default class SegmentStore implements ISegmentStore {
 | 
				
			|||||||
        }));
 | 
					        }));
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async existsByName(name: string): Promise<boolean> {
 | 
				
			||||||
 | 
					        const rows: ISegmentRow[] = await this.db
 | 
				
			||||||
 | 
					            .select(this.prefixColumns())
 | 
				
			||||||
 | 
					            .from(T.segments)
 | 
				
			||||||
 | 
					            .where({ name });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return Boolean(rows[0]);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    prefixColumns(): string[] {
 | 
					    prefixColumns(): string[] {
 | 
				
			||||||
        return COLUMNS.map((c) => `${T.segments}.${c}`);
 | 
					        return COLUMNS.map((c) => `${T.segments}.${c}`);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
				
			|||||||
@ -2,6 +2,7 @@ import { IUnleashConfig } from '../types/option';
 | 
				
			|||||||
import { IEventStore } from '../types/stores/event-store';
 | 
					import { IEventStore } from '../types/stores/event-store';
 | 
				
			||||||
import { IUnleashStores } from '../types';
 | 
					import { IUnleashStores } from '../types';
 | 
				
			||||||
import { Logger } from '../logger';
 | 
					import { Logger } from '../logger';
 | 
				
			||||||
 | 
					import NameExistsError from '../error/name-exists-error';
 | 
				
			||||||
import { ISegmentStore } from '../types/stores/segment-store';
 | 
					import { ISegmentStore } from '../types/stores/segment-store';
 | 
				
			||||||
import { IFeatureStrategy, ISegment } from '../types/model';
 | 
					import { IFeatureStrategy, ISegment } from '../types/model';
 | 
				
			||||||
import { segmentSchema } from './segment-schema';
 | 
					import { segmentSchema } from './segment-schema';
 | 
				
			||||||
@ -69,6 +70,7 @@ export class SegmentService {
 | 
				
			|||||||
    async create(data: unknown, user: User): Promise<void> {
 | 
					    async create(data: unknown, user: User): Promise<void> {
 | 
				
			||||||
        const input = await segmentSchema.validateAsync(data);
 | 
					        const input = await segmentSchema.validateAsync(data);
 | 
				
			||||||
        this.validateSegmentValuesLimit(input);
 | 
					        this.validateSegmentValuesLimit(input);
 | 
				
			||||||
 | 
					        await this.validateName(input.name);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const segment = await this.segmentStore.create(input, user);
 | 
					        const segment = await this.segmentStore.create(input, user);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -82,8 +84,12 @@ export class SegmentService {
 | 
				
			|||||||
    async update(id: number, data: unknown, user: User): Promise<void> {
 | 
					    async update(id: number, data: unknown, user: User): Promise<void> {
 | 
				
			||||||
        const input = await segmentSchema.validateAsync(data);
 | 
					        const input = await segmentSchema.validateAsync(data);
 | 
				
			||||||
        this.validateSegmentValuesLimit(input);
 | 
					        this.validateSegmentValuesLimit(input);
 | 
				
			||||||
 | 
					 | 
				
			||||||
        const preData = await this.segmentStore.get(id);
 | 
					        const preData = await this.segmentStore.get(id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (preData.name !== input.name) {
 | 
				
			||||||
 | 
					            await this.validateName(input.name);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const segment = await this.segmentStore.update(id, input);
 | 
					        const segment = await this.segmentStore.update(id, input);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        await this.eventStore.store({
 | 
					        await this.eventStore.store({
 | 
				
			||||||
@ -115,6 +121,16 @@ export class SegmentService {
 | 
				
			|||||||
        await this.segmentStore.removeFromStrategy(id, strategyId);
 | 
					        await this.segmentStore.removeFromStrategy(id, strategyId);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async validateName(name: string): Promise<void> {
 | 
				
			||||||
 | 
					        if (!name) {
 | 
				
			||||||
 | 
					            throw new BadDataError('Segment name cannot be empty');
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (await this.segmentStore.existsByName(name)) {
 | 
				
			||||||
 | 
					            throw new NameExistsError('Segment name already exists');
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private async validateStrategySegmentLimit(
 | 
					    private async validateStrategySegmentLimit(
 | 
				
			||||||
        strategyId: string,
 | 
					        strategyId: string,
 | 
				
			||||||
    ): Promise<void> {
 | 
					    ): Promise<void> {
 | 
				
			||||||
 | 
				
			|||||||
@ -23,4 +23,6 @@ export interface ISegmentStore extends Store<ISegment, number> {
 | 
				
			|||||||
    removeFromStrategy(id: number, strategyId: string): Promise<void>;
 | 
					    removeFromStrategy(id: number, strategyId: string): Promise<void>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    getAllFeatureStrategySegments(): Promise<IFeatureStrategySegment[]>;
 | 
					    getAllFeatureStrategySegments(): Promise<IFeatureStrategySegment[]>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    existsByName(name: string): Promise<boolean>;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										19
									
								
								src/migrations/20220405103233-add-segments-name-index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								src/migrations/20220405103233-add-segments-name-index.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,19 @@
 | 
				
			|||||||
 | 
					'use strict';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					exports.up = function (db, cb) {
 | 
				
			||||||
 | 
					    db.runSql(
 | 
				
			||||||
 | 
					        `
 | 
				
			||||||
 | 
					        create index segments_name_index on segments (name);
 | 
				
			||||||
 | 
					        `,
 | 
				
			||||||
 | 
					        cb,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					exports.down = function (db, cb) {
 | 
				
			||||||
 | 
					    db.runSql(
 | 
				
			||||||
 | 
					        `
 | 
				
			||||||
 | 
					        drop index segments_name_index;
 | 
				
			||||||
 | 
					        `,
 | 
				
			||||||
 | 
					        cb,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										4
									
								
								src/test/fixtures/fake-segment-store.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								src/test/fixtures/fake-segment-store.ts
									
									
									
									
										vendored
									
									
								
							@ -50,5 +50,9 @@ export default class FakeSegmentStore implements ISegmentStore {
 | 
				
			|||||||
        return [];
 | 
					        return [];
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async existsByName(): Promise<boolean> {
 | 
				
			||||||
 | 
					        throw new Error('Method not implemented.');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    destroy(): void {}
 | 
					    destroy(): void {}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
		Reference in New Issue
	
	Block a user