mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	feature: add query support to features endpoint (#2693)
## About the changes The deprecated /api/admin/features endpoint supported querying with tag and namePrefix parameters. This PR adds this functionality to /api/admin/projects/<project>/features as well, allowing to replicate queries that used to work. Closes #2306 ### Important files src/lib/db/feature-strategy-store.ts src/test/e2e/stores/feature-strategies-store.e2e.test.ts ## Discussion points I'm extending our query parameters support for /api/admin/projects/<projectId>/features endpoint. This will be reflected in our open-api spec, so I also made an adminFeaturesQuerySchema for this. Also, very open for something similar to what we did for the modifyQuery for the archived parameter, but couldn't come up with a good way to support subselects using the query builder, it just ended up blowing the stack. If anyone has a suggestion, I'm all ears. Co-authored-by: Thomas Heartman <thomas@getunleash.ai>
This commit is contained in:
		
							parent
							
								
									1d1219a055
								
							
						
					
					
						commit
						eafba10cac
					
				@ -414,9 +414,25 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
 | 
				
			|||||||
        projectId,
 | 
					        projectId,
 | 
				
			||||||
        archived,
 | 
					        archived,
 | 
				
			||||||
        userId,
 | 
					        userId,
 | 
				
			||||||
 | 
					        tag,
 | 
				
			||||||
 | 
					        namePrefix,
 | 
				
			||||||
    }: IFeatureProjectUserParams): Promise<IFeatureOverview[]> {
 | 
					    }: IFeatureProjectUserParams): Promise<IFeatureOverview[]> {
 | 
				
			||||||
        let query = this.db('features')
 | 
					        let query = this.db('features').where({ project: projectId });
 | 
				
			||||||
            .where({ project: projectId })
 | 
					        if (tag) {
 | 
				
			||||||
 | 
					            const tagQuery = this.db
 | 
				
			||||||
 | 
					                .from('feature_tag')
 | 
				
			||||||
 | 
					                .select('feature_name')
 | 
				
			||||||
 | 
					                .whereIn(['tag_type', 'tag_value'], tag);
 | 
				
			||||||
 | 
					            query = query.whereIn('features.name', tagQuery);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if (namePrefix && namePrefix.trim()) {
 | 
				
			||||||
 | 
					            let namePrefixQuery = namePrefix;
 | 
				
			||||||
 | 
					            if (!namePrefix.endsWith('%')) {
 | 
				
			||||||
 | 
					                namePrefixQuery = namePrefixQuery + '%';
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            query = query.whereILike('features.name', namePrefixQuery);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        query = query
 | 
				
			||||||
            .modify(FeatureToggleStore.filterByArchived, archived)
 | 
					            .modify(FeatureToggleStore.filterByArchived, archived)
 | 
				
			||||||
            .leftJoin(
 | 
					            .leftJoin(
 | 
				
			||||||
                'feature_environments',
 | 
					                'feature_environments',
 | 
				
			||||||
@ -461,7 +477,6 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        query = query.select(selectColumns);
 | 
					        query = query.select(selectColumns);
 | 
				
			||||||
 | 
					 | 
				
			||||||
        const rows = await query;
 | 
					        const rows = await query;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (rows.length > 0) {
 | 
					        if (rows.length > 0) {
 | 
				
			||||||
 | 
				
			|||||||
@ -1,5 +1,6 @@
 | 
				
			|||||||
import { OpenAPIV3 } from 'openapi-types';
 | 
					import { OpenAPIV3 } from 'openapi-types';
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
 | 
					    adminFeaturesQuerySchema,
 | 
				
			||||||
    addonParameterSchema,
 | 
					    addonParameterSchema,
 | 
				
			||||||
    addonSchema,
 | 
					    addonSchema,
 | 
				
			||||||
    addonsSchema,
 | 
					    addonsSchema,
 | 
				
			||||||
@ -131,6 +132,7 @@ import apiVersion from '../util/version';
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// All schemas in `openapi/spec` should be listed here.
 | 
					// All schemas in `openapi/spec` should be listed here.
 | 
				
			||||||
export const schemas = {
 | 
					export const schemas = {
 | 
				
			||||||
 | 
					    adminFeaturesQuerySchema,
 | 
				
			||||||
    addonParameterSchema,
 | 
					    addonParameterSchema,
 | 
				
			||||||
    addonSchema,
 | 
					    addonSchema,
 | 
				
			||||||
    addonsSchema,
 | 
					    addonsSchema,
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										31
									
								
								src/lib/openapi/spec/admin-features-query-schema.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								src/lib/openapi/spec/admin-features-query-schema.test.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,31 @@
 | 
				
			|||||||
 | 
					import { validateSchema } from '../validate';
 | 
				
			||||||
 | 
					import { AdminFeaturesQuerySchema } from './admin-features-query-schema';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					test('adminFeaturesQuerySchema empty', () => {
 | 
				
			||||||
 | 
					    const data: AdminFeaturesQuerySchema = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    expect(
 | 
				
			||||||
 | 
					        validateSchema('#/components/schemas/adminFeaturesQuerySchema', data),
 | 
				
			||||||
 | 
					    ).toBeUndefined();
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					test('adminFeatureQuerySchema all fields', () => {
 | 
				
			||||||
 | 
					    const data: AdminFeaturesQuerySchema = {
 | 
				
			||||||
 | 
					        tag: ['simple:some-tag', 'simple:some-other-tag'],
 | 
				
			||||||
 | 
					        namePrefix: 'some-prefix',
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    expect(
 | 
				
			||||||
 | 
					        validateSchema('#/components/schemas/adminFeaturesQuerySchema', data),
 | 
				
			||||||
 | 
					    ).toBeUndefined();
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					test('pattern validation should deny invalid tags', () => {
 | 
				
			||||||
 | 
					    const data: AdminFeaturesQuerySchema = {
 | 
				
			||||||
 | 
					        tag: ['something', 'somethingelse'],
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    expect(
 | 
				
			||||||
 | 
					        validateSchema('#/components/schemas/adminFeaturesQuerySchema', data),
 | 
				
			||||||
 | 
					    ).toBeDefined();
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
							
								
								
									
										30
									
								
								src/lib/openapi/spec/admin-features-query-schema.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								src/lib/openapi/spec/admin-features-query-schema.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,30 @@
 | 
				
			|||||||
 | 
					import { FromSchema } from 'json-schema-to-ts';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const adminFeaturesQuerySchema = {
 | 
				
			||||||
 | 
					    $id: '#/components/schemas/adminFeaturesQuerySchema',
 | 
				
			||||||
 | 
					    type: 'object',
 | 
				
			||||||
 | 
					    additionalProperties: false,
 | 
				
			||||||
 | 
					    properties: {
 | 
				
			||||||
 | 
					        tag: {
 | 
				
			||||||
 | 
					            type: 'array',
 | 
				
			||||||
 | 
					            items: {
 | 
				
			||||||
 | 
					                type: 'string',
 | 
				
			||||||
 | 
					                pattern: '\\w+:\\w+',
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            description:
 | 
				
			||||||
 | 
					                'Used to filter by tags. For each entry, a TAGTYPE:TAGVALUE is expected',
 | 
				
			||||||
 | 
					            example: ['simple:mytag'],
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        namePrefix: {
 | 
				
			||||||
 | 
					            type: 'string',
 | 
				
			||||||
 | 
					            description:
 | 
				
			||||||
 | 
					                'A case-insensitive prefix filter for the names of feature toggles',
 | 
				
			||||||
 | 
					            example: 'demo.part1',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    components: {},
 | 
				
			||||||
 | 
					} as const;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type AdminFeaturesQuerySchema = FromSchema<
 | 
				
			||||||
 | 
					    typeof adminFeaturesQuerySchema
 | 
				
			||||||
 | 
					>;
 | 
				
			||||||
@ -111,6 +111,7 @@ export * from './public-signup-tokens-schema';
 | 
				
			|||||||
export * from './upsert-context-field-schema';
 | 
					export * from './upsert-context-field-schema';
 | 
				
			||||||
export * from './validate-edge-tokens-schema';
 | 
					export * from './validate-edge-tokens-schema';
 | 
				
			||||||
export * from './client-features-query-schema';
 | 
					export * from './client-features-query-schema';
 | 
				
			||||||
 | 
					export * from './admin-features-query-schema';
 | 
				
			||||||
export * from './playground-constraint-schema';
 | 
					export * from './playground-constraint-schema';
 | 
				
			||||||
export * from './create-feature-strategy-schema';
 | 
					export * from './create-feature-strategy-schema';
 | 
				
			||||||
export * from './set-strategy-sort-order-schema';
 | 
					export * from './set-strategy-sort-order-schema';
 | 
				
			||||||
 | 
				
			|||||||
@ -43,6 +43,8 @@ import {
 | 
				
			|||||||
    getStandardResponses,
 | 
					    getStandardResponses,
 | 
				
			||||||
} from '../../../openapi/util/standard-responses';
 | 
					} from '../../../openapi/util/standard-responses';
 | 
				
			||||||
import { SegmentService } from '../../../services/segment-service';
 | 
					import { SegmentService } from '../../../services/segment-service';
 | 
				
			||||||
 | 
					import { querySchema } from '../../../schema/feature-schema';
 | 
				
			||||||
 | 
					import { AdminFeaturesQuerySchema } from '../../../openapi';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface FeatureStrategyParams {
 | 
					interface FeatureStrategyParams {
 | 
				
			||||||
    projectId: string;
 | 
					    projectId: string;
 | 
				
			||||||
@ -66,6 +68,9 @@ interface StrategyIdParams extends FeatureStrategyParams {
 | 
				
			|||||||
export interface IFeatureProjectUserParams extends ProjectParam {
 | 
					export interface IFeatureProjectUserParams extends ProjectParam {
 | 
				
			||||||
    archived?: boolean;
 | 
					    archived?: boolean;
 | 
				
			||||||
    userId?: number;
 | 
					    userId?: number;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    tag?: string[][];
 | 
				
			||||||
 | 
					    namePrefix?: string;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const PATH = '/:projectId/features';
 | 
					const PATH = '/:projectId/features';
 | 
				
			||||||
@ -399,13 +404,12 @@ export default class ProjectFeaturesController extends Controller {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async getFeatures(
 | 
					    async getFeatures(
 | 
				
			||||||
        req: IAuthRequest<ProjectParam, any, any, any>,
 | 
					        req: IAuthRequest<ProjectParam, any, any, AdminFeaturesQuerySchema>,
 | 
				
			||||||
        res: Response<FeaturesSchema>,
 | 
					        res: Response<FeaturesSchema>,
 | 
				
			||||||
    ): Promise<void> {
 | 
					    ): Promise<void> {
 | 
				
			||||||
        const { projectId } = req.params;
 | 
					        const { projectId } = req.params;
 | 
				
			||||||
        const features = await this.featureService.getFeatureOverview({
 | 
					        const query = await this.prepQuery(req.query, projectId);
 | 
				
			||||||
            projectId,
 | 
					        const features = await this.featureService.getFeatureOverview(query);
 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
        this.openApiService.respondWithValidation(
 | 
					        this.openApiService.respondWithValidation(
 | 
				
			||||||
            200,
 | 
					            200,
 | 
				
			||||||
            res,
 | 
					            res,
 | 
				
			||||||
@ -414,6 +418,33 @@ export default class ProjectFeaturesController extends Controller {
 | 
				
			|||||||
        );
 | 
					        );
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async prepQuery(
 | 
				
			||||||
 | 
					        // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
 | 
				
			||||||
 | 
					        { tag, namePrefix }: AdminFeaturesQuerySchema,
 | 
				
			||||||
 | 
					        projectId: string,
 | 
				
			||||||
 | 
					    ): Promise<IFeatureProjectUserParams> {
 | 
				
			||||||
 | 
					        if (!tag && !namePrefix) {
 | 
				
			||||||
 | 
					            return { projectId };
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        const tagQuery = this.paramToArray(tag);
 | 
				
			||||||
 | 
					        const query = await querySchema.validateAsync({
 | 
				
			||||||
 | 
					            tag: tagQuery,
 | 
				
			||||||
 | 
					            namePrefix,
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        if (query.tag) {
 | 
				
			||||||
 | 
					            query.tag = query.tag.map((q) => q.split(':'));
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return { projectId, ...query };
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
 | 
				
			||||||
 | 
					    paramToArray(param: any): Array<any> {
 | 
				
			||||||
 | 
					        if (!param) {
 | 
				
			||||||
 | 
					            return param;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return Array.isArray(param) ? param : [param];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async cloneFeature(
 | 
					    async cloneFeature(
 | 
				
			||||||
        req: IAuthRequest<
 | 
					        req: IAuthRequest<
 | 
				
			||||||
            FeatureParams,
 | 
					            FeatureParams,
 | 
				
			||||||
 | 
				
			|||||||
@ -2710,3 +2710,102 @@ test('should add multiple segments to a strategy', async () => {
 | 
				
			|||||||
            ]);
 | 
					            ]);
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					test('Can filter based on tags', async () => {
 | 
				
			||||||
 | 
					    const tag = { type: 'simple', value: 'hello-tags' };
 | 
				
			||||||
 | 
					    await db.stores.tagStore.createTag(tag);
 | 
				
			||||||
 | 
					    await db.stores.featureToggleStore.create('default', {
 | 
				
			||||||
 | 
					        name: 'to-be-tagged',
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    await db.stores.featureToggleStore.create('default', {
 | 
				
			||||||
 | 
					        name: 'not-tagged',
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    await db.stores.featureTagStore.tagFeature('to-be-tagged', tag);
 | 
				
			||||||
 | 
					    await app.request
 | 
				
			||||||
 | 
					        .get('/api/admin/projects/default/features?tag=simple:hello-tags')
 | 
				
			||||||
 | 
					        .expect((res) => {
 | 
				
			||||||
 | 
					            expect(res.body.features).toHaveLength(1);
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					test('Can query for features with namePrefix', async () => {
 | 
				
			||||||
 | 
					    await db.stores.featureToggleStore.create('default', {
 | 
				
			||||||
 | 
					        name: 'nameprefix-to-be-hit',
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    await db.stores.featureToggleStore.create('default', {
 | 
				
			||||||
 | 
					        name: 'nameprefix-not-be-hit',
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    await app.request
 | 
				
			||||||
 | 
					        .get('/api/admin/projects/default/features?namePrefix=nameprefix-to')
 | 
				
			||||||
 | 
					        .expect((res) => {
 | 
				
			||||||
 | 
					            expect(res.body.features).toHaveLength(1);
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					test('Can query for features with namePrefix and tags', async () => {
 | 
				
			||||||
 | 
					    const tag = { type: 'simple', value: 'hello-nameprefix-tags' };
 | 
				
			||||||
 | 
					    await db.stores.tagStore.createTag(tag);
 | 
				
			||||||
 | 
					    await db.stores.featureToggleStore.create('default', {
 | 
				
			||||||
 | 
					        name: 'to-be-tagged-nameprefix-and-tags',
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    await db.stores.featureToggleStore.create('default', {
 | 
				
			||||||
 | 
					        name: 'not-tagged-nameprefix-and-tags',
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    await db.stores.featureToggleStore.create('default', {
 | 
				
			||||||
 | 
					        name: 'tagged-but-not-hit-nameprefix-and-tags',
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    await db.stores.featureTagStore.tagFeature(
 | 
				
			||||||
 | 
					        'to-be-tagged-nameprefix-and-tags',
 | 
				
			||||||
 | 
					        tag,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    await db.stores.featureTagStore.tagFeature(
 | 
				
			||||||
 | 
					        'tagged-but-not-hit-nameprefix-and-tags',
 | 
				
			||||||
 | 
					        tag,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    await app.request
 | 
				
			||||||
 | 
					        .get(
 | 
				
			||||||
 | 
					            '/api/admin/projects/default/features?namePrefix=to&tag=simple:hello-nameprefix-tags',
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        .expect((res) => {
 | 
				
			||||||
 | 
					            expect(res.body.features).toHaveLength(1);
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					test('Can query for two tags at the same time. Tags are ORed together', async () => {
 | 
				
			||||||
 | 
					    const tag = { type: 'simple', value: 'twotags-first-tag' };
 | 
				
			||||||
 | 
					    const secondTag = { type: 'simple', value: 'twotags-second-tag' };
 | 
				
			||||||
 | 
					    await db.stores.tagStore.createTag(tag);
 | 
				
			||||||
 | 
					    await db.stores.tagStore.createTag(secondTag);
 | 
				
			||||||
 | 
					    const taggedWithFirst = await db.stores.featureToggleStore.create(
 | 
				
			||||||
 | 
					        'default',
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            name: 'tagged-with-first-tag',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    const taggedWithSecond = await db.stores.featureToggleStore.create(
 | 
				
			||||||
 | 
					        'default',
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            name: 'tagged-with-second-tag',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    const taggedWithBoth = await db.stores.featureToggleStore.create(
 | 
				
			||||||
 | 
					        'default',
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            name: 'tagged-with-both-tags',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    await db.stores.featureTagStore.tagFeature(taggedWithFirst.name, tag);
 | 
				
			||||||
 | 
					    await db.stores.featureTagStore.tagFeature(
 | 
				
			||||||
 | 
					        taggedWithSecond.name,
 | 
				
			||||||
 | 
					        secondTag,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    await db.stores.featureTagStore.tagFeature(taggedWithBoth.name, tag);
 | 
				
			||||||
 | 
					    await db.stores.featureTagStore.tagFeature(taggedWithBoth.name, secondTag);
 | 
				
			||||||
 | 
					    await app.request
 | 
				
			||||||
 | 
					        .get(
 | 
				
			||||||
 | 
					            `/api/admin/projects/default/features?tag=${tag.type}:${tag.value}&tag=${secondTag.type}:${secondTag.value}`,
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        .expect((res) => {
 | 
				
			||||||
 | 
					            expect(res.body.features).toHaveLength(3);
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
				
			|||||||
@ -189,6 +189,28 @@ exports[`should serve the OpenAPI spec 1`] = `
 | 
				
			|||||||
        ],
 | 
					        ],
 | 
				
			||||||
        "type": "object",
 | 
					        "type": "object",
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
 | 
					      "adminFeaturesQuerySchema": {
 | 
				
			||||||
 | 
					        "additionalProperties": false,
 | 
				
			||||||
 | 
					        "properties": {
 | 
				
			||||||
 | 
					          "namePrefix": {
 | 
				
			||||||
 | 
					            "description": "A case-insensitive prefix filter for the names of feature toggles",
 | 
				
			||||||
 | 
					            "example": "demo.part1",
 | 
				
			||||||
 | 
					            "type": "string",
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          "tag": {
 | 
				
			||||||
 | 
					            "description": "Used to filter by tags. For each entry, a TAGTYPE:TAGVALUE is expected",
 | 
				
			||||||
 | 
					            "example": [
 | 
				
			||||||
 | 
					              "simple:mytag",
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					            "items": {
 | 
				
			||||||
 | 
					              "pattern": "\\w+:\\w+",
 | 
				
			||||||
 | 
					              "type": "string",
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            "type": "array",
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        "type": "object",
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
      "apiTokenSchema": {
 | 
					      "apiTokenSchema": {
 | 
				
			||||||
        "additionalProperties": false,
 | 
					        "additionalProperties": false,
 | 
				
			||||||
        "properties": {
 | 
					        "properties": {
 | 
				
			||||||
 | 
				
			|||||||
@ -70,3 +70,58 @@ test('Can successfully update project for all strategies belonging to feature',
 | 
				
			|||||||
        );
 | 
					        );
 | 
				
			||||||
    return expect(oldProjectStrats).toHaveLength(0);
 | 
					    return expect(oldProjectStrats).toHaveLength(0);
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					test('Can query for features with tags', async () => {
 | 
				
			||||||
 | 
					    const tag = { type: 'simple', value: 'hello-tags' };
 | 
				
			||||||
 | 
					    await stores.tagStore.createTag(tag);
 | 
				
			||||||
 | 
					    await featureToggleStore.create('default', { name: 'to-be-tagged' });
 | 
				
			||||||
 | 
					    await featureToggleStore.create('default', { name: 'not-tagged' });
 | 
				
			||||||
 | 
					    await stores.featureTagStore.tagFeature('to-be-tagged', tag);
 | 
				
			||||||
 | 
					    const features = await featureStrategiesStore.getFeatureOverview({
 | 
				
			||||||
 | 
					        projectId: 'default',
 | 
				
			||||||
 | 
					        tag: [[tag.type, tag.value]],
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    expect(features).toHaveLength(1);
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					test('Can query for features with namePrefix', async () => {
 | 
				
			||||||
 | 
					    await featureToggleStore.create('default', {
 | 
				
			||||||
 | 
					        name: 'nameprefix-to-be-hit',
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    await featureToggleStore.create('default', {
 | 
				
			||||||
 | 
					        name: 'nameprefix-not-be-hit',
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    const features = await featureStrategiesStore.getFeatureOverview({
 | 
				
			||||||
 | 
					        projectId: 'default',
 | 
				
			||||||
 | 
					        namePrefix: 'nameprefix-to',
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    expect(features).toHaveLength(1);
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					test('Can query for features with namePrefix and tags', async () => {
 | 
				
			||||||
 | 
					    const tag = { type: 'simple', value: 'hello-nameprefix-and-tags' };
 | 
				
			||||||
 | 
					    await stores.tagStore.createTag(tag);
 | 
				
			||||||
 | 
					    await featureToggleStore.create('default', {
 | 
				
			||||||
 | 
					        name: 'to-be-tagged-nameprefix-and-tags',
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    await featureToggleStore.create('default', {
 | 
				
			||||||
 | 
					        name: 'not-tagged-nameprefix-and-tags',
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    await featureToggleStore.create('default', {
 | 
				
			||||||
 | 
					        name: 'tagged-but-not-hit-nameprefix-and-tags',
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    await stores.featureTagStore.tagFeature(
 | 
				
			||||||
 | 
					        'to-be-tagged-nameprefix-and-tags',
 | 
				
			||||||
 | 
					        tag,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    await stores.featureTagStore.tagFeature(
 | 
				
			||||||
 | 
					        'tagged-but-not-hit-nameprefix-and-tags',
 | 
				
			||||||
 | 
					        tag,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    const features = await featureStrategiesStore.getFeatureOverview({
 | 
				
			||||||
 | 
					        projectId: 'default',
 | 
				
			||||||
 | 
					        tag: [[tag.type, tag.value]],
 | 
				
			||||||
 | 
					        namePrefix: 'to',
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    expect(features).toHaveLength(1);
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
		Reference in New Issue
	
	Block a user