nestjs-ddd-cli 2.2.1 → 3.2.1
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/README.md +247 -408
- package/ddd.schema.json +111 -0
- package/dist/commands/aggregate-validator.d.ts +9 -0
- package/dist/commands/aggregate-validator.js +953 -0
- package/dist/commands/aggregate-validator.js.map +1 -0
- package/dist/commands/ai-assist.d.ts +8 -0
- package/dist/commands/ai-assist.js +337 -0
- package/dist/commands/ai-assist.js.map +1 -0
- package/dist/commands/api-contracts.d.ts +9 -0
- package/dist/commands/api-contracts.js +1368 -0
- package/dist/commands/api-contracts.js.map +1 -0
- package/dist/commands/api-docs.d.ts +8 -0
- package/dist/commands/api-docs.js +408 -0
- package/dist/commands/api-docs.js.map +1 -0
- package/dist/commands/api-versioning.d.ts +11 -0
- package/dist/commands/api-versioning.js +643 -0
- package/dist/commands/api-versioning.js.map +1 -0
- package/dist/commands/audit-logging.d.ts +9 -0
- package/dist/commands/audit-logging.js +1129 -0
- package/dist/commands/audit-logging.js.map +1 -0
- package/dist/commands/batch-generate.d.ts +10 -0
- package/dist/commands/batch-generate.js +405 -0
- package/dist/commands/batch-generate.js.map +1 -0
- package/dist/commands/caching-strategies.d.ts +9 -0
- package/dist/commands/caching-strategies.js +874 -0
- package/dist/commands/caching-strategies.js.map +1 -0
- package/dist/commands/code-analyzer.d.ts +42 -0
- package/dist/commands/code-analyzer.js +474 -0
- package/dist/commands/code-analyzer.js.map +1 -0
- package/dist/commands/database-seeding.d.ts +6 -0
- package/dist/commands/database-seeding.js +621 -0
- package/dist/commands/database-seeding.js.map +1 -0
- package/dist/commands/db-optimization.d.ts +7 -0
- package/dist/commands/db-optimization.js +687 -0
- package/dist/commands/db-optimization.js.map +1 -0
- package/dist/commands/dependency-graph.d.ts +6 -0
- package/dist/commands/dependency-graph.js +329 -0
- package/dist/commands/dependency-graph.js.map +1 -0
- package/dist/commands/doctor-enhanced.d.ts +22 -0
- package/dist/commands/doctor-enhanced.js +543 -0
- package/dist/commands/doctor-enhanced.js.map +1 -0
- package/dist/commands/doctor.d.ts +4 -0
- package/dist/commands/doctor.js +151 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/env-manager.d.ts +6 -0
- package/dist/commands/env-manager.js +419 -0
- package/dist/commands/env-manager.js.map +1 -0
- package/dist/commands/event-sourcing-full.d.ts +10 -0
- package/dist/commands/event-sourcing-full.js +1107 -0
- package/dist/commands/event-sourcing-full.js.map +1 -0
- package/dist/commands/feature-flags.d.ts +9 -0
- package/dist/commands/feature-flags.js +824 -0
- package/dist/commands/feature-flags.js.map +1 -0
- package/dist/commands/filter-dsl.d.ts +10 -0
- package/dist/commands/filter-dsl.js +1407 -0
- package/dist/commands/filter-dsl.js.map +1 -0
- package/dist/commands/generate-all.js +485 -32
- package/dist/commands/generate-all.js.map +1 -1
- package/dist/commands/generate-deployment.d.ts +8 -0
- package/dist/commands/generate-deployment.js +746 -0
- package/dist/commands/generate-deployment.js.map +1 -0
- package/dist/commands/generate-domain-service.d.ts +14 -0
- package/dist/commands/generate-domain-service.js +796 -0
- package/dist/commands/generate-domain-service.js.map +1 -0
- package/dist/commands/generate-entity.js +82 -24
- package/dist/commands/generate-entity.js.map +1 -1
- package/dist/commands/generate-from-schema.d.ts +56 -0
- package/dist/commands/generate-from-schema.js +222 -0
- package/dist/commands/generate-from-schema.js.map +1 -0
- package/dist/commands/generate-orchestrator.d.ts +14 -0
- package/dist/commands/generate-orchestrator.js +887 -0
- package/dist/commands/generate-orchestrator.js.map +1 -0
- package/dist/commands/generate-repository.d.ts +14 -0
- package/dist/commands/generate-repository.js +1019 -0
- package/dist/commands/generate-repository.js.map +1 -0
- package/dist/commands/generate-shared.d.ts +4 -0
- package/dist/commands/generate-shared.js +388 -0
- package/dist/commands/generate-shared.js.map +1 -0
- package/dist/commands/generate-value-object.d.ts +32 -0
- package/dist/commands/generate-value-object.js +700 -0
- package/dist/commands/generate-value-object.js.map +1 -0
- package/dist/commands/graphql-subscriptions.d.ts +6 -0
- package/dist/commands/graphql-subscriptions.js +607 -0
- package/dist/commands/graphql-subscriptions.js.map +1 -0
- package/dist/commands/graphql-types.d.ts +5 -0
- package/dist/commands/graphql-types.js +423 -0
- package/dist/commands/graphql-types.js.map +1 -0
- package/dist/commands/health-probes-advanced.d.ts +6 -0
- package/dist/commands/health-probes-advanced.js +655 -0
- package/dist/commands/health-probes-advanced.js.map +1 -0
- package/dist/commands/i18n-setup.d.ts +10 -0
- package/dist/commands/i18n-setup.js +677 -0
- package/dist/commands/i18n-setup.js.map +1 -0
- package/dist/commands/init-config.d.ts +6 -0
- package/dist/commands/init-config.js +370 -0
- package/dist/commands/init-config.js.map +1 -0
- package/dist/commands/init-project.js +56 -6
- package/dist/commands/init-project.js.map +1 -1
- package/dist/commands/interactive-scaffold.d.ts +5 -0
- package/dist/commands/interactive-scaffold.js +271 -0
- package/dist/commands/interactive-scaffold.js.map +1 -0
- package/dist/commands/metrics-prometheus.d.ts +6 -0
- package/dist/commands/metrics-prometheus.js +681 -0
- package/dist/commands/metrics-prometheus.js.map +1 -0
- package/dist/commands/migration-engine.d.ts +6 -0
- package/dist/commands/migration-engine.js +446 -0
- package/dist/commands/migration-engine.js.map +1 -0
- package/dist/commands/migration.d.ts +12 -0
- package/dist/commands/migration.js +484 -0
- package/dist/commands/migration.js.map +1 -0
- package/dist/commands/monorepo.d.ts +8 -0
- package/dist/commands/monorepo.js +483 -0
- package/dist/commands/monorepo.js.map +1 -0
- package/dist/commands/multi-database.d.ts +5 -0
- package/dist/commands/multi-database.js +439 -0
- package/dist/commands/multi-database.js.map +1 -0
- package/dist/commands/observability-tracing.d.ts +10 -0
- package/dist/commands/observability-tracing.js +740 -0
- package/dist/commands/observability-tracing.js.map +1 -0
- package/dist/commands/openapi-export.d.ts +8 -0
- package/dist/commands/openapi-export.js +359 -0
- package/dist/commands/openapi-export.js.map +1 -0
- package/dist/commands/perf-analyzer.d.ts +8 -0
- package/dist/commands/perf-analyzer.js +423 -0
- package/dist/commands/perf-analyzer.js.map +1 -0
- package/dist/commands/rate-limiting.d.ts +10 -0
- package/dist/commands/rate-limiting.js +953 -0
- package/dist/commands/rate-limiting.js.map +1 -0
- package/dist/commands/recipe-plugin.d.ts +56 -0
- package/dist/commands/recipe-plugin.js +315 -0
- package/dist/commands/recipe-plugin.js.map +1 -0
- package/dist/commands/recipe.d.ts +6 -0
- package/dist/commands/recipe.js +3941 -0
- package/dist/commands/recipe.js.map +1 -0
- package/dist/commands/recipes/elasticsearch.recipe.d.ts +1 -0
- package/dist/commands/recipes/elasticsearch.recipe.js +761 -0
- package/dist/commands/recipes/elasticsearch.recipe.js.map +1 -0
- package/dist/commands/recipes/event-sourcing.recipe.d.ts +1 -0
- package/dist/commands/recipes/event-sourcing.recipe.js +889 -0
- package/dist/commands/recipes/event-sourcing.recipe.js.map +1 -0
- package/dist/commands/recipes/index.d.ts +7 -0
- package/dist/commands/recipes/index.js +24 -0
- package/dist/commands/recipes/index.js.map +1 -0
- package/dist/commands/recipes/message-queue.recipe.d.ts +1 -0
- package/dist/commands/recipes/message-queue.recipe.js +706 -0
- package/dist/commands/recipes/message-queue.recipe.js.map +1 -0
- package/dist/commands/recipes/middleware.recipe.d.ts +1 -0
- package/dist/commands/recipes/middleware.recipe.js +383 -0
- package/dist/commands/recipes/middleware.recipe.js.map +1 -0
- package/dist/commands/recipes/multi-tenancy.recipe.d.ts +1 -0
- package/dist/commands/recipes/multi-tenancy.recipe.js +520 -0
- package/dist/commands/recipes/multi-tenancy.recipe.js.map +1 -0
- package/dist/commands/recipes/oauth2.recipe.d.ts +1 -0
- package/dist/commands/recipes/oauth2.recipe.js +472 -0
- package/dist/commands/recipes/oauth2.recipe.js.map +1 -0
- package/dist/commands/recipes/websocket.recipe.d.ts +1 -0
- package/dist/commands/recipes/websocket.recipe.js +453 -0
- package/dist/commands/recipes/websocket.recipe.js.map +1 -0
- package/dist/commands/resilience-patterns.d.ts +13 -0
- package/dist/commands/resilience-patterns.js +1029 -0
- package/dist/commands/resilience-patterns.js.map +1 -0
- package/dist/commands/security-patterns.d.ts +11 -0
- package/dist/commands/security-patterns.js +2233 -0
- package/dist/commands/security-patterns.js.map +1 -0
- package/dist/commands/template-debug.d.ts +27 -0
- package/dist/commands/template-debug.js +388 -0
- package/dist/commands/template-debug.js.map +1 -0
- package/dist/commands/test-factory-full.d.ts +9 -0
- package/dist/commands/test-factory-full.js +1570 -0
- package/dist/commands/test-factory-full.js.map +1 -0
- package/dist/commands/test-scaffold.d.ts +7 -0
- package/dist/commands/test-scaffold.js +621 -0
- package/dist/commands/test-scaffold.js.map +1 -0
- package/dist/index.js +1088 -0
- package/dist/index.js.map +1 -1
- package/dist/templates/ai-context/CLAUDE.md.hbs +158 -0
- package/dist/templates/ai-context/conventions.md.hbs +154 -0
- package/dist/templates/command/create-command.hbs +6 -14
- package/dist/templates/command/delete-command.hbs +19 -0
- package/dist/templates/command/update-command.hbs +24 -0
- package/dist/templates/controller/controller.hbs +64 -17
- package/dist/templates/dto/create-dto.hbs +29 -5
- package/dist/templates/dto/filter-dto.hbs +52 -0
- package/dist/templates/dto/filter-query.dto.hbs +148 -0
- package/dist/templates/dto/paginated-response.dto.hbs +29 -0
- package/dist/templates/dto/pagination-query.dto.hbs +30 -0
- package/dist/templates/dto/response-dto.hbs +38 -0
- package/dist/templates/dto/update-dto.hbs +11 -0
- package/dist/templates/entity/entity.hbs +32 -1
- package/dist/templates/event/domain-event.hbs +33 -7
- package/dist/templates/event/event-handler.hbs +40 -0
- package/dist/templates/exception/base-exceptions.hbs +69 -0
- package/dist/templates/exception/entity-not-found.exception.hbs +7 -0
- package/dist/templates/mapper/mapper.hbs +49 -24
- package/dist/templates/module/module.hbs +34 -10
- package/dist/templates/orm-entity/orm-entity.hbs +63 -12
- package/dist/templates/prisma/prisma-mapper.hbs +71 -0
- package/dist/templates/prisma/prisma-repository.hbs +114 -0
- package/dist/templates/prisma/prisma-schema.hbs +20 -0
- package/dist/templates/prisma/prisma-service.hbs +51 -0
- package/dist/templates/query/get-all.query.hbs +50 -0
- package/dist/templates/query/get-by-id.query.hbs +31 -0
- package/dist/templates/repository/repository.hbs +55 -13
- package/dist/templates/resolver/graphql-input.hbs +54 -0
- package/dist/templates/resolver/graphql-type.hbs +58 -0
- package/dist/templates/resolver/pagination-args.hbs +33 -0
- package/dist/templates/resolver/resolver.hbs +62 -0
- package/dist/templates/shared/prisma-query-builder.util.hbs +189 -0
- package/dist/templates/shared/query-builder.util.hbs +218 -0
- package/dist/templates/test/controller.spec.hbs +124 -0
- package/dist/templates/test/repository.spec.hbs +158 -0
- package/dist/templates/test/usecase.spec.hbs +116 -0
- package/dist/templates/usecase/create-usecase.hbs +19 -7
- package/dist/templates/usecase/delete-usecase.hbs +17 -0
- package/dist/templates/usecase/update-usecase.hbs +31 -0
- package/dist/utils/config.utils.d.ts +45 -0
- package/dist/utils/config.utils.js +211 -0
- package/dist/utils/config.utils.js.map +1 -0
- package/dist/utils/error.utils.d.ts +145 -0
- package/dist/utils/error.utils.js +422 -0
- package/dist/utils/error.utils.js.map +1 -0
- package/dist/utils/field.utils.d.ts +54 -0
- package/dist/utils/field.utils.js +389 -0
- package/dist/utils/field.utils.js.map +1 -0
- package/dist/utils/file.utils.d.ts +19 -8
- package/dist/utils/file.utils.js +135 -4
- package/dist/utils/file.utils.js.map +1 -1
- package/dist/utils/idempotency.utils.d.ts +123 -0
- package/dist/utils/idempotency.utils.js +444 -0
- package/dist/utils/idempotency.utils.js.map +1 -0
- package/dist/utils/naming.utils.js +24 -5
- package/dist/utils/naming.utils.js.map +1 -1
- package/dist/utils/performance.utils.d.ts +37 -0
- package/dist/utils/performance.utils.js +158 -0
- package/dist/utils/performance.utils.js.map +1 -0
- package/dist/utils/relation.utils.d.ts +92 -0
- package/dist/utils/relation.utils.js +388 -0
- package/dist/utils/relation.utils.js.map +1 -0
- package/dist/utils/rollback.utils.d.ts +49 -0
- package/dist/utils/rollback.utils.js +306 -0
- package/dist/utils/rollback.utils.js.map +1 -0
- package/dist/utils/schema.utils.d.ts +123 -0
- package/dist/utils/schema.utils.js +419 -0
- package/dist/utils/schema.utils.js.map +1 -0
- package/dist/utils/security.utils.d.ts +57 -0
- package/dist/utils/security.utils.js +315 -0
- package/dist/utils/security.utils.js.map +1 -0
- package/dist/utils/template-engine.utils.d.ts +80 -0
- package/dist/utils/template-engine.utils.js +463 -0
- package/dist/utils/template-engine.utils.js.map +1 -0
- package/dist/utils/validation-registry.utils.d.ts +160 -0
- package/dist/utils/validation-registry.utils.js +526 -0
- package/dist/utils/validation-registry.utils.js.map +1 -0
- package/package.json +3 -1
|
@@ -0,0 +1,824 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Feature Flags & Toggles Framework Generator
|
|
4
|
+
* Runtime feature management with A/B testing support
|
|
5
|
+
*/
|
|
6
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
7
|
+
if (k2 === undefined) k2 = k;
|
|
8
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
9
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
10
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
11
|
+
}
|
|
12
|
+
Object.defineProperty(o, k2, desc);
|
|
13
|
+
}) : (function(o, m, k, k2) {
|
|
14
|
+
if (k2 === undefined) k2 = k;
|
|
15
|
+
o[k2] = m[k];
|
|
16
|
+
}));
|
|
17
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
18
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
19
|
+
}) : function(o, v) {
|
|
20
|
+
o["default"] = v;
|
|
21
|
+
});
|
|
22
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
23
|
+
var ownKeys = function(o) {
|
|
24
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
25
|
+
var ar = [];
|
|
26
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
27
|
+
return ar;
|
|
28
|
+
};
|
|
29
|
+
return ownKeys(o);
|
|
30
|
+
};
|
|
31
|
+
return function (mod) {
|
|
32
|
+
if (mod && mod.__esModule) return mod;
|
|
33
|
+
var result = {};
|
|
34
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
35
|
+
__setModuleDefault(result, mod);
|
|
36
|
+
return result;
|
|
37
|
+
};
|
|
38
|
+
})();
|
|
39
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
40
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
41
|
+
};
|
|
42
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
43
|
+
exports.setupFeatureFlags = setupFeatureFlags;
|
|
44
|
+
const fs = __importStar(require("fs"));
|
|
45
|
+
const path = __importStar(require("path"));
|
|
46
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
47
|
+
async function setupFeatureFlags(basePath, options = {}) {
|
|
48
|
+
console.log(chalk_1.default.bold.blue('\n🚩 Setting up Feature Flags Framework\n'));
|
|
49
|
+
const sharedPath = path.join(basePath, 'src/shared/features');
|
|
50
|
+
if (!fs.existsSync(sharedPath)) {
|
|
51
|
+
fs.mkdirSync(sharedPath, { recursive: true });
|
|
52
|
+
}
|
|
53
|
+
// Generate feature flag service
|
|
54
|
+
fs.writeFileSync(path.join(sharedPath, 'feature-flag.service.ts'), generateFeatureFlagService());
|
|
55
|
+
console.log(chalk_1.default.green(` ✓ Created feature flag service`));
|
|
56
|
+
// Generate feature decorator
|
|
57
|
+
fs.writeFileSync(path.join(sharedPath, 'feature.decorator.ts'), generateFeatureDecorator());
|
|
58
|
+
console.log(chalk_1.default.green(` ✓ Created feature decorator`));
|
|
59
|
+
// Generate feature guard
|
|
60
|
+
fs.writeFileSync(path.join(sharedPath, 'feature.guard.ts'), generateFeatureGuard());
|
|
61
|
+
console.log(chalk_1.default.green(` ✓ Created feature guard`));
|
|
62
|
+
// Generate A/B testing service
|
|
63
|
+
fs.writeFileSync(path.join(sharedPath, 'ab-testing.service.ts'), generateABTestingService());
|
|
64
|
+
console.log(chalk_1.default.green(` ✓ Created A/B testing service`));
|
|
65
|
+
// Generate feature flag entity
|
|
66
|
+
fs.writeFileSync(path.join(sharedPath, 'feature-flag.entity.ts'), generateFeatureFlagEntity());
|
|
67
|
+
console.log(chalk_1.default.green(` ✓ Created feature flag entity`));
|
|
68
|
+
// Generate feature module
|
|
69
|
+
fs.writeFileSync(path.join(sharedPath, 'feature.module.ts'), generateFeatureModule());
|
|
70
|
+
console.log(chalk_1.default.green(` ✓ Created feature module`));
|
|
71
|
+
console.log(chalk_1.default.bold.green('\n✅ Feature flags framework ready!\n'));
|
|
72
|
+
}
|
|
73
|
+
function generateFeatureFlagService() {
|
|
74
|
+
return `/**
|
|
75
|
+
* Feature Flag Service
|
|
76
|
+
* Manages feature toggles at runtime
|
|
77
|
+
*/
|
|
78
|
+
|
|
79
|
+
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
|
|
80
|
+
import { EventEmitter2 } from '@nestjs/event-emitter';
|
|
81
|
+
|
|
82
|
+
export interface FeatureFlag {
|
|
83
|
+
key: string;
|
|
84
|
+
enabled: boolean;
|
|
85
|
+
description?: string;
|
|
86
|
+
conditions?: FeatureCondition[];
|
|
87
|
+
variants?: FeatureVariant[];
|
|
88
|
+
percentage?: number;
|
|
89
|
+
metadata?: Record<string, any>;
|
|
90
|
+
createdAt: Date;
|
|
91
|
+
updatedAt: Date;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface FeatureCondition {
|
|
95
|
+
type: 'user' | 'group' | 'percentage' | 'date' | 'custom';
|
|
96
|
+
operator: 'equals' | 'contains' | 'gt' | 'lt' | 'in' | 'not_in';
|
|
97
|
+
field?: string;
|
|
98
|
+
value: any;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export interface FeatureVariant {
|
|
102
|
+
name: string;
|
|
103
|
+
weight: number;
|
|
104
|
+
payload?: any;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export interface FeatureContext {
|
|
108
|
+
userId?: string;
|
|
109
|
+
userGroups?: string[];
|
|
110
|
+
attributes?: Record<string, any>;
|
|
111
|
+
sessionId?: string;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
@Injectable()
|
|
115
|
+
export class FeatureFlagService implements OnModuleInit {
|
|
116
|
+
private readonly logger = new Logger(FeatureFlagService.name);
|
|
117
|
+
private readonly flags = new Map<string, FeatureFlag>();
|
|
118
|
+
private readonly overrides = new Map<string, Map<string, boolean>>();
|
|
119
|
+
|
|
120
|
+
constructor(private readonly eventEmitter: EventEmitter2) {}
|
|
121
|
+
|
|
122
|
+
async onModuleInit() {
|
|
123
|
+
await this.loadFlags();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Check if a feature is enabled
|
|
128
|
+
*/
|
|
129
|
+
isEnabled(key: string, context?: FeatureContext): boolean {
|
|
130
|
+
// Check for user override
|
|
131
|
+
if (context?.userId) {
|
|
132
|
+
const userOverrides = this.overrides.get(context.userId);
|
|
133
|
+
if (userOverrides?.has(key)) {
|
|
134
|
+
return userOverrides.get(key)!;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const flag = this.flags.get(key);
|
|
139
|
+
if (!flag) {
|
|
140
|
+
this.logger.warn(\`Feature flag '\${key}' not found\`);
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (!flag.enabled) {
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Check conditions
|
|
149
|
+
if (flag.conditions && flag.conditions.length > 0) {
|
|
150
|
+
return this.evaluateConditions(flag.conditions, context);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Check percentage rollout
|
|
154
|
+
if (flag.percentage !== undefined) {
|
|
155
|
+
return this.evaluatePercentage(key, flag.percentage, context);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Get feature variant
|
|
163
|
+
*/
|
|
164
|
+
getVariant(key: string, context?: FeatureContext): FeatureVariant | null {
|
|
165
|
+
const flag = this.flags.get(key);
|
|
166
|
+
if (!flag || !flag.variants || flag.variants.length === 0) {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (!this.isEnabled(key, context)) {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return this.selectVariant(key, flag.variants, context);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Create or update a feature flag
|
|
179
|
+
*/
|
|
180
|
+
async setFlag(key: string, config: Partial<FeatureFlag>): Promise<FeatureFlag> {
|
|
181
|
+
const existing = this.flags.get(key);
|
|
182
|
+
const flag: FeatureFlag = {
|
|
183
|
+
key,
|
|
184
|
+
enabled: config.enabled ?? existing?.enabled ?? false,
|
|
185
|
+
description: config.description ?? existing?.description,
|
|
186
|
+
conditions: config.conditions ?? existing?.conditions,
|
|
187
|
+
variants: config.variants ?? existing?.variants,
|
|
188
|
+
percentage: config.percentage ?? existing?.percentage,
|
|
189
|
+
metadata: config.metadata ?? existing?.metadata,
|
|
190
|
+
createdAt: existing?.createdAt ?? new Date(),
|
|
191
|
+
updatedAt: new Date(),
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
this.flags.set(key, flag);
|
|
195
|
+
this.eventEmitter.emit('feature-flag.updated', { key, flag });
|
|
196
|
+
|
|
197
|
+
return flag;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Enable a feature
|
|
202
|
+
*/
|
|
203
|
+
async enable(key: string): Promise<void> {
|
|
204
|
+
await this.setFlag(key, { enabled: true });
|
|
205
|
+
this.logger.log(\`Feature '\${key}' enabled\`);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Disable a feature
|
|
210
|
+
*/
|
|
211
|
+
async disable(key: string): Promise<void> {
|
|
212
|
+
await this.setFlag(key, { enabled: false });
|
|
213
|
+
this.logger.log(\`Feature '\${key}' disabled\`);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Set user override
|
|
218
|
+
*/
|
|
219
|
+
setUserOverride(userId: string, key: string, enabled: boolean): void {
|
|
220
|
+
let userOverrides = this.overrides.get(userId);
|
|
221
|
+
if (!userOverrides) {
|
|
222
|
+
userOverrides = new Map();
|
|
223
|
+
this.overrides.set(userId, userOverrides);
|
|
224
|
+
}
|
|
225
|
+
userOverrides.set(key, enabled);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Remove user override
|
|
230
|
+
*/
|
|
231
|
+
removeUserOverride(userId: string, key: string): void {
|
|
232
|
+
const userOverrides = this.overrides.get(userId);
|
|
233
|
+
if (userOverrides) {
|
|
234
|
+
userOverrides.delete(key);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Get all flags
|
|
240
|
+
*/
|
|
241
|
+
getAllFlags(): FeatureFlag[] {
|
|
242
|
+
return Array.from(this.flags.values());
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Get flag by key
|
|
247
|
+
*/
|
|
248
|
+
getFlag(key: string): FeatureFlag | undefined {
|
|
249
|
+
return this.flags.get(key);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Delete a flag
|
|
254
|
+
*/
|
|
255
|
+
async deleteFlag(key: string): Promise<void> {
|
|
256
|
+
this.flags.delete(key);
|
|
257
|
+
this.eventEmitter.emit('feature-flag.deleted', { key });
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
private async loadFlags(): Promise<void> {
|
|
261
|
+
// Override this method to load flags from database/external source
|
|
262
|
+
this.logger.log('Feature flags loaded');
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
private evaluateConditions(conditions: FeatureCondition[], context?: FeatureContext): boolean {
|
|
266
|
+
if (!context) return false;
|
|
267
|
+
|
|
268
|
+
return conditions.every(condition => {
|
|
269
|
+
switch (condition.type) {
|
|
270
|
+
case 'user':
|
|
271
|
+
return this.evaluateUserCondition(condition, context);
|
|
272
|
+
case 'group':
|
|
273
|
+
return this.evaluateGroupCondition(condition, context);
|
|
274
|
+
case 'percentage':
|
|
275
|
+
return this.evaluatePercentageCondition(condition, context);
|
|
276
|
+
case 'date':
|
|
277
|
+
return this.evaluateDateCondition(condition);
|
|
278
|
+
case 'custom':
|
|
279
|
+
return this.evaluateCustomCondition(condition, context);
|
|
280
|
+
default:
|
|
281
|
+
return false;
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
private evaluateUserCondition(condition: FeatureCondition, context: FeatureContext): boolean {
|
|
287
|
+
if (!context.userId) return false;
|
|
288
|
+
|
|
289
|
+
switch (condition.operator) {
|
|
290
|
+
case 'equals':
|
|
291
|
+
return context.userId === condition.value;
|
|
292
|
+
case 'in':
|
|
293
|
+
return Array.isArray(condition.value) && condition.value.includes(context.userId);
|
|
294
|
+
case 'not_in':
|
|
295
|
+
return Array.isArray(condition.value) && !condition.value.includes(context.userId);
|
|
296
|
+
default:
|
|
297
|
+
return false;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
private evaluateGroupCondition(condition: FeatureCondition, context: FeatureContext): boolean {
|
|
302
|
+
if (!context.userGroups) return false;
|
|
303
|
+
|
|
304
|
+
switch (condition.operator) {
|
|
305
|
+
case 'contains':
|
|
306
|
+
return context.userGroups.includes(condition.value);
|
|
307
|
+
case 'in':
|
|
308
|
+
return Array.isArray(condition.value) &&
|
|
309
|
+
condition.value.some(g => context.userGroups!.includes(g));
|
|
310
|
+
default:
|
|
311
|
+
return false;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
private evaluatePercentageCondition(condition: FeatureCondition, context: FeatureContext): boolean {
|
|
316
|
+
const hash = this.hashString(context.userId || context.sessionId || 'anonymous');
|
|
317
|
+
return (hash % 100) < condition.value;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
private evaluateDateCondition(condition: FeatureCondition): boolean {
|
|
321
|
+
const now = new Date();
|
|
322
|
+
const targetDate = new Date(condition.value);
|
|
323
|
+
|
|
324
|
+
switch (condition.operator) {
|
|
325
|
+
case 'gt':
|
|
326
|
+
return now > targetDate;
|
|
327
|
+
case 'lt':
|
|
328
|
+
return now < targetDate;
|
|
329
|
+
default:
|
|
330
|
+
return false;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
private evaluateCustomCondition(condition: FeatureCondition, context: FeatureContext): boolean {
|
|
335
|
+
if (!context.attributes || !condition.field) return false;
|
|
336
|
+
|
|
337
|
+
const value = context.attributes[condition.field];
|
|
338
|
+
|
|
339
|
+
switch (condition.operator) {
|
|
340
|
+
case 'equals':
|
|
341
|
+
return value === condition.value;
|
|
342
|
+
case 'contains':
|
|
343
|
+
return String(value).includes(condition.value);
|
|
344
|
+
case 'in':
|
|
345
|
+
return Array.isArray(condition.value) && condition.value.includes(value);
|
|
346
|
+
default:
|
|
347
|
+
return false;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
private evaluatePercentage(key: string, percentage: number, context?: FeatureContext): boolean {
|
|
352
|
+
const identifier = context?.userId || context?.sessionId || 'anonymous';
|
|
353
|
+
const hash = this.hashString(\`\${key}:\${identifier}\`);
|
|
354
|
+
return (hash % 100) < percentage;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
private selectVariant(key: string, variants: FeatureVariant[], context?: FeatureContext): FeatureVariant {
|
|
358
|
+
const identifier = context?.userId || context?.sessionId || 'anonymous';
|
|
359
|
+
const hash = this.hashString(\`\${key}:variant:\${identifier}\`);
|
|
360
|
+
const totalWeight = variants.reduce((sum, v) => sum + v.weight, 0);
|
|
361
|
+
let random = hash % totalWeight;
|
|
362
|
+
|
|
363
|
+
for (const variant of variants) {
|
|
364
|
+
random -= variant.weight;
|
|
365
|
+
if (random < 0) {
|
|
366
|
+
return variant;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
return variants[variants.length - 1];
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
private hashString(str: string): number {
|
|
374
|
+
let hash = 0;
|
|
375
|
+
for (let i = 0; i < str.length; i++) {
|
|
376
|
+
const char = str.charCodeAt(i);
|
|
377
|
+
hash = ((hash << 5) - hash) + char;
|
|
378
|
+
hash = hash & hash;
|
|
379
|
+
}
|
|
380
|
+
return Math.abs(hash);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
`;
|
|
384
|
+
}
|
|
385
|
+
function generateFeatureDecorator() {
|
|
386
|
+
return `/**
|
|
387
|
+
* Feature Decorators
|
|
388
|
+
* Mark endpoints and methods with feature flag requirements
|
|
389
|
+
*/
|
|
390
|
+
|
|
391
|
+
import { SetMetadata, applyDecorators, UseGuards } from '@nestjs/common';
|
|
392
|
+
import { FeatureGuard } from './feature.guard';
|
|
393
|
+
|
|
394
|
+
export const FEATURE_KEY = 'feature_flag_key';
|
|
395
|
+
export const FEATURE_FALLBACK = 'feature_fallback';
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Require a feature flag to be enabled
|
|
399
|
+
*/
|
|
400
|
+
export function Feature(key: string, options?: { fallback?: any }) {
|
|
401
|
+
return applyDecorators(
|
|
402
|
+
SetMetadata(FEATURE_KEY, key),
|
|
403
|
+
SetMetadata(FEATURE_FALLBACK, options?.fallback),
|
|
404
|
+
UseGuards(FeatureGuard),
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Mark as beta feature
|
|
410
|
+
*/
|
|
411
|
+
export function BetaFeature(key: string) {
|
|
412
|
+
return Feature(\`beta:\${key}\`);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Mark as experimental feature
|
|
417
|
+
*/
|
|
418
|
+
export function ExperimentalFeature(key: string) {
|
|
419
|
+
return Feature(\`experimental:\${key}\`);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Mark feature with variant requirement
|
|
424
|
+
*/
|
|
425
|
+
export function FeatureVariant(key: string, variant: string) {
|
|
426
|
+
return applyDecorators(
|
|
427
|
+
SetMetadata(FEATURE_KEY, key),
|
|
428
|
+
SetMetadata('feature_variant', variant),
|
|
429
|
+
UseGuards(FeatureGuard),
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Skip feature flag check (useful for overriding in tests)
|
|
435
|
+
*/
|
|
436
|
+
export function SkipFeatureCheck() {
|
|
437
|
+
return SetMetadata('skip_feature_check', true);
|
|
438
|
+
}
|
|
439
|
+
`;
|
|
440
|
+
}
|
|
441
|
+
function generateFeatureGuard() {
|
|
442
|
+
return `/**
|
|
443
|
+
* Feature Guard
|
|
444
|
+
* Protects routes based on feature flags
|
|
445
|
+
*/
|
|
446
|
+
|
|
447
|
+
import {
|
|
448
|
+
Injectable,
|
|
449
|
+
CanActivate,
|
|
450
|
+
ExecutionContext,
|
|
451
|
+
ForbiddenException,
|
|
452
|
+
} from '@nestjs/common';
|
|
453
|
+
import { Reflector } from '@nestjs/core';
|
|
454
|
+
import { FeatureFlagService, FeatureContext } from './feature-flag.service';
|
|
455
|
+
import { FEATURE_KEY, FEATURE_FALLBACK } from './feature.decorator';
|
|
456
|
+
|
|
457
|
+
@Injectable()
|
|
458
|
+
export class FeatureGuard implements CanActivate {
|
|
459
|
+
constructor(
|
|
460
|
+
private readonly reflector: Reflector,
|
|
461
|
+
private readonly featureService: FeatureFlagService,
|
|
462
|
+
) {}
|
|
463
|
+
|
|
464
|
+
async canActivate(context: ExecutionContext): Promise<boolean> {
|
|
465
|
+
const skipCheck = this.reflector.get<boolean>('skip_feature_check', context.getHandler());
|
|
466
|
+
if (skipCheck) {
|
|
467
|
+
return true;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
const featureKey = this.reflector.get<string>(FEATURE_KEY, context.getHandler());
|
|
471
|
+
if (!featureKey) {
|
|
472
|
+
return true;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const request = context.switchToHttp().getRequest();
|
|
476
|
+
const featureContext = this.extractContext(request);
|
|
477
|
+
|
|
478
|
+
const isEnabled = this.featureService.isEnabled(featureKey, featureContext);
|
|
479
|
+
|
|
480
|
+
if (!isEnabled) {
|
|
481
|
+
const fallback = this.reflector.get<any>(FEATURE_FALLBACK, context.getHandler());
|
|
482
|
+
if (fallback !== undefined) {
|
|
483
|
+
// Store fallback in request for controller to use
|
|
484
|
+
request.featureFallback = fallback;
|
|
485
|
+
return true;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
throw new ForbiddenException(\`Feature '\${featureKey}' is not available\`);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Check variant if specified
|
|
492
|
+
const requiredVariant = this.reflector.get<string>('feature_variant', context.getHandler());
|
|
493
|
+
if (requiredVariant) {
|
|
494
|
+
const variant = this.featureService.getVariant(featureKey, featureContext);
|
|
495
|
+
if (!variant || variant.name !== requiredVariant) {
|
|
496
|
+
throw new ForbiddenException(\`Feature variant '\${requiredVariant}' is not available\`);
|
|
497
|
+
}
|
|
498
|
+
request.featureVariant = variant;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
return true;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
private extractContext(request: any): FeatureContext {
|
|
505
|
+
return {
|
|
506
|
+
userId: request.user?.id,
|
|
507
|
+
userGroups: request.user?.groups || request.user?.roles,
|
|
508
|
+
sessionId: request.sessionID || request.headers['x-session-id'],
|
|
509
|
+
attributes: {
|
|
510
|
+
ip: request.ip,
|
|
511
|
+
userAgent: request.headers['user-agent'],
|
|
512
|
+
...request.user,
|
|
513
|
+
},
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
`;
|
|
518
|
+
}
|
|
519
|
+
function generateABTestingService() {
|
|
520
|
+
return `/**
|
|
521
|
+
* A/B Testing Service
|
|
522
|
+
* Manage experiments and variant assignments
|
|
523
|
+
*/
|
|
524
|
+
|
|
525
|
+
import { Injectable, Logger } from '@nestjs/common';
|
|
526
|
+
import { EventEmitter2 } from '@nestjs/event-emitter';
|
|
527
|
+
|
|
528
|
+
export interface Experiment {
|
|
529
|
+
id: string;
|
|
530
|
+
name: string;
|
|
531
|
+
description?: string;
|
|
532
|
+
variants: ExperimentVariant[];
|
|
533
|
+
status: 'draft' | 'running' | 'paused' | 'completed';
|
|
534
|
+
targetPercentage: number;
|
|
535
|
+
startDate?: Date;
|
|
536
|
+
endDate?: Date;
|
|
537
|
+
metrics: string[];
|
|
538
|
+
createdAt: Date;
|
|
539
|
+
updatedAt: Date;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
export interface ExperimentVariant {
|
|
543
|
+
id: string;
|
|
544
|
+
name: string;
|
|
545
|
+
weight: number;
|
|
546
|
+
isControl: boolean;
|
|
547
|
+
payload?: any;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
export interface ExperimentAssignment {
|
|
551
|
+
experimentId: string;
|
|
552
|
+
variantId: string;
|
|
553
|
+
userId: string;
|
|
554
|
+
assignedAt: Date;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
export interface ExperimentResult {
|
|
558
|
+
experimentId: string;
|
|
559
|
+
variantId: string;
|
|
560
|
+
metric: string;
|
|
561
|
+
value: number;
|
|
562
|
+
sampleSize: number;
|
|
563
|
+
confidence?: number;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
@Injectable()
|
|
567
|
+
export class ABTestingService {
|
|
568
|
+
private readonly logger = new Logger(ABTestingService.name);
|
|
569
|
+
private readonly experiments = new Map<string, Experiment>();
|
|
570
|
+
private readonly assignments = new Map<string, ExperimentAssignment>();
|
|
571
|
+
|
|
572
|
+
constructor(private readonly eventEmitter: EventEmitter2) {}
|
|
573
|
+
|
|
574
|
+
/**
|
|
575
|
+
* Create an experiment
|
|
576
|
+
*/
|
|
577
|
+
createExperiment(config: Omit<Experiment, 'createdAt' | 'updatedAt' | 'status'>): Experiment {
|
|
578
|
+
const experiment: Experiment = {
|
|
579
|
+
...config,
|
|
580
|
+
status: 'draft',
|
|
581
|
+
createdAt: new Date(),
|
|
582
|
+
updatedAt: new Date(),
|
|
583
|
+
};
|
|
584
|
+
|
|
585
|
+
this.experiments.set(experiment.id, experiment);
|
|
586
|
+
this.eventEmitter.emit('experiment.created', experiment);
|
|
587
|
+
|
|
588
|
+
return experiment;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* Start an experiment
|
|
593
|
+
*/
|
|
594
|
+
startExperiment(id: string): Experiment {
|
|
595
|
+
const experiment = this.experiments.get(id);
|
|
596
|
+
if (!experiment) {
|
|
597
|
+
throw new Error(\`Experiment '\${id}' not found\`);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
experiment.status = 'running';
|
|
601
|
+
experiment.startDate = new Date();
|
|
602
|
+
experiment.updatedAt = new Date();
|
|
603
|
+
|
|
604
|
+
this.eventEmitter.emit('experiment.started', experiment);
|
|
605
|
+
return experiment;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/**
|
|
609
|
+
* Stop an experiment
|
|
610
|
+
*/
|
|
611
|
+
stopExperiment(id: string): Experiment {
|
|
612
|
+
const experiment = this.experiments.get(id);
|
|
613
|
+
if (!experiment) {
|
|
614
|
+
throw new Error(\`Experiment '\${id}' not found\`);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
experiment.status = 'completed';
|
|
618
|
+
experiment.endDate = new Date();
|
|
619
|
+
experiment.updatedAt = new Date();
|
|
620
|
+
|
|
621
|
+
this.eventEmitter.emit('experiment.stopped', experiment);
|
|
622
|
+
return experiment;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
/**
|
|
626
|
+
* Get variant for user
|
|
627
|
+
*/
|
|
628
|
+
getVariant(experimentId: string, userId: string): ExperimentVariant | null {
|
|
629
|
+
const experiment = this.experiments.get(experimentId);
|
|
630
|
+
if (!experiment || experiment.status !== 'running') {
|
|
631
|
+
return null;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// Check existing assignment
|
|
635
|
+
const assignmentKey = \`\${experimentId}:\${userId}\`;
|
|
636
|
+
const existing = this.assignments.get(assignmentKey);
|
|
637
|
+
if (existing) {
|
|
638
|
+
return experiment.variants.find(v => v.id === existing.variantId) || null;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// Check if user is in target percentage
|
|
642
|
+
if (!this.isInTarget(experimentId, userId, experiment.targetPercentage)) {
|
|
643
|
+
return null;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// Assign variant
|
|
647
|
+
const variant = this.selectVariant(experimentId, userId, experiment.variants);
|
|
648
|
+
|
|
649
|
+
const assignment: ExperimentAssignment = {
|
|
650
|
+
experimentId,
|
|
651
|
+
variantId: variant.id,
|
|
652
|
+
userId,
|
|
653
|
+
assignedAt: new Date(),
|
|
654
|
+
};
|
|
655
|
+
this.assignments.set(assignmentKey, assignment);
|
|
656
|
+
|
|
657
|
+
this.eventEmitter.emit('experiment.assigned', {
|
|
658
|
+
experimentId,
|
|
659
|
+
variantId: variant.id,
|
|
660
|
+
userId,
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
return variant;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* Track experiment event
|
|
668
|
+
*/
|
|
669
|
+
trackEvent(experimentId: string, userId: string, metric: string, value: number = 1): void {
|
|
670
|
+
const assignmentKey = \`\${experimentId}:\${userId}\`;
|
|
671
|
+
const assignment = this.assignments.get(assignmentKey);
|
|
672
|
+
|
|
673
|
+
if (!assignment) {
|
|
674
|
+
this.logger.warn(\`No assignment found for user \${userId} in experiment \${experimentId}\`);
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
this.eventEmitter.emit('experiment.event', {
|
|
679
|
+
experimentId,
|
|
680
|
+
variantId: assignment.variantId,
|
|
681
|
+
userId,
|
|
682
|
+
metric,
|
|
683
|
+
value,
|
|
684
|
+
timestamp: new Date(),
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
/**
|
|
689
|
+
* Get experiment results
|
|
690
|
+
*/
|
|
691
|
+
getResults(experimentId: string): ExperimentResult[] {
|
|
692
|
+
// This would aggregate tracked events
|
|
693
|
+
// Implementation depends on your analytics storage
|
|
694
|
+
return [];
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
/**
|
|
698
|
+
* List all experiments
|
|
699
|
+
*/
|
|
700
|
+
listExperiments(): Experiment[] {
|
|
701
|
+
return Array.from(this.experiments.values());
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
/**
|
|
705
|
+
* Get experiment by ID
|
|
706
|
+
*/
|
|
707
|
+
getExperiment(id: string): Experiment | undefined {
|
|
708
|
+
return this.experiments.get(id);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
private isInTarget(experimentId: string, userId: string, percentage: number): boolean {
|
|
712
|
+
const hash = this.hashString(\`\${experimentId}:target:\${userId}\`);
|
|
713
|
+
return (hash % 100) < percentage;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
private selectVariant(experimentId: string, userId: string, variants: ExperimentVariant[]): ExperimentVariant {
|
|
717
|
+
const hash = this.hashString(\`\${experimentId}:variant:\${userId}\`);
|
|
718
|
+
const totalWeight = variants.reduce((sum, v) => sum + v.weight, 0);
|
|
719
|
+
let random = hash % totalWeight;
|
|
720
|
+
|
|
721
|
+
for (const variant of variants) {
|
|
722
|
+
random -= variant.weight;
|
|
723
|
+
if (random < 0) {
|
|
724
|
+
return variant;
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
return variants[variants.length - 1];
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
private hashString(str: string): number {
|
|
732
|
+
let hash = 0;
|
|
733
|
+
for (let i = 0; i < str.length; i++) {
|
|
734
|
+
const char = str.charCodeAt(i);
|
|
735
|
+
hash = ((hash << 5) - hash) + char;
|
|
736
|
+
hash = hash & hash;
|
|
737
|
+
}
|
|
738
|
+
return Math.abs(hash);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
`;
|
|
742
|
+
}
|
|
743
|
+
function generateFeatureFlagEntity() {
|
|
744
|
+
return `import {
|
|
745
|
+
Entity,
|
|
746
|
+
PrimaryColumn,
|
|
747
|
+
Column,
|
|
748
|
+
CreateDateColumn,
|
|
749
|
+
UpdateDateColumn,
|
|
750
|
+
} from 'typeorm';
|
|
751
|
+
|
|
752
|
+
@Entity('feature_flags')
|
|
753
|
+
export class FeatureFlagEntity {
|
|
754
|
+
@PrimaryColumn()
|
|
755
|
+
key: string;
|
|
756
|
+
|
|
757
|
+
@Column({ default: false })
|
|
758
|
+
enabled: boolean;
|
|
759
|
+
|
|
760
|
+
@Column({ nullable: true })
|
|
761
|
+
description: string;
|
|
762
|
+
|
|
763
|
+
@Column({ type: 'jsonb', nullable: true })
|
|
764
|
+
conditions: any;
|
|
765
|
+
|
|
766
|
+
@Column({ type: 'jsonb', nullable: true })
|
|
767
|
+
variants: any;
|
|
768
|
+
|
|
769
|
+
@Column({ type: 'float', nullable: true })
|
|
770
|
+
percentage: number;
|
|
771
|
+
|
|
772
|
+
@Column({ type: 'jsonb', nullable: true })
|
|
773
|
+
metadata: any;
|
|
774
|
+
|
|
775
|
+
@CreateDateColumn()
|
|
776
|
+
createdAt: Date;
|
|
777
|
+
|
|
778
|
+
@UpdateDateColumn()
|
|
779
|
+
updatedAt: Date;
|
|
780
|
+
}
|
|
781
|
+
`;
|
|
782
|
+
}
|
|
783
|
+
function generateFeatureModule() {
|
|
784
|
+
return `import { Module, Global, DynamicModule } from '@nestjs/common';
|
|
785
|
+
import { TypeOrmModule } from '@nestjs/typeorm';
|
|
786
|
+
import { FeatureFlagService } from './feature-flag.service';
|
|
787
|
+
import { FeatureGuard } from './feature.guard';
|
|
788
|
+
import { ABTestingService } from './ab-testing.service';
|
|
789
|
+
import { FeatureFlagEntity } from './feature-flag.entity';
|
|
790
|
+
|
|
791
|
+
export interface FeatureModuleOptions {
|
|
792
|
+
provider?: 'memory' | 'database' | 'redis';
|
|
793
|
+
refreshInterval?: number;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
@Global()
|
|
797
|
+
@Module({})
|
|
798
|
+
export class FeatureModule {
|
|
799
|
+
static forRoot(options: FeatureModuleOptions = {}): DynamicModule {
|
|
800
|
+
const imports = [];
|
|
801
|
+
|
|
802
|
+
if (options.provider === 'database') {
|
|
803
|
+
imports.push(TypeOrmModule.forFeature([FeatureFlagEntity]));
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
return {
|
|
807
|
+
module: FeatureModule,
|
|
808
|
+
imports,
|
|
809
|
+
providers: [
|
|
810
|
+
{
|
|
811
|
+
provide: 'FEATURE_OPTIONS',
|
|
812
|
+
useValue: options,
|
|
813
|
+
},
|
|
814
|
+
FeatureFlagService,
|
|
815
|
+
ABTestingService,
|
|
816
|
+
FeatureGuard,
|
|
817
|
+
],
|
|
818
|
+
exports: [FeatureFlagService, ABTestingService, FeatureGuard],
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
`;
|
|
823
|
+
}
|
|
824
|
+
//# sourceMappingURL=feature-flags.js.map
|