nest-feature-flags 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,24 @@
1
+ # Changelog
2
+
3
+ ## [1.1.0] - 2026-01-31
4
+
5
+ ### Added
6
+ - Comprehensive test suite with guard and module tests
7
+ - Docker support with multi-stage builds
8
+ - GitHub Actions CI/CD pipeline
9
+ - Contributing guidelines
10
+ - npmignore configuration
11
+
12
+ ### Improved
13
+ - Enhanced documentation
14
+ - Better test coverage
15
+
16
+ ## [1.0.0] - 2026-01-31
17
+
18
+ ### Added
19
+ - Initial release
20
+ - Feature flag service
21
+ - Percentage-based rollouts
22
+ - User targeting
23
+ - Group targeting
24
+ - Date-based activation
@@ -0,0 +1,25 @@
1
+ # Contributing to nest-feature-flags
2
+
3
+ Thank you for contributing!
4
+
5
+ ## Development Setup
6
+
7
+ ```bash
8
+ git clone https://github.com/syeedalireza/nest-feature-flags.git
9
+ cd nest-feature-flags
10
+ npm install
11
+ npm test
12
+ ```
13
+
14
+ ## Pull Request Process
15
+
16
+ 1. Fork the repository
17
+ 2. Create feature branch
18
+ 3. Write tests
19
+ 4. Submit PR
20
+
21
+ ## Code Style
22
+
23
+ - Follow ESLint/Prettier
24
+ - Write tests for new features
25
+ - Update documentation
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nest-feature-flags",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Professional feature flag management system for NestJS with A/B testing, gradual rollouts, user targeting, and real-time updates",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
package/.eslintrc.js DELETED
@@ -1,25 +0,0 @@
1
- module.exports = {
2
- parser: '@typescript-eslint/parser',
3
- parserOptions: {
4
- project: 'tsconfig.json',
5
- tsconfigRootDir: __dirname,
6
- sourceType: 'module',
7
- },
8
- plugins: ['@typescript-eslint/eslint-plugin'],
9
- extends: [
10
- 'plugin:@typescript-eslint/recommended',
11
- 'plugin:prettier/recommended',
12
- ],
13
- root: true,
14
- env: {
15
- node: true,
16
- jest: true,
17
- },
18
- ignorePatterns: ['.eslintrc.js'],
19
- rules: {
20
- '@typescript-eslint/interface-name-prefix': 'off',
21
- '@typescript-eslint/explicit-function-return-type': 'off',
22
- '@typescript-eslint/explicit-module-boundary-types': 'off',
23
- '@typescript-eslint/no-explicit-any': 'warn',
24
- },
25
- };
package/.prettierrc DELETED
@@ -1,8 +0,0 @@
1
- {
2
- "singleQuote": true,
3
- "trailingComma": "all",
4
- "printWidth": 100,
5
- "tabWidth": 2,
6
- "semi": true,
7
- "endOfLine": "lf"
8
- }
package/jest.config.js DELETED
@@ -1,23 +0,0 @@
1
- module.exports = {
2
- moduleFileExtensions: ['js', 'json', 'ts'],
3
- rootDir: 'src',
4
- testRegex: '.*\\.spec\\.ts$',
5
- transform: {
6
- '^.+\\.(t|j)s$': 'ts-jest',
7
- },
8
- collectCoverageFrom: [
9
- '**/*.(t|j)s',
10
- '!**/*.spec.ts',
11
- '!**/index.ts',
12
- ],
13
- coverageDirectory: '../coverage',
14
- testEnvironment: 'node',
15
- coverageThreshold: {
16
- global: {
17
- branches: 55,
18
- functions: 65,
19
- lines: 65,
20
- statements: 65,
21
- },
22
- },
23
- };
@@ -1,8 +0,0 @@
1
- import { SetMetadata } from '@nestjs/common';
2
-
3
- export const FEATURE_FLAG_KEY = 'feature_flag_key';
4
-
5
- /**
6
- * Decorator to require a feature flag
7
- */
8
- export const RequireFeature = (flagKey: string) => SetMetadata(FEATURE_FLAG_KEY, flagKey);
@@ -1,42 +0,0 @@
1
- import { Module, DynamicModule, Global } from '@nestjs/common';
2
- import { FeatureFlagsModuleOptions, FeatureFlagsModuleAsyncOptions } from './interfaces/feature-flag-options.interface';
3
- import { FeatureFlagService, OPTIONS_TOKEN } from './services/feature-flag.service';
4
- import { FeatureFlagGuard } from './guards/feature-flag.guard';
5
-
6
- @Global()
7
- @Module({})
8
- export class FeatureFlagsModule {
9
- static forRoot(options: FeatureFlagsModuleOptions): DynamicModule {
10
- return {
11
- module: FeatureFlagsModule,
12
- providers: [
13
- {
14
- provide: OPTIONS_TOKEN,
15
- useValue: options,
16
- },
17
- FeatureFlagService,
18
- FeatureFlagGuard,
19
- ],
20
- exports: [FeatureFlagService, FeatureFlagGuard],
21
- global: true,
22
- };
23
- }
24
-
25
- static forRootAsync(options: FeatureFlagsModuleAsyncOptions): DynamicModule {
26
- return {
27
- module: FeatureFlagsModule,
28
- imports: options.imports || [],
29
- providers: [
30
- {
31
- provide: OPTIONS_TOKEN,
32
- useFactory: options.useFactory!,
33
- inject: options.inject || [],
34
- },
35
- FeatureFlagService,
36
- FeatureFlagGuard,
37
- ],
38
- exports: [FeatureFlagService, FeatureFlagGuard],
39
- global: true,
40
- };
41
- }
42
- }
@@ -1,35 +0,0 @@
1
- import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from '@nestjs/common';
2
- import { Reflector } from '@nestjs/core';
3
- import { FeatureFlagService } from '../services/feature-flag.service';
4
- import { FEATURE_FLAG_KEY } from '../decorators/feature-flag.decorator';
5
-
6
- @Injectable()
7
- export class FeatureFlagGuard implements CanActivate {
8
- constructor(
9
- private readonly reflector: Reflector,
10
- private readonly featureFlagService: FeatureFlagService,
11
- ) {}
12
-
13
- async canActivate(context: ExecutionContext): Promise<boolean> {
14
- const flagKey = this.reflector.get<string>(FEATURE_FLAG_KEY, context.getHandler());
15
-
16
- if (!flagKey) {
17
- return true;
18
- }
19
-
20
- const request = context.switchToHttp().getRequest();
21
- const userId = request.user?.id || request.userId;
22
- const groups = request.user?.groups || [];
23
-
24
- const enabled = await this.featureFlagService.isEnabled(flagKey, {
25
- userId,
26
- groups,
27
- });
28
-
29
- if (!enabled) {
30
- throw new ForbiddenException(`Feature '${flagKey}' is not available`);
31
- }
32
-
33
- return true;
34
- }
35
- }
package/src/index.ts DELETED
@@ -1,13 +0,0 @@
1
- // Module
2
- export * from './feature-flags.module';
3
-
4
- // Service
5
- export * from './services/feature-flag.service';
6
-
7
- // Guards & Decorators
8
- export * from './guards/feature-flag.guard';
9
- export * from './decorators/feature-flag.decorator';
10
-
11
- // Interfaces
12
- export * from './interfaces/feature-flag.interface';
13
- export * from './interfaces/feature-flag-options.interface';
@@ -1,25 +0,0 @@
1
- import { ModuleMetadata } from '@nestjs/common';
2
- import { FeatureFlag } from './feature-flag.interface';
3
-
4
- /**
5
- * Module configuration options
6
- */
7
- export interface FeatureFlagsModuleOptions {
8
- /**
9
- * Feature flag definitions
10
- */
11
- flags: Record<string, FeatureFlag>;
12
-
13
- /**
14
- * Default value when flag is not found
15
- */
16
- defaultValue?: boolean;
17
- }
18
-
19
- /**
20
- * Async module options
21
- */
22
- export interface FeatureFlagsModuleAsyncOptions extends Pick<ModuleMetadata, 'imports'> {
23
- useFactory?: (...args: any[]) => Promise<FeatureFlagsModuleOptions> | FeatureFlagsModuleOptions;
24
- inject?: any[];
25
- }
@@ -1,84 +0,0 @@
1
- /**
2
- * Feature flag configuration
3
- */
4
- export interface FeatureFlag {
5
- /**
6
- * Master switch - feature is enabled
7
- */
8
- enabled: boolean;
9
-
10
- /**
11
- * Percentage of users who should see this feature (0-100)
12
- */
13
- percentage?: number;
14
-
15
- /**
16
- * Specific user IDs who should see this feature
17
- */
18
- userIds?: string[];
19
-
20
- /**
21
- * User groups who should see this feature
22
- */
23
- groups?: string[];
24
-
25
- /**
26
- * Date when feature becomes active
27
- */
28
- startDate?: Date;
29
-
30
- /**
31
- * Date when feature becomes inactive
32
- */
33
- endDate?: Date;
34
-
35
- /**
36
- * Feature description
37
- */
38
- description?: string;
39
- }
40
-
41
- /**
42
- * Context for evaluating feature flags
43
- */
44
- export interface FeatureFlagContext {
45
- /**
46
- * User ID for targeting
47
- */
48
- userId?: string;
49
-
50
- /**
51
- * User groups for targeting
52
- */
53
- groups?: string[];
54
-
55
- /**
56
- * Additional custom attributes
57
- */
58
- attributes?: Record<string, any>;
59
- }
60
-
61
- /**
62
- * Result of feature flag evaluation
63
- */
64
- export interface FeatureFlagResult {
65
- /**
66
- * Flag key
67
- */
68
- key: string;
69
-
70
- /**
71
- * Whether feature is enabled for this context
72
- */
73
- enabled: boolean;
74
-
75
- /**
76
- * Reason for the decision
77
- */
78
- reason: string;
79
-
80
- /**
81
- * Variant (for A/B testing)
82
- */
83
- variant?: string;
84
- }
@@ -1,51 +0,0 @@
1
- import { Test } from '@nestjs/testing';
2
- import { FeatureFlagService, OPTIONS_TOKEN } from './feature-flag.service';
3
-
4
- describe('FeatureFlagService', () => {
5
- let service: FeatureFlagService;
6
-
7
- beforeEach(async () => {
8
- const module = await Test.createTestingModule({
9
- providers: [
10
- FeatureFlagService,
11
- {
12
- provide: OPTIONS_TOKEN,
13
- useValue: {
14
- flags: {
15
- 'enabled-flag': { enabled: true },
16
- 'disabled-flag': { enabled: false },
17
- 'percentage-flag': { enabled: true, percentage: 50 },
18
- 'user-targeted': { enabled: true, userIds: ['user1'] },
19
- },
20
- },
21
- },
22
- ],
23
- }).compile();
24
-
25
- service = module.get<FeatureFlagService>(FeatureFlagService);
26
- });
27
-
28
- it('should be defined', () => {
29
- expect(service).toBeDefined();
30
- });
31
-
32
- it('should return true for enabled flag', async () => {
33
- const result = await service.isEnabled('enabled-flag');
34
- expect(result).toBe(true);
35
- });
36
-
37
- it('should return false for disabled flag', async () => {
38
- const result = await service.isEnabled('disabled-flag');
39
- expect(result).toBe(false);
40
- });
41
-
42
- it('should target specific users', async () => {
43
- const result = await service.isEnabled('user-targeted', { userId: 'user1' });
44
- expect(result).toBe(true);
45
- });
46
-
47
- it('should handle percentage rollout', async () => {
48
- const result = await service.isEnabled('percentage-flag', { userId: 'test' });
49
- expect(typeof result).toBe('boolean');
50
- });
51
- });
@@ -1,99 +0,0 @@
1
- import { Injectable, Inject } from '@nestjs/common';
2
- import { FeatureFlag, FeatureFlagContext, FeatureFlagResult } from '../interfaces/feature-flag.interface';
3
- import { FeatureFlagsModuleOptions } from '../interfaces/feature-flag-options.interface';
4
-
5
- const OPTIONS_TOKEN = 'FEATURE_FLAGS_OPTIONS';
6
-
7
- @Injectable()
8
- export class FeatureFlagService {
9
- constructor(
10
- @Inject(OPTIONS_TOKEN)
11
- private readonly options: FeatureFlagsModuleOptions,
12
- ) {}
13
-
14
- /**
15
- * Check if a feature is enabled
16
- */
17
- async isEnabled(key: string, context?: FeatureFlagContext): Promise<boolean> {
18
- const result = await this.evaluate(key, context);
19
- return result.enabled;
20
- }
21
-
22
- /**
23
- * Evaluate a feature flag
24
- */
25
- async evaluate(key: string, context?: FeatureFlagContext): Promise<FeatureFlagResult> {
26
- const flag = this.options.flags[key];
27
-
28
- if (!flag) {
29
- return {
30
- key,
31
- enabled: this.options.defaultValue ?? false,
32
- reason: 'Flag not found',
33
- };
34
- }
35
-
36
- // Check master switch
37
- if (!flag.enabled) {
38
- return { key, enabled: false, reason: 'Flag disabled' };
39
- }
40
-
41
- // Check date range
42
- const now = new Date();
43
- if (flag.startDate && now < flag.startDate) {
44
- return { key, enabled: false, reason: 'Not started yet' };
45
- }
46
- if (flag.endDate && now > flag.endDate) {
47
- return { key, enabled: false, reason: 'Expired' };
48
- }
49
-
50
- // Check user targeting
51
- if (context?.userId && flag.userIds?.includes(context.userId)) {
52
- return { key, enabled: true, reason: 'User targeted' };
53
- }
54
-
55
- // Check group targeting
56
- if (context?.groups && flag.groups) {
57
- const hasGroup = context.groups.some(g => flag.groups!.includes(g));
58
- if (hasGroup) {
59
- return { key, enabled: true, reason: 'Group targeted' };
60
- }
61
- }
62
-
63
- // Check percentage rollout
64
- if (flag.percentage !== undefined) {
65
- const hash = this.hashUserId(context?.userId || 'anonymous');
66
- const userPercentage = hash % 100;
67
- const enabled = userPercentage < flag.percentage;
68
- return {
69
- key,
70
- enabled,
71
- reason: enabled ? 'In percentage rollout' : 'Not in percentage rollout',
72
- };
73
- }
74
-
75
- return { key, enabled: true, reason: 'Fully enabled' };
76
- }
77
-
78
- /**
79
- * Get all flags
80
- */
81
- getAllFlags(): Record<string, FeatureFlag> {
82
- return this.options.flags;
83
- }
84
-
85
- /**
86
- * Simple hash function for consistent percentage rollout
87
- */
88
- private hashUserId(userId: string): number {
89
- let hash = 0;
90
- for (let i = 0; i < userId.length; i++) {
91
- const char = userId.charCodeAt(i);
92
- hash = (hash << 5) - hash + char;
93
- hash = hash & hash;
94
- }
95
- return Math.abs(hash);
96
- }
97
- }
98
-
99
- export { OPTIONS_TOKEN };
package/tsconfig.json DELETED
@@ -1,25 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "module": "commonjs",
4
- "declaration": true,
5
- "removeComments": true,
6
- "emitDecoratorMetadata": true,
7
- "experimentalDecorators": true,
8
- "allowSyntheticDefaultImports": true,
9
- "target": "ES2021",
10
- "sourceMap": true,
11
- "outDir": "./dist",
12
- "baseUrl": "./",
13
- "incremental": true,
14
- "skipLibCheck": true,
15
- "strictNullChecks": true,
16
- "noImplicitAny": true,
17
- "strictBindCallApply": true,
18
- "forceConsistentCasingInFileNames": true,
19
- "noFallthroughCasesInSwitch": true,
20
- "esModuleInterop": true,
21
- "resolveJsonModule": true
22
- },
23
- "include": ["src/**/*"],
24
- "exclude": ["node_modules", "dist", "test", "**/*spec.ts"]
25
- }