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 +24 -0
- package/CONTRIBUTING.md +25 -0
- package/package.json +1 -1
- package/.eslintrc.js +0 -25
- package/.prettierrc +0 -8
- package/jest.config.js +0 -23
- package/src/decorators/feature-flag.decorator.ts +0 -8
- package/src/feature-flags.module.ts +0 -42
- package/src/guards/feature-flag.guard.ts +0 -35
- package/src/index.ts +0 -13
- package/src/interfaces/feature-flag-options.interface.ts +0 -25
- package/src/interfaces/feature-flag.interface.ts +0 -84
- package/src/services/feature-flag.service.spec.ts +0 -51
- package/src/services/feature-flag.service.ts +0 -99
- package/tsconfig.json +0 -25
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
|
package/CONTRIBUTING.md
ADDED
|
@@ -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.
|
|
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
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,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
|
-
}
|