nest-feature-flags 1.0.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.
Files changed (37) hide show
  1. package/.eslintrc.js +25 -0
  2. package/.prettierrc +8 -0
  3. package/LICENSE +21 -0
  4. package/README.md +153 -0
  5. package/dist/decorators/feature-flag.decorator.d.ts +2 -0
  6. package/dist/decorators/feature-flag.decorator.js +8 -0
  7. package/dist/decorators/feature-flag.decorator.js.map +1 -0
  8. package/dist/feature-flags.module.d.ts +6 -0
  9. package/dist/feature-flags.module.js +53 -0
  10. package/dist/feature-flags.module.js.map +1 -0
  11. package/dist/guards/feature-flag.guard.d.ts +9 -0
  12. package/dist/guards/feature-flag.guard.js +46 -0
  13. package/dist/guards/feature-flag.guard.js.map +1 -0
  14. package/dist/index.d.ts +6 -0
  15. package/dist/index.js +23 -0
  16. package/dist/index.js.map +1 -0
  17. package/dist/interfaces/feature-flag-options.interface.d.ts +10 -0
  18. package/dist/interfaces/feature-flag-options.interface.js +3 -0
  19. package/dist/interfaces/feature-flag-options.interface.js.map +1 -0
  20. package/dist/interfaces/feature-flag.interface.d.ts +20 -0
  21. package/dist/interfaces/feature-flag.interface.js +3 -0
  22. package/dist/interfaces/feature-flag.interface.js.map +1 -0
  23. package/dist/services/feature-flag.service.d.ts +12 -0
  24. package/dist/services/feature-flag.service.js +86 -0
  25. package/dist/services/feature-flag.service.js.map +1 -0
  26. package/dist/tsconfig.tsbuildinfo +1 -0
  27. package/jest.config.js +23 -0
  28. package/package.json +77 -0
  29. package/src/decorators/feature-flag.decorator.ts +8 -0
  30. package/src/feature-flags.module.ts +42 -0
  31. package/src/guards/feature-flag.guard.ts +35 -0
  32. package/src/index.ts +13 -0
  33. package/src/interfaces/feature-flag-options.interface.ts +25 -0
  34. package/src/interfaces/feature-flag.interface.ts +84 -0
  35. package/src/services/feature-flag.service.spec.ts +51 -0
  36. package/src/services/feature-flag.service.ts +99 -0
  37. package/tsconfig.json +25 -0
@@ -0,0 +1,25 @@
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
+ }
@@ -0,0 +1,84 @@
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
+ }
@@ -0,0 +1,51 @@
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
+ });
@@ -0,0 +1,99 @@
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 ADDED
@@ -0,0 +1,25 @@
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
+ }