@plyaz/core 1.1.0 → 1.2.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/dist/backend/featureFlags/config/feature-flag.config.d.ts +111 -0
- package/dist/backend/featureFlags/config/feature-flag.config.d.ts.map +1 -0
- package/dist/backend/featureFlags/config/validation.d.ts +181 -0
- package/dist/backend/featureFlags/config/validation.d.ts.map +1 -0
- package/dist/backend/featureFlags/database/connection.d.ts +321 -0
- package/dist/backend/featureFlags/database/connection.d.ts.map +1 -0
- package/dist/backend/featureFlags/database/repository.d.ts +518 -0
- package/dist/backend/featureFlags/database/repository.d.ts.map +1 -0
- package/dist/backend/featureFlags/decorators/feature-disabled.decorator.d.ts +6 -0
- package/dist/backend/featureFlags/decorators/feature-disabled.decorator.d.ts.map +1 -0
- package/dist/backend/featureFlags/decorators/feature-enabled.decorator.d.ts +8 -0
- package/dist/backend/featureFlags/decorators/feature-enabled.decorator.d.ts.map +1 -0
- package/dist/backend/featureFlags/decorators/feature-flag.decorator.d.ts +11 -0
- package/dist/backend/featureFlags/decorators/feature-flag.decorator.d.ts.map +1 -0
- package/dist/backend/featureFlags/feature-flag.controller.d.ts +1 -2
- package/dist/backend/featureFlags/feature-flag.controller.d.ts.map +1 -1
- package/dist/backend/featureFlags/feature-flag.module.d.ts +2 -3
- package/dist/backend/featureFlags/feature-flag.module.d.ts.map +1 -1
- package/dist/backend/featureFlags/feature-flag.service.d.ts +149 -8
- package/dist/backend/featureFlags/feature-flag.service.d.ts.map +1 -1
- package/dist/backend/featureFlags/guards/feature-flag.guard.d.ts +19 -0
- package/dist/backend/featureFlags/guards/feature-flag.guard.d.ts.map +1 -0
- package/dist/backend/featureFlags/index.d.ts +10 -36
- package/dist/backend/featureFlags/index.d.ts.map +1 -1
- package/dist/backend/featureFlags/interceptors/error-handling-interceptor.d.ts +16 -0
- package/dist/backend/featureFlags/interceptors/error-handling-interceptor.d.ts.map +1 -0
- package/dist/backend/featureFlags/interceptors/feature-flag-logging-interceptor.d.ts +18 -0
- package/dist/backend/featureFlags/interceptors/feature-flag-logging-interceptor.d.ts.map +1 -0
- package/dist/backend/featureFlags/middleware/feature-flag-middleware.d.ts +167 -0
- package/dist/backend/featureFlags/middleware/feature-flag-middleware.d.ts.map +1 -0
- package/dist/base/cache/feature/caching.d.ts +16 -0
- package/dist/base/cache/feature/caching.d.ts.map +1 -0
- package/dist/base/cache/index.d.ts +1 -0
- package/dist/base/cache/index.d.ts.map +1 -1
- package/dist/domain/featureFlags/providers/database.d.ts +17 -12
- package/dist/domain/featureFlags/providers/database.d.ts.map +1 -1
- package/dist/frontend/index.d.ts +1 -0
- package/dist/frontend/index.d.ts.map +1 -1
- package/dist/frontend/providers/ApiProvider.d.ts +41 -0
- package/dist/frontend/providers/ApiProvider.d.ts.map +1 -0
- package/dist/frontend/providers/index.d.ts +7 -0
- package/dist/frontend/providers/index.d.ts.map +1 -0
- package/dist/index.cjs +2325 -273
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +2315 -275
- package/dist/index.mjs.map +1 -1
- package/dist/services/ApiClientService.d.ts +90 -0
- package/dist/services/ApiClientService.d.ts.map +1 -0
- package/dist/services/index.d.ts +8 -0
- package/dist/services/index.d.ts.map +1 -0
- package/dist/utils/common/index.d.ts +1 -1
- package/dist/utils/common/index.d.ts.map +1 -1
- package/dist/utils/common/validation.d.ts +20 -0
- package/dist/utils/common/validation.d.ts.map +1 -0
- package/dist/utils/db/databaseService.d.ts +6 -0
- package/dist/utils/db/databaseService.d.ts.map +1 -0
- package/dist/utils/db/index.d.ts +2 -0
- package/dist/utils/db/index.d.ts.map +1 -0
- package/dist/web_app/auth/add_user.d.ts +3 -0
- package/dist/web_app/auth/add_user.d.ts.map +1 -0
- package/dist/web_app/auth/update_user.d.ts +2 -0
- package/dist/web_app/auth/update_user.d.ts.map +1 -0
- package/package.json +20 -7
package/dist/index.cjs
CHANGED
|
@@ -1,14 +1,22 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
var config = require('@plyaz/config');
|
|
4
|
+
var types = require('@plyaz/types');
|
|
5
|
+
var common = require('@nestjs/common');
|
|
6
|
+
var rxjs = require('rxjs');
|
|
4
7
|
var fs = require('fs');
|
|
5
8
|
var path = require('path');
|
|
6
9
|
var util = require('util');
|
|
7
10
|
var url = require('url');
|
|
8
11
|
var yaml = require('yaml');
|
|
9
|
-
var
|
|
12
|
+
var db = require('@plyaz/db');
|
|
13
|
+
var errors = require('@plyaz/errors');
|
|
14
|
+
var db$1 = require('@plyaz/types/db');
|
|
15
|
+
var crypto = require('crypto');
|
|
16
|
+
var operators = require('rxjs/operators');
|
|
10
17
|
var React = require('react');
|
|
11
18
|
var jsxRuntime = require('react/jsx-runtime');
|
|
19
|
+
var api = require('@plyaz/api');
|
|
12
20
|
|
|
13
21
|
var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
|
|
14
22
|
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
@@ -221,6 +229,10 @@ var ValueUtils = {
|
|
|
221
229
|
return current;
|
|
222
230
|
}, "getNestedProperty")
|
|
223
231
|
};
|
|
232
|
+
var isString = /* @__PURE__ */ __name((value) => typeof value === "string", "isString");
|
|
233
|
+
var isDefined = /* @__PURE__ */ __name((value) => value !== void 0, "isDefined");
|
|
234
|
+
var isNumber = /* @__PURE__ */ __name((value) => typeof value === "number", "isNumber");
|
|
235
|
+
var getValidProviders = /* @__PURE__ */ __name(() => Object.values(types.FEATURE_FLAG_PROVIDERS), "getValidProviders");
|
|
224
236
|
var FeatureFlagContextBuilder = class _FeatureFlagContextBuilder {
|
|
225
237
|
static {
|
|
226
238
|
__name(this, "FeatureFlagContextBuilder");
|
|
@@ -1364,6 +1376,27 @@ var RedisCacheStrategy = class {
|
|
|
1364
1376
|
return `${this.keyPrefix}${key}`;
|
|
1365
1377
|
}
|
|
1366
1378
|
};
|
|
1379
|
+
exports.Caching = class Caching {
|
|
1380
|
+
// Map to store cached responses keyed by URL + active feature flags
|
|
1381
|
+
cache = /* @__PURE__ */ new Map();
|
|
1382
|
+
intercept(context, next) {
|
|
1383
|
+
const request = context.switchToHttp().getRequest();
|
|
1384
|
+
const flagsKey = JSON.stringify(request.featureFlags ?? {});
|
|
1385
|
+
const cacheKey = `${request.url}|${flagsKey}`;
|
|
1386
|
+
if (this.cache.has(cacheKey)) {
|
|
1387
|
+
return rxjs.of(this.cache.get(cacheKey));
|
|
1388
|
+
}
|
|
1389
|
+
return next.handle().pipe(
|
|
1390
|
+
rxjs.tap((response) => {
|
|
1391
|
+
this.cache.set(cacheKey, response);
|
|
1392
|
+
})
|
|
1393
|
+
);
|
|
1394
|
+
}
|
|
1395
|
+
};
|
|
1396
|
+
__name(exports.Caching, "Caching");
|
|
1397
|
+
exports.Caching = __decorateClass([
|
|
1398
|
+
common.Injectable()
|
|
1399
|
+
], exports.Caching);
|
|
1367
1400
|
|
|
1368
1401
|
// src/base/cache/index.ts
|
|
1369
1402
|
var CacheManager = class {
|
|
@@ -2692,210 +2725,1243 @@ Example: "https://api.plyaz.co.uk"`
|
|
|
2692
2725
|
};
|
|
2693
2726
|
}
|
|
2694
2727
|
};
|
|
2695
|
-
|
|
2696
|
-
// src/domain/featureFlags/providers/database.ts
|
|
2697
|
-
var DatabaseFeatureFlagProvider = class extends FeatureFlagProvider {
|
|
2728
|
+
var DatabaseConnectionManager = class _DatabaseConnectionManager {
|
|
2698
2729
|
static {
|
|
2699
|
-
__name(this, "
|
|
2730
|
+
__name(this, "DatabaseConnectionManager");
|
|
2700
2731
|
}
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
* @param config - Provider configuration with database settings
|
|
2705
|
-
* @throws Error indicating that @plyaz/db implementation is required
|
|
2706
|
-
*/
|
|
2707
|
-
constructor(config, features) {
|
|
2708
|
-
super(config, features);
|
|
2709
|
-
this.validateConfig();
|
|
2710
|
-
throw new Error("Database provider requires @plyaz/db package implementation");
|
|
2732
|
+
static instance;
|
|
2733
|
+
databaseService = null;
|
|
2734
|
+
constructor() {
|
|
2711
2735
|
}
|
|
2712
2736
|
/**
|
|
2713
|
-
*
|
|
2714
|
-
* Currently throws an error as the database implementation is not ready.
|
|
2737
|
+
* Gets the singleton instance of DatabaseConnectionManager
|
|
2715
2738
|
*
|
|
2716
|
-
* @
|
|
2717
|
-
*
|
|
2718
|
-
*
|
|
2739
|
+
* @description Returns the single instance of the connection manager, creating it if it doesn't exist.
|
|
2740
|
+
* This ensures only one database connection pool is maintained across the application.
|
|
2741
|
+
*
|
|
2742
|
+
* @returns {DatabaseConnectionManager} The singleton instance
|
|
2743
|
+
*
|
|
2744
|
+
* @example Getting the Instance
|
|
2745
|
+
* ```typescript
|
|
2746
|
+
* // Always returns the same instance
|
|
2747
|
+
* const manager1 = DatabaseConnectionManager.getInstance();
|
|
2748
|
+
* const manager2 = DatabaseConnectionManager.getInstance();
|
|
2749
|
+
* console.log(manager1 === manager2); // true
|
|
2750
|
+
*
|
|
2751
|
+
* // Use in services
|
|
2752
|
+
* class FeatureFlagRepository {
|
|
2753
|
+
* private connectionManager = DatabaseConnectionManager.getInstance();
|
|
2754
|
+
*
|
|
2755
|
+
* async getFlags() {
|
|
2756
|
+
* const db = this.connectionManager.getDatabase();
|
|
2757
|
+
* return db.query('feature_flags');
|
|
2758
|
+
* }
|
|
2759
|
+
* }
|
|
2760
|
+
* ```
|
|
2719
2761
|
*/
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2762
|
+
static getInstance() {
|
|
2763
|
+
if (!_DatabaseConnectionManager.instance) {
|
|
2764
|
+
_DatabaseConnectionManager.instance = new _DatabaseConnectionManager();
|
|
2765
|
+
}
|
|
2766
|
+
return _DatabaseConnectionManager.instance;
|
|
2724
2767
|
}
|
|
2725
2768
|
/**
|
|
2726
|
-
*
|
|
2769
|
+
* Initializes the database connection and registers feature flag tables
|
|
2727
2770
|
*
|
|
2728
|
-
* @
|
|
2729
|
-
*
|
|
2771
|
+
* @description Sets up the Supabase connection using environment variables and registers
|
|
2772
|
+
* all feature flag related tables. This method is idempotent - calling it multiple
|
|
2773
|
+
* times won't create additional connections.
|
|
2774
|
+
*
|
|
2775
|
+
* **What it does:**
|
|
2776
|
+
* 1. Validates required environment variables
|
|
2777
|
+
* 2. Creates Supabase database service
|
|
2778
|
+
* 3. Configures caching (5-minute TTL)
|
|
2779
|
+
* 4. Registers feature flag tables with proper ID columns
|
|
2780
|
+
* 5. Disables audit logging (until tables are created)
|
|
2781
|
+
*
|
|
2782
|
+
* @throws {DatabaseError} When environment variables are missing or connection fails
|
|
2783
|
+
*
|
|
2784
|
+
* @example Initialization Process
|
|
2785
|
+
* ```typescript
|
|
2786
|
+
* // Called automatically by FeatureFlagService.onModuleInit()
|
|
2787
|
+
* const manager = DatabaseConnectionManager.getInstance();
|
|
2788
|
+
*
|
|
2789
|
+
* try {
|
|
2790
|
+
* await manager.initialize();
|
|
2791
|
+
* console.log('Database connection established');
|
|
2792
|
+
* } catch (error) {
|
|
2793
|
+
* console.error('Failed to connect to database:', error.message);
|
|
2794
|
+
* // Error: Missing environment variables for Supabase connection
|
|
2795
|
+
* }
|
|
2796
|
+
* ```
|
|
2797
|
+
*
|
|
2798
|
+
* @example Environment Variable Validation
|
|
2799
|
+
* ```typescript
|
|
2800
|
+
* // If any of these are missing, initialization fails:
|
|
2801
|
+
* // SUPABASE_URL=undefined
|
|
2802
|
+
* // SUPABASE_SERVICE_ROLE_KEY=undefined
|
|
2803
|
+
* // SUPABASE_ANON_PUBLIC_KEY=undefined
|
|
2804
|
+
*
|
|
2805
|
+
* // Throws: DatabaseError with code CONFIG_REQUIRED
|
|
2806
|
+
* ```
|
|
2807
|
+
*
|
|
2808
|
+
* @example Registered Tables
|
|
2809
|
+
* ```typescript
|
|
2810
|
+
* // These tables are automatically registered:
|
|
2811
|
+
* // - feature_flags (ID: key)
|
|
2812
|
+
* // - feature_flag_rules (ID: id)
|
|
2813
|
+
* // - feature_flag_overrides (ID: id)
|
|
2814
|
+
* // - feature_flag_evaluations (ID: id)
|
|
2815
|
+
* ```
|
|
2730
2816
|
*/
|
|
2731
|
-
|
|
2732
|
-
this.
|
|
2733
|
-
|
|
2734
|
-
this.validateConnectionString();
|
|
2735
|
-
this.logConfigurationStatus();
|
|
2736
|
-
}
|
|
2737
|
-
validateProviderType() {
|
|
2738
|
-
if (this.config.provider !== "database") {
|
|
2739
|
-
throw new Error('Database provider requires provider to be set to "database"');
|
|
2817
|
+
async initialize() {
|
|
2818
|
+
if (this.databaseService) {
|
|
2819
|
+
return;
|
|
2740
2820
|
}
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2821
|
+
const supabaseUrl = process.env.SUPABASE_URL;
|
|
2822
|
+
const supabaseServiceKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
|
|
2823
|
+
const supabaseAnonKey = process.env.SUPABASE_ANON_PUBLIC_KEY;
|
|
2824
|
+
if (!supabaseUrl || !supabaseServiceKey || !supabaseAnonKey) {
|
|
2825
|
+
throw new errors.DatabaseError(
|
|
2826
|
+
"Missing environment variables for Supabase connection. Please check your .env file.",
|
|
2827
|
+
types.DATABASE_ERROR_CODES.CONFIG_REQUIRED
|
|
2746
2828
|
);
|
|
2747
2829
|
}
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2830
|
+
this.databaseService = await db.createDatabaseService({
|
|
2831
|
+
adapter: db$1.ADAPTER_TYPES.SUPABASE,
|
|
2832
|
+
config: {
|
|
2833
|
+
supabaseUrl,
|
|
2834
|
+
supabaseServiceKey,
|
|
2835
|
+
supabaseAnonKey
|
|
2836
|
+
},
|
|
2837
|
+
// Enable working extensions only
|
|
2838
|
+
audit: {
|
|
2839
|
+
enabled: false,
|
|
2840
|
+
retentionDays: 90
|
|
2841
|
+
},
|
|
2842
|
+
cache: {
|
|
2843
|
+
enabled: true,
|
|
2844
|
+
provider: "memory",
|
|
2845
|
+
ttl: 300
|
|
2846
|
+
},
|
|
2847
|
+
softDelete: {
|
|
2848
|
+
enabled: false,
|
|
2849
|
+
field: "deleted_at"
|
|
2850
|
+
}
|
|
2851
|
+
});
|
|
2852
|
+
const serviceWithAdapter = this.databaseService;
|
|
2853
|
+
if (serviceWithAdapter.adapter?.registerTable) {
|
|
2854
|
+
serviceWithAdapter.adapter.registerTable("feature_flags", "feature_flags", "key");
|
|
2855
|
+
serviceWithAdapter.adapter.registerTable("feature_flag_rules", "feature_flag_rules", "id");
|
|
2856
|
+
serviceWithAdapter.adapter.registerTable(
|
|
2857
|
+
"feature_flag_overrides",
|
|
2858
|
+
"feature_flag_overrides",
|
|
2859
|
+
"id"
|
|
2754
2860
|
);
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
Examples:
|
|
2760
|
-
- PostgreSQL: "postgresql://user:pass@localhost:5432/plyaz"
|
|
2761
|
-
- MySQL: "mysql://user:pass@localhost:3306/plyaz"`
|
|
2861
|
+
serviceWithAdapter.adapter.registerTable(
|
|
2862
|
+
"feature_flag_evaluations",
|
|
2863
|
+
"feature_flag_evaluations",
|
|
2864
|
+
"id"
|
|
2762
2865
|
);
|
|
2763
2866
|
}
|
|
2764
|
-
if (!tableName || typeof tableName !== "string") {
|
|
2765
|
-
throw new Error("Database provider requires databaseConfig.tableName");
|
|
2766
|
-
}
|
|
2767
|
-
}
|
|
2768
|
-
logConfigurationStatus() {
|
|
2769
|
-
const { connectionString, tableName } = this.config.databaseConfig;
|
|
2770
|
-
this.log("Database provider configuration is valid, but implementation is not ready");
|
|
2771
|
-
this.log("Connection String:", this.maskConnectionString(connectionString));
|
|
2772
|
-
this.log("Table Name:", tableName ?? "feature_flags (default)");
|
|
2773
2867
|
}
|
|
2774
2868
|
/**
|
|
2775
|
-
*
|
|
2869
|
+
* Gets the initialized database service instance
|
|
2776
2870
|
*
|
|
2777
|
-
* @
|
|
2778
|
-
*
|
|
2779
|
-
* @returns True if valid PostgreSQL URL
|
|
2780
|
-
*/
|
|
2781
|
-
isValidPostgresUrl(url) {
|
|
2782
|
-
return url.startsWith("postgresql://") || url.startsWith("postgres://");
|
|
2783
|
-
}
|
|
2784
|
-
/**
|
|
2785
|
-
* Validates MySQL URL format.
|
|
2871
|
+
* @description Returns the database service for performing queries. The database must be
|
|
2872
|
+
* initialized first using initialize() method, otherwise this will throw an error.
|
|
2786
2873
|
*
|
|
2787
|
-
* @
|
|
2788
|
-
* @
|
|
2789
|
-
*
|
|
2874
|
+
* @returns {DatabaseServiceInterface} The database service instance
|
|
2875
|
+
* @throws {DatabaseError} When database is not initialized
|
|
2876
|
+
*
|
|
2877
|
+
* @example Basic Usage
|
|
2878
|
+
* ```typescript
|
|
2879
|
+
* const manager = DatabaseConnectionManager.getInstance();
|
|
2880
|
+
* await manager.initialize(); // Must initialize first
|
|
2881
|
+
*
|
|
2882
|
+
* const db = manager.getDatabase();
|
|
2883
|
+
*
|
|
2884
|
+
* // Query feature flags
|
|
2885
|
+
* const result = await db.query('feature_flags', {
|
|
2886
|
+
* filters: [{ field: 'is_enabled', operator: 'eq', value: true }],
|
|
2887
|
+
* sort: [{ field: 'key', direction: 'asc' }]
|
|
2888
|
+
* });
|
|
2889
|
+
*
|
|
2890
|
+
* if (result.success) {
|
|
2891
|
+
* console.log('Found flags:', result.value.data);
|
|
2892
|
+
* }
|
|
2893
|
+
* ```
|
|
2894
|
+
*
|
|
2895
|
+
* @example Error Handling
|
|
2896
|
+
* ```typescript
|
|
2897
|
+
* const manager = DatabaseConnectionManager.getInstance();
|
|
2898
|
+
* // Don't call initialize()
|
|
2899
|
+
*
|
|
2900
|
+
* try {
|
|
2901
|
+
* const db = manager.getDatabase();
|
|
2902
|
+
* } catch (error) {
|
|
2903
|
+
* console.error(error.message);
|
|
2904
|
+
* // "Database not initialized. Call initialize() first."
|
|
2905
|
+
* }
|
|
2906
|
+
* ```
|
|
2907
|
+
*
|
|
2908
|
+
* @example CRUD Operations
|
|
2909
|
+
* ```typescript
|
|
2910
|
+
* const db = manager.getDatabase();
|
|
2911
|
+
*
|
|
2912
|
+
* // Create a flag
|
|
2913
|
+
* const createResult = await db.create('feature_flags', {
|
|
2914
|
+
* key: 'NEW_FEATURE',
|
|
2915
|
+
* value: true,
|
|
2916
|
+
* is_enabled: true
|
|
2917
|
+
* });
|
|
2918
|
+
*
|
|
2919
|
+
* // Update a flag
|
|
2920
|
+
* const updateResult = await db.update('feature_flags', 'NEW_FEATURE', {
|
|
2921
|
+
* value: false
|
|
2922
|
+
* });
|
|
2923
|
+
*
|
|
2924
|
+
* // Delete a flag
|
|
2925
|
+
* const deleteResult = await db.delete('feature_flags', 'NEW_FEATURE');
|
|
2926
|
+
* ```
|
|
2790
2927
|
*/
|
|
2791
|
-
|
|
2792
|
-
|
|
2928
|
+
getDatabase() {
|
|
2929
|
+
if (!this.databaseService) {
|
|
2930
|
+
throw new errors.DatabaseError(
|
|
2931
|
+
"Database not initialized. Call initialize() first.",
|
|
2932
|
+
types.DATABASE_ERROR_CODES.INIT_FAILED
|
|
2933
|
+
);
|
|
2934
|
+
}
|
|
2935
|
+
return this.databaseService;
|
|
2793
2936
|
}
|
|
2794
2937
|
/**
|
|
2795
|
-
*
|
|
2938
|
+
* Executes a database transaction with automatic rollback on failure
|
|
2796
2939
|
*
|
|
2797
|
-
* @
|
|
2798
|
-
*
|
|
2799
|
-
*
|
|
2940
|
+
* @description Provides atomic database operations by wrapping multiple queries in a transaction.
|
|
2941
|
+
* If any operation fails, all changes are rolled back automatically. This is essential for
|
|
2942
|
+
* maintaining data consistency when creating flags with rules or performing bulk operations.
|
|
2943
|
+
*
|
|
2944
|
+
* @template T The return type of the transaction callback
|
|
2945
|
+
* @param {Function} callback - Function that receives transaction object and performs operations
|
|
2946
|
+
* @returns {Promise<T>} The result of the transaction callback
|
|
2947
|
+
* @throws {DatabaseError} When transaction fails or returns no value
|
|
2948
|
+
*
|
|
2949
|
+
* @example Atomic Flag Creation with Rules
|
|
2950
|
+
* ```typescript
|
|
2951
|
+
* const manager = DatabaseConnectionManager.getInstance();
|
|
2952
|
+
*
|
|
2953
|
+
* const result = await manager.transaction(async (tx) => {
|
|
2954
|
+
* // Create the flag
|
|
2955
|
+
* const flag = await tx.create('feature_flags', {
|
|
2956
|
+
* key: 'PREMIUM_CHECKOUT',
|
|
2957
|
+
* value: true,
|
|
2958
|
+
* is_enabled: true,
|
|
2959
|
+
* description: 'Premium checkout flow'
|
|
2960
|
+
* });
|
|
2961
|
+
*
|
|
2962
|
+
* // Create targeting rule
|
|
2963
|
+
* const rule = await tx.create('feature_flag_rules', {
|
|
2964
|
+
* flag_key: 'PREMIUM_CHECKOUT',
|
|
2965
|
+
* name: 'Premium Users Only',
|
|
2966
|
+
* conditions: [
|
|
2967
|
+
* { field: 'userRole', operator: 'equals', value: 'premium' },
|
|
2968
|
+
* { field: 'subscriptionActive', operator: 'equals', value: true }
|
|
2969
|
+
* ],
|
|
2970
|
+
* value: true,
|
|
2971
|
+
* priority: 100
|
|
2972
|
+
* });
|
|
2973
|
+
*
|
|
2974
|
+
* return { flag, rule };
|
|
2975
|
+
* });
|
|
2976
|
+
*
|
|
2977
|
+
* console.log('Created flag and rule:', result);
|
|
2978
|
+
* ```
|
|
2979
|
+
*
|
|
2980
|
+
* @example Bulk Flag Updates
|
|
2981
|
+
* ```typescript
|
|
2982
|
+
* const flagsToUpdate = ['FEATURE_A', 'FEATURE_B', 'FEATURE_C'];
|
|
2983
|
+
*
|
|
2984
|
+
* await manager.transaction(async (tx) => {
|
|
2985
|
+
* for (const flagKey of flagsToUpdate) {
|
|
2986
|
+
* await tx.update('feature_flags', flagKey, {
|
|
2987
|
+
* is_enabled: false,
|
|
2988
|
+
* updated_at: new Date().toISOString()
|
|
2989
|
+
* });
|
|
2990
|
+
* }
|
|
2991
|
+
*
|
|
2992
|
+
* // Log the bulk update
|
|
2993
|
+
* await tx.create('feature_flag_evaluations', {
|
|
2994
|
+
* flag_key: 'BULK_UPDATE',
|
|
2995
|
+
* result: { updatedFlags: flagsToUpdate },
|
|
2996
|
+
* reason: 'bulk_disable',
|
|
2997
|
+
* evaluated_at: new Date().toISOString()
|
|
2998
|
+
* });
|
|
2999
|
+
* });
|
|
3000
|
+
* ```
|
|
3001
|
+
*
|
|
3002
|
+
* @example Error Handling and Rollback
|
|
3003
|
+
* ```typescript
|
|
3004
|
+
* try {
|
|
3005
|
+
* await manager.transaction(async (tx) => {
|
|
3006
|
+
* await tx.create('feature_flags', { key: 'TEST_FLAG', value: true });
|
|
3007
|
+
*
|
|
3008
|
+
* // This will cause the entire transaction to rollback
|
|
3009
|
+
* throw new Error('Something went wrong');
|
|
3010
|
+
*
|
|
3011
|
+
* // This won't execute, and the flag creation above will be rolled back
|
|
3012
|
+
* await tx.create('feature_flag_rules', { flag_key: 'TEST_FLAG' });
|
|
3013
|
+
* });
|
|
3014
|
+
* } catch (error) {
|
|
3015
|
+
* console.log('Transaction failed, all changes rolled back');
|
|
3016
|
+
* }
|
|
3017
|
+
* ```
|
|
2800
3018
|
*/
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
return "[INVALID_URL]";
|
|
3019
|
+
async transaction(callback) {
|
|
3020
|
+
const db = this.getDatabase();
|
|
3021
|
+
const result = await db.transaction(callback);
|
|
3022
|
+
if (!result.success) {
|
|
3023
|
+
const errorMessage = result.error?.message ?? "Transaction failed";
|
|
3024
|
+
throw new errors.DatabaseError(errorMessage, types.DATABASE_ERROR_CODES.TRANSACTION_FAILED);
|
|
2808
3025
|
}
|
|
3026
|
+
if (result.value === void 0 || result.value === null) {
|
|
3027
|
+
throw new errors.DatabaseError("Transaction returned no value", types.DATABASE_ERROR_CODES.INVALID_RESULT);
|
|
3028
|
+
}
|
|
3029
|
+
return result.value;
|
|
2809
3030
|
}
|
|
2810
3031
|
/**
|
|
2811
|
-
*
|
|
3032
|
+
* Closes the database connection and cleans up resources
|
|
2812
3033
|
*
|
|
2813
|
-
* @
|
|
3034
|
+
* @description Properly closes the database connection and resets the singleton instance.
|
|
3035
|
+
* This is typically called during application shutdown or in tests that need to reset
|
|
3036
|
+
* the connection state.
|
|
3037
|
+
*
|
|
3038
|
+
* @example Application Shutdown
|
|
3039
|
+
* ```typescript
|
|
3040
|
+
* // In your NestJS module's onModuleDestroy
|
|
3041
|
+
* export class FeatureFlagModule implements OnModuleDestroy {
|
|
3042
|
+
* async onModuleDestroy() {
|
|
3043
|
+
* const manager = DatabaseConnectionManager.getInstance();
|
|
3044
|
+
* await manager.close();
|
|
3045
|
+
* console.log('Database connection closed');
|
|
3046
|
+
* }
|
|
3047
|
+
* }
|
|
3048
|
+
* ```
|
|
3049
|
+
*
|
|
3050
|
+
* @example Test Cleanup
|
|
3051
|
+
* ```typescript
|
|
3052
|
+
* describe('Feature Flag Tests', () => {
|
|
3053
|
+
* afterEach(async () => {
|
|
3054
|
+
* // Clean up connection between tests
|
|
3055
|
+
* const manager = DatabaseConnectionManager.getInstance();
|
|
3056
|
+
* await manager.close();
|
|
3057
|
+
* });
|
|
3058
|
+
* });
|
|
3059
|
+
* ```
|
|
2814
3060
|
*/
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
connectionString: this.config.databaseConfig?.connectionString ? this.maskConnectionString(this.config.databaseConfig.connectionString) : void 0,
|
|
2818
|
-
tableName: this.config.databaseConfig?.tableName ?? "feature_flags",
|
|
2819
|
-
isImplemented: false,
|
|
2820
|
-
requiredPackages: ["@plyaz/db"],
|
|
2821
|
-
recommendedORM: ["drizzle-orm", "prisma"],
|
|
2822
|
-
documentationPath: "/docs/feature-flag-to-implement/database-requirements.md",
|
|
2823
|
-
schemaPath: "/docs/feature-flag-to-implement/database-requirements.md#database-schema"
|
|
2824
|
-
};
|
|
3061
|
+
async close() {
|
|
3062
|
+
this.databaseService = null;
|
|
2825
3063
|
}
|
|
2826
3064
|
};
|
|
2827
|
-
var
|
|
2828
|
-
memory: MemoryFeatureFlagProvider,
|
|
2829
|
-
file: FileFeatureFlagProvider,
|
|
2830
|
-
redis: RedisFeatureFlagProvider,
|
|
2831
|
-
api: ApiFeatureFlagProvider,
|
|
2832
|
-
database: DatabaseFeatureFlagProvider
|
|
2833
|
-
};
|
|
2834
|
-
var FeatureFlagProviderFactory = class {
|
|
3065
|
+
var FeatureFlagDatabaseRepository = class {
|
|
2835
3066
|
static {
|
|
2836
|
-
__name(this, "
|
|
3067
|
+
__name(this, "FeatureFlagDatabaseRepository");
|
|
2837
3068
|
}
|
|
3069
|
+
connectionManager = DatabaseConnectionManager.getInstance();
|
|
2838
3070
|
/**
|
|
2839
|
-
*
|
|
3071
|
+
* Retrieves all feature flags from the database with optional environment filtering
|
|
3072
|
+
*
|
|
3073
|
+
* @description Fetches all feature flags from the database. This method is primarily called
|
|
3074
|
+
* during system initialization to load flags into cache, but can also be used for
|
|
3075
|
+
* administrative purposes or cache refresh operations.
|
|
3076
|
+
*
|
|
3077
|
+
* @param {string} [environment] - Optional environment filter (e.g., 'production', 'staging')
|
|
3078
|
+
* @returns {Promise<FeatureFlag<TKey>[]>} Array of feature flags matching the criteria
|
|
3079
|
+
*
|
|
3080
|
+
* @throws {Error} When database query fails critically
|
|
3081
|
+
*
|
|
3082
|
+
* @example
|
|
3083
|
+
* ```typescript
|
|
3084
|
+
* // Get all flags for system initialization
|
|
3085
|
+
* const repository = new FeatureFlagDatabaseRepository();
|
|
3086
|
+
* const allFlags = await repository.getAllFlags();
|
|
3087
|
+
* console.log(`Loaded ${allFlags.length} feature flags`);
|
|
3088
|
+
*
|
|
3089
|
+
* // Get only production flags
|
|
3090
|
+
* const prodFlags = await repository.getAllFlags('production');
|
|
3091
|
+
* console.log(`Production flags: ${prodFlags.length}`);
|
|
3092
|
+
*
|
|
3093
|
+
* // Handle empty results
|
|
3094
|
+
* const flags = await repository.getAllFlags('nonexistent');
|
|
3095
|
+
* if (flags.length === 0) {
|
|
3096
|
+
* console.log('No flags found for environment');
|
|
3097
|
+
* }
|
|
3098
|
+
* ```
|
|
2840
3099
|
*
|
|
2841
|
-
* @param config - Provider configuration
|
|
2842
|
-
* @param features - Record of feature flag keys to their default values
|
|
2843
|
-
* @returns Configured provider instance
|
|
2844
|
-
* @throws Error if provider type is unsupported or configuration is invalid
|
|
2845
3100
|
*/
|
|
2846
|
-
|
|
2847
|
-
this.
|
|
2848
|
-
const
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
}
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
);
|
|
3101
|
+
async getAllFlags(environment) {
|
|
3102
|
+
const db = this.connectionManager.getDatabase();
|
|
3103
|
+
const options = {
|
|
3104
|
+
sort: [
|
|
3105
|
+
{
|
|
3106
|
+
field: types.FEATURE_FLAG_FIELD.Key,
|
|
3107
|
+
direction: types.SORT_DIRECTION.Asc
|
|
3108
|
+
}
|
|
3109
|
+
],
|
|
3110
|
+
pagination: { page: 1, limit: 1e3 }
|
|
3111
|
+
};
|
|
3112
|
+
const result = await db.query(types.DATABASE_TABLE.FeatureFlags, options);
|
|
3113
|
+
if (!result.success) {
|
|
3114
|
+
console.warn("Query failed, returning empty array:", result.error?.message);
|
|
3115
|
+
return [];
|
|
3116
|
+
}
|
|
3117
|
+
let flags = result.value?.data ?? [];
|
|
3118
|
+
if (environment) {
|
|
3119
|
+
flags = flags.filter((flag) => {
|
|
3120
|
+
return Boolean(flag.environments?.includes(environment));
|
|
3121
|
+
});
|
|
2860
3122
|
}
|
|
3123
|
+
return flags.map((flag) => this.mapToFeatureFlag(flag));
|
|
2861
3124
|
}
|
|
2862
3125
|
/**
|
|
2863
|
-
*
|
|
3126
|
+
* Retrieves a specific feature flag by its key
|
|
3127
|
+
*
|
|
3128
|
+
* @description Fetches a single feature flag from the database using its unique key.
|
|
3129
|
+
* This method is optimized for runtime flag evaluation and uses the primary key index
|
|
3130
|
+
* for O(1) lookup performance.
|
|
3131
|
+
*
|
|
3132
|
+
* @param {TKey} key - The unique identifier of the feature flag to retrieve
|
|
3133
|
+
* @returns {Promise<FeatureFlag<TKey> | null>} The feature flag object or null if not found
|
|
3134
|
+
*
|
|
3135
|
+
* @throws {Error} When database operation fails
|
|
3136
|
+
*
|
|
3137
|
+
* @example
|
|
3138
|
+
* ```typescript
|
|
3139
|
+
* const repository = new FeatureFlagDatabaseRepository();
|
|
3140
|
+
*
|
|
3141
|
+
* // Get a specific flag for evaluation
|
|
3142
|
+
* const premiumFlag = await repository.getFlag('PREMIUM_FEATURE');
|
|
3143
|
+
* if (premiumFlag) {
|
|
3144
|
+
* console.log(`Flag enabled: ${premiumFlag.isEnabled}`);
|
|
3145
|
+
* console.log(`Flag value:`, premiumFlag.value);
|
|
3146
|
+
* } else {
|
|
3147
|
+
* console.log('Flag not found');
|
|
3148
|
+
* }
|
|
3149
|
+
*
|
|
3150
|
+
* // Handle flag evaluation
|
|
3151
|
+
* const checkoutFlag = await repository.getFlag('NEW_CHECKOUT');
|
|
3152
|
+
* const isNewCheckoutEnabled = checkoutFlag?.isEnabled ?? false;
|
|
3153
|
+
* ```
|
|
2864
3154
|
*
|
|
2865
|
-
* @param config - Provider configuration
|
|
2866
|
-
* @param features - Record of feature flag keys to their default values
|
|
2867
|
-
* @returns Promise resolving to initialized provider instance
|
|
2868
3155
|
*/
|
|
2869
|
-
|
|
2870
|
-
const
|
|
2871
|
-
await
|
|
2872
|
-
|
|
3156
|
+
async getFlag(key) {
|
|
3157
|
+
const db = this.connectionManager.getDatabase();
|
|
3158
|
+
const result = await db.get(types.DATABASE_TABLE.FeatureFlags, key);
|
|
3159
|
+
if (!result.success) {
|
|
3160
|
+
throw new errors.DatabaseError(
|
|
3161
|
+
`Failed to get flag: ${result.error?.message}`,
|
|
3162
|
+
types.DATABASE_ERROR_CODES.NO_DATA
|
|
3163
|
+
);
|
|
3164
|
+
}
|
|
3165
|
+
return result.value ? this.mapToFeatureFlag(result.value) : null;
|
|
2873
3166
|
}
|
|
2874
3167
|
/**
|
|
2875
|
-
*
|
|
3168
|
+
* Creates a new feature flag in the database
|
|
3169
|
+
*
|
|
3170
|
+
* @description Creates a new feature flag record with the provided configuration.
|
|
3171
|
+
* This method is typically called through administrative interfaces or during
|
|
3172
|
+
* system setup and migration processes.
|
|
3173
|
+
*
|
|
3174
|
+
* @param {CreateFlagRequest<TKey>} data - The feature flag creation data
|
|
3175
|
+
* @returns {Promise<FeatureFlag<TKey>>} The created feature flag object
|
|
3176
|
+
*
|
|
3177
|
+
* @throws {Error} When flag key already exists (primary key constraint violation)
|
|
3178
|
+
* @throws {Error} When database operation fails
|
|
3179
|
+
*
|
|
3180
|
+
* @example
|
|
3181
|
+
* ```typescript
|
|
3182
|
+
* const repository = new FeatureFlagDatabaseRepository();
|
|
3183
|
+
*
|
|
3184
|
+
* // Create a simple boolean flag
|
|
3185
|
+
* const simpleFlag = await repository.createFlag({
|
|
3186
|
+
* key: 'ENABLE_DARK_MODE',
|
|
3187
|
+
* value: true,
|
|
3188
|
+
* isEnabled: true,
|
|
3189
|
+
* environment: 'production',
|
|
3190
|
+
* description: 'Enable dark mode theme'
|
|
3191
|
+
* });
|
|
3192
|
+
*
|
|
3193
|
+
* // Create a complex flag with object value
|
|
3194
|
+
* const complexFlag = await repository.createFlag({
|
|
3195
|
+
* key: 'CHECKOUT_CONFIG',
|
|
3196
|
+
* value: {
|
|
3197
|
+
* variant: 'blue',
|
|
3198
|
+
* showPromo: true,
|
|
3199
|
+
* maxItems: 10
|
|
3200
|
+
* },
|
|
3201
|
+
* isEnabled: false,
|
|
3202
|
+
* environment: 'staging',
|
|
3203
|
+
* description: 'Checkout page configuration'
|
|
3204
|
+
* });
|
|
3205
|
+
*
|
|
3206
|
+
* console.log(`Created flag: ${simpleFlag.key}`);
|
|
3207
|
+
* ```
|
|
2876
3208
|
*
|
|
2877
|
-
* @returns Array of supported provider names
|
|
2878
3209
|
*/
|
|
2879
|
-
|
|
2880
|
-
|
|
3210
|
+
async createFlag(data) {
|
|
3211
|
+
const db = this.connectionManager.getDatabase();
|
|
3212
|
+
const flagData = {
|
|
3213
|
+
[types.FEATURE_FLAG_FIELD.Key]: data.key,
|
|
3214
|
+
[types.FEATURE_FLAG_FIELD.Value]: data.value,
|
|
3215
|
+
[types.FEATURE_FLAG_FIELD.IsEnabled]: data.isEnabled ?? true,
|
|
3216
|
+
[types.FEATURE_FLAG_FIELD.Environments]: data.environment ? [data.environment] : void 0,
|
|
3217
|
+
[types.FEATURE_FLAG_FIELD.Description]: data.description,
|
|
3218
|
+
[types.FEATURE_FLAG_FIELD.UpdatedAt]: (/* @__PURE__ */ new Date()).toISOString()
|
|
3219
|
+
};
|
|
3220
|
+
const result = await db.create(types.DATABASE_TABLE.FeatureFlags, flagData);
|
|
3221
|
+
if (!result.success || !result.value) {
|
|
3222
|
+
throw new errors.DatabaseError(
|
|
3223
|
+
`Failed to create flag: ${result.error?.message}`,
|
|
3224
|
+
types.DATABASE_ERROR_CODES.CREATE_FAILED
|
|
3225
|
+
);
|
|
3226
|
+
}
|
|
3227
|
+
return this.mapToFeatureFlag(result.value);
|
|
2881
3228
|
}
|
|
2882
3229
|
/**
|
|
2883
|
-
*
|
|
3230
|
+
* Updates an existing feature flag in the database
|
|
3231
|
+
*
|
|
3232
|
+
* @description Updates specific fields of an existing feature flag. Only provided
|
|
3233
|
+
* fields will be updated, leaving other fields unchanged. The updated_at timestamp
|
|
3234
|
+
* is automatically set to the current time.
|
|
3235
|
+
*
|
|
3236
|
+
* @param {TKey} key - The unique identifier of the feature flag to update
|
|
3237
|
+
* @param {Partial<CreateFlagRequest<TKey>>} data - Partial flag data to update
|
|
3238
|
+
* @returns {Promise<FeatureFlag<TKey>>} The updated feature flag object
|
|
3239
|
+
*
|
|
3240
|
+
* @throws {Error} When flag with the specified key is not found
|
|
3241
|
+
* @throws {Error} When database operation fails
|
|
3242
|
+
*
|
|
3243
|
+
* @example
|
|
3244
|
+
* ```typescript
|
|
3245
|
+
* const repository = new FeatureFlagDatabaseRepository();
|
|
3246
|
+
*
|
|
3247
|
+
* // Enable a flag
|
|
3248
|
+
* const enabledFlag = await repository.updateFlag('BETA_FEATURE', {
|
|
3249
|
+
* isEnabled: true
|
|
3250
|
+
* });
|
|
3251
|
+
*
|
|
3252
|
+
* // Update flag value and description
|
|
3253
|
+
* const updatedFlag = await repository.updateFlag('CHECKOUT_CONFIG', {
|
|
3254
|
+
* value: { variant: 'green', showPromo: false },
|
|
3255
|
+
* description: 'Updated checkout configuration'
|
|
3256
|
+
* });
|
|
3257
|
+
*
|
|
3258
|
+
* // Change environment
|
|
3259
|
+
* const prodFlag = await repository.updateFlag('NEW_FEATURE', {
|
|
3260
|
+
* environment: 'production'
|
|
3261
|
+
* });
|
|
3262
|
+
*
|
|
3263
|
+
* console.log(`Updated flag: ${updatedFlag.key}`);
|
|
3264
|
+
* ```
|
|
2884
3265
|
*
|
|
2885
|
-
* @param providerType - Provider type to check
|
|
2886
|
-
* @returns True if provider type is supported
|
|
2887
3266
|
*/
|
|
2888
|
-
|
|
2889
|
-
|
|
3267
|
+
async updateFlag(key, data) {
|
|
3268
|
+
const db = this.connectionManager.getDatabase();
|
|
3269
|
+
const updateData = {
|
|
3270
|
+
...data,
|
|
3271
|
+
[types.FEATURE_FLAG_FIELD.UpdatedAt]: (/* @__PURE__ */ new Date()).toISOString()
|
|
3272
|
+
};
|
|
3273
|
+
const result = await db.update(types.DATABASE_TABLE.FeatureFlags, key, updateData);
|
|
3274
|
+
if (!result.success || !result.value) {
|
|
3275
|
+
throw new errors.DatabaseError(
|
|
3276
|
+
`Failed to update flag: ${result.error?.message}`,
|
|
3277
|
+
types.DATABASE_ERROR_CODES.UPDATE_FAILED
|
|
3278
|
+
);
|
|
3279
|
+
}
|
|
3280
|
+
return this.mapToFeatureFlag(result.value);
|
|
2890
3281
|
}
|
|
2891
3282
|
/**
|
|
2892
|
-
*
|
|
3283
|
+
* Deletes a feature flag from the database
|
|
2893
3284
|
*
|
|
2894
|
-
* @
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
3285
|
+
* @description Permanently removes a feature flag from the database. This operation
|
|
3286
|
+
* cannot be undone. All associated rules, evaluations, and overrides should be
|
|
3287
|
+
* cleaned up separately if needed.
|
|
3288
|
+
*
|
|
3289
|
+
* @param {TKey} key - The unique identifier of the feature flag to delete
|
|
3290
|
+
* @returns {Promise<void>} Promise that resolves when deletion is complete
|
|
3291
|
+
*
|
|
3292
|
+
* @throws {Error} When flag with the specified key is not found
|
|
3293
|
+
* @throws {Error} When database operation fails
|
|
3294
|
+
*
|
|
3295
|
+
* @example
|
|
3296
|
+
* ```typescript
|
|
3297
|
+
* const repository = new FeatureFlagDatabaseRepository();
|
|
3298
|
+
*
|
|
3299
|
+
* // Delete a flag
|
|
3300
|
+
* try {
|
|
3301
|
+
* await repository.deleteFlag('OLD_FEATURE');
|
|
3302
|
+
* console.log('Flag deleted successfully');
|
|
3303
|
+
* } catch (error) {
|
|
3304
|
+
* console.error('Failed to delete flag:', error.message);
|
|
3305
|
+
* }
|
|
3306
|
+
*
|
|
3307
|
+
* // Verify deletion
|
|
3308
|
+
* const deletedFlag = await repository.getFlag('OLD_FEATURE');
|
|
3309
|
+
* console.log(deletedFlag === null); // true
|
|
3310
|
+
* ```
|
|
3311
|
+
*
|
|
3312
|
+
*/
|
|
3313
|
+
async deleteFlag(key) {
|
|
3314
|
+
const db = this.connectionManager.getDatabase();
|
|
3315
|
+
const result = await db.delete(types.DATABASE_TABLE.FeatureFlags, key);
|
|
3316
|
+
if (!result.success) {
|
|
3317
|
+
throw new errors.DatabaseError(
|
|
3318
|
+
`Failed to delete flag: ${result.error?.message}`,
|
|
3319
|
+
types.DATABASE_ERROR_CODES.DELETE_FAILED
|
|
3320
|
+
);
|
|
3321
|
+
}
|
|
3322
|
+
}
|
|
3323
|
+
/**
|
|
3324
|
+
* Retrieves all rules associated with a specific feature flag
|
|
3325
|
+
*
|
|
3326
|
+
* @description Fetches all rules configured for a specific feature flag, ordered by
|
|
3327
|
+
* priority in descending order. Rules are used for advanced flag evaluation logic
|
|
3328
|
+
* based on user context and conditions.
|
|
3329
|
+
*
|
|
3330
|
+
* @param {TKey} key - The feature flag key to get rules for
|
|
3331
|
+
* @returns {Promise<FeatureFlagRule<TKey>[]>} Array of rules for the specified flag
|
|
3332
|
+
*
|
|
3333
|
+
* @example
|
|
3334
|
+
* ```typescript
|
|
3335
|
+
* const repository = new FeatureFlagDatabaseRepository();
|
|
3336
|
+
*
|
|
3337
|
+
* // Get rules for a flag
|
|
3338
|
+
* const rules = await repository.getFlagRules('PREMIUM_FEATURE');
|
|
3339
|
+
* console.log(`Found ${rules.length} rules`);
|
|
3340
|
+
*
|
|
3341
|
+
* // Process rules by priority
|
|
3342
|
+
* rules.forEach(rule => {
|
|
3343
|
+
* console.log(`Rule: ${rule.name}, Priority: ${rule.priority}`);
|
|
3344
|
+
* });
|
|
3345
|
+
* ```
|
|
3346
|
+
*
|
|
3347
|
+
*/
|
|
3348
|
+
async getFlagRules(key) {
|
|
3349
|
+
const db = this.connectionManager.getDatabase();
|
|
3350
|
+
const options = {
|
|
3351
|
+
filter: {
|
|
3352
|
+
field: types.FEATURE_FLAG_RULE_FIELD.FlagKey,
|
|
3353
|
+
operator: "eq",
|
|
3354
|
+
value: key
|
|
3355
|
+
},
|
|
3356
|
+
sort: [
|
|
3357
|
+
{
|
|
3358
|
+
field: types.FEATURE_FLAG_RULE_FIELD.Priority,
|
|
3359
|
+
direction: types.SORT_DIRECTION.Desc
|
|
3360
|
+
}
|
|
3361
|
+
]
|
|
3362
|
+
};
|
|
3363
|
+
const result = await db.list(
|
|
3364
|
+
types.DATABASE_TABLE.FeatureFlagRules,
|
|
3365
|
+
options
|
|
3366
|
+
);
|
|
3367
|
+
if (!result.success) {
|
|
3368
|
+
console.warn("getFlagRules failed, returning empty array:", result.error?.message);
|
|
3369
|
+
return [];
|
|
3370
|
+
}
|
|
3371
|
+
const rules = result.value?.data ?? [];
|
|
3372
|
+
return rules.map((rule) => this.mapToFeatureFlagRule(rule));
|
|
3373
|
+
}
|
|
3374
|
+
/**
|
|
3375
|
+
* Retrieves all enabled feature flag rules from the database
|
|
3376
|
+
*
|
|
3377
|
+
* @description Fetches all enabled rules across all feature flags, sorted by flag key
|
|
3378
|
+
* and then by priority. This method is used during system initialization to load
|
|
3379
|
+
* all active rules into the evaluation engine.
|
|
3380
|
+
*
|
|
3381
|
+
* @returns {Promise<FeatureFlagRule<TKey>[]>} Array of all enabled rules
|
|
3382
|
+
*
|
|
3383
|
+
* @example
|
|
3384
|
+
* ```typescript
|
|
3385
|
+
* const repository = new FeatureFlagDatabaseRepository();
|
|
3386
|
+
*
|
|
3387
|
+
* // Load all rules for evaluation engine
|
|
3388
|
+
* const allRules = await repository.getAllRules();
|
|
3389
|
+
* console.log(`Loaded ${allRules.length} active rules`);
|
|
3390
|
+
*
|
|
3391
|
+
* // Group rules by flag
|
|
3392
|
+
* const rulesByFlag = allRules.reduce((acc, rule) => {
|
|
3393
|
+
* if (!acc[rule.flagKey]) acc[rule.flagKey] = [];
|
|
3394
|
+
* acc[rule.flagKey].push(rule);
|
|
3395
|
+
* return acc;
|
|
3396
|
+
* }, {});
|
|
3397
|
+
* ```
|
|
3398
|
+
*
|
|
3399
|
+
*/
|
|
3400
|
+
async getAllRules() {
|
|
3401
|
+
const db = this.connectionManager.getDatabase();
|
|
3402
|
+
const options = {
|
|
3403
|
+
filter: {
|
|
3404
|
+
field: types.FEATURE_FLAG_RULE_FIELD.IsEnabled,
|
|
3405
|
+
operator: "eq",
|
|
3406
|
+
value: true
|
|
3407
|
+
},
|
|
3408
|
+
sort: [
|
|
3409
|
+
{ field: types.FEATURE_FLAG_RULE_FIELD.FlagKey, direction: types.SORT_DIRECTION.Asc },
|
|
3410
|
+
{ field: types.FEATURE_FLAG_RULE_FIELD.Priority, direction: types.SORT_DIRECTION.Desc }
|
|
3411
|
+
]
|
|
3412
|
+
};
|
|
3413
|
+
const result = await db.list(
|
|
3414
|
+
types.DATABASE_TABLE.FeatureFlagRules,
|
|
3415
|
+
options
|
|
3416
|
+
);
|
|
3417
|
+
if (!result.success) {
|
|
3418
|
+
console.warn("getAllRules failed, returning empty array:", result.error?.message);
|
|
3419
|
+
return [];
|
|
3420
|
+
}
|
|
3421
|
+
const rules = result.value?.data ?? [];
|
|
3422
|
+
return rules.map((rule) => this.mapToFeatureFlagRule(rule));
|
|
3423
|
+
}
|
|
3424
|
+
/**
|
|
3425
|
+
* Logs a feature flag evaluation for audit and analytics purposes
|
|
3426
|
+
*
|
|
3427
|
+
* @description Records feature flag evaluation events for compliance tracking,
|
|
3428
|
+
* debugging, and analytics. This method is called after each flag evaluation
|
|
3429
|
+
* when audit logging is enabled.
|
|
3430
|
+
*
|
|
3431
|
+
* @param {Object} params - The evaluation parameters
|
|
3432
|
+
* @param {TKey} params.flagKey - The feature flag key that was evaluated
|
|
3433
|
+
* @param {string} [params.userId] - Optional user ID who triggered the evaluation
|
|
3434
|
+
* @param {FeatureFlagContext} [params.context] - Optional evaluation context
|
|
3435
|
+
* @param {FeatureFlagValue} params.result - The evaluation result
|
|
3436
|
+
* @returns {Promise<void>} Promise that resolves when log entry is created
|
|
3437
|
+
*
|
|
3438
|
+
* @example
|
|
3439
|
+
* ```typescript
|
|
3440
|
+
* const repository = new FeatureFlagDatabaseRepository();
|
|
3441
|
+
*
|
|
3442
|
+
* // Log a user evaluation
|
|
3443
|
+
* await repository.logEvaluation({
|
|
3444
|
+
* flagKey: 'PREMIUM_FEATURE',
|
|
3445
|
+
* userId: 'user123',
|
|
3446
|
+
* context: {
|
|
3447
|
+
* userRole: 'premium',
|
|
3448
|
+
* environment: 'production',
|
|
3449
|
+
* requestId: 'req-456'
|
|
3450
|
+
* },
|
|
3451
|
+
* result: true
|
|
3452
|
+
* });
|
|
3453
|
+
*
|
|
3454
|
+
* // Log anonymous evaluation
|
|
3455
|
+
* await repository.logEvaluation({
|
|
3456
|
+
* flagKey: 'PUBLIC_FEATURE',
|
|
3457
|
+
* result: { variant: 'blue', enabled: true }
|
|
3458
|
+
* });
|
|
3459
|
+
* ```
|
|
3460
|
+
*
|
|
3461
|
+
*/
|
|
3462
|
+
async logEvaluation(params) {
|
|
3463
|
+
try {
|
|
3464
|
+
const db = this.connectionManager.getDatabase();
|
|
3465
|
+
const evaluationData = {
|
|
3466
|
+
id: crypto.randomUUID(),
|
|
3467
|
+
flag_key: params.flagKey,
|
|
3468
|
+
user_id: params.userId,
|
|
3469
|
+
context: params.context ? params.context : void 0,
|
|
3470
|
+
result: params.result,
|
|
3471
|
+
reason: types.EVALUATION_REASONS.EVALUATION,
|
|
3472
|
+
evaluated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
3473
|
+
};
|
|
3474
|
+
const result = await db.create(types.DATABASE_TABLE.FeatureFlagEvaluations, evaluationData);
|
|
3475
|
+
if (!result.success) {
|
|
3476
|
+
console.warn("logEvaluation failed:", result.error?.message);
|
|
3477
|
+
}
|
|
3478
|
+
} catch (error) {
|
|
3479
|
+
console.warn("logEvaluation error:", error);
|
|
3480
|
+
}
|
|
3481
|
+
}
|
|
3482
|
+
/**
|
|
3483
|
+
* Sets a user-specific override for a feature flag
|
|
3484
|
+
*
|
|
3485
|
+
* @description Creates a user-specific override that takes precedence over the
|
|
3486
|
+
* default flag value and rules. Overrides can have optional expiration times
|
|
3487
|
+
* and are useful for testing, gradual rollouts, or user-specific configurations.
|
|
3488
|
+
*
|
|
3489
|
+
* @param {TKey} flagKey - The feature flag key to override
|
|
3490
|
+
* @param {string} userId - The user ID for whom to set the override
|
|
3491
|
+
* @param {FeatureFlagValue} value - The override value
|
|
3492
|
+
* @param {Date} [expiresAt] - Optional expiration date for the override
|
|
3493
|
+
* @returns {Promise<void>} Promise that resolves when override is created
|
|
3494
|
+
*
|
|
3495
|
+
* @example
|
|
3496
|
+
* ```typescript
|
|
3497
|
+
* const repository = new FeatureFlagDatabaseRepository();
|
|
3498
|
+
*
|
|
3499
|
+
* // Set permanent override
|
|
3500
|
+
* await repository.setOverride(
|
|
3501
|
+
* 'BETA_FEATURE',
|
|
3502
|
+
* 'user123',
|
|
3503
|
+
* true
|
|
3504
|
+
* );
|
|
3505
|
+
*
|
|
3506
|
+
* // Set temporary override (expires in 1 hour)
|
|
3507
|
+
* const expiresAt = new Date(Date.now() + 60 * 60 * 1000);
|
|
3508
|
+
* await repository.setOverride(
|
|
3509
|
+
* 'PREMIUM_FEATURE',
|
|
3510
|
+
* 'testuser456',
|
|
3511
|
+
* { enabled: true, variant: 'premium' },
|
|
3512
|
+
* expiresAt
|
|
3513
|
+
* );
|
|
3514
|
+
* ```
|
|
3515
|
+
*
|
|
3516
|
+
*/
|
|
3517
|
+
async setOverride(flagKey, userId, value, expiresAt) {
|
|
3518
|
+
const db = this.connectionManager.getDatabase();
|
|
3519
|
+
const overrideData = {
|
|
3520
|
+
id: crypto.randomUUID(),
|
|
3521
|
+
flag_key: flagKey,
|
|
3522
|
+
user_id: userId,
|
|
3523
|
+
value,
|
|
3524
|
+
expires_at: expiresAt?.toISOString(),
|
|
3525
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
3526
|
+
};
|
|
3527
|
+
const result = await db.create(types.DATABASE_TABLE.FeatureFlagOverrides, overrideData);
|
|
3528
|
+
if (!result.success) {
|
|
3529
|
+
console.warn("setOverride failed:", result.error?.message);
|
|
3530
|
+
}
|
|
3531
|
+
}
|
|
3532
|
+
/**
|
|
3533
|
+
* Retrieves a user-specific override for a feature flag
|
|
3534
|
+
*
|
|
3535
|
+
* @description Fetches the override value for a specific user and flag combination.
|
|
3536
|
+
* Automatically filters out expired overrides. Returns null if no valid override
|
|
3537
|
+
* exists for the user.
|
|
3538
|
+
*
|
|
3539
|
+
* @param {TKey} flagKey - The feature flag key to check for overrides
|
|
3540
|
+
* @param {string} userId - The user ID to get the override for
|
|
3541
|
+
* @returns {Promise<FeatureFlagValue | null>} The override value or null if none exists
|
|
3542
|
+
*
|
|
3543
|
+
* @example
|
|
3544
|
+
* ```typescript
|
|
3545
|
+
* const repository = new FeatureFlagDatabaseRepository();
|
|
3546
|
+
*
|
|
3547
|
+
* // Check for user override
|
|
3548
|
+
* const override = await repository.getOverride('BETA_FEATURE', 'user123');
|
|
3549
|
+
* if (override !== null) {
|
|
3550
|
+
* console.log('User has override:', override);
|
|
3551
|
+
* } else {
|
|
3552
|
+
* console.log('No override found, using default flag value');
|
|
3553
|
+
* }
|
|
3554
|
+
*
|
|
3555
|
+
* // Use in flag evaluation
|
|
3556
|
+
* const userOverride = await repository.getOverride('PREMIUM_FEATURE', userId);
|
|
3557
|
+
* const flagValue = userOverride ?? defaultFlagValue;
|
|
3558
|
+
* ```
|
|
3559
|
+
*
|
|
3560
|
+
*/
|
|
3561
|
+
async getOverride(flagKey, userId) {
|
|
3562
|
+
const db = this.connectionManager.getDatabase();
|
|
3563
|
+
const options = {
|
|
3564
|
+
filter: {
|
|
3565
|
+
field: types.DATABASE_FIELDS.FlagKey,
|
|
3566
|
+
operator: "eq",
|
|
3567
|
+
value: flagKey
|
|
3568
|
+
}
|
|
3569
|
+
};
|
|
3570
|
+
const result = await db.list(
|
|
3571
|
+
types.DATABASE_TABLE.FeatureFlagOverrides,
|
|
3572
|
+
options
|
|
3573
|
+
);
|
|
3574
|
+
if (!result.success || !result.value?.data) {
|
|
3575
|
+
return null;
|
|
3576
|
+
}
|
|
3577
|
+
const typedData = result.value.data;
|
|
3578
|
+
const validOverrides = typedData.filter((override) => {
|
|
3579
|
+
if (override.user_id !== userId) return false;
|
|
3580
|
+
if (override.expires_at && new Date(override.expires_at) < /* @__PURE__ */ new Date()) return false;
|
|
3581
|
+
return true;
|
|
3582
|
+
});
|
|
3583
|
+
return validOverrides.length > 0 ? validOverrides[0].value : null;
|
|
3584
|
+
}
|
|
3585
|
+
/**
|
|
3586
|
+
* Removes user-specific overrides for a feature flag
|
|
3587
|
+
*
|
|
3588
|
+
* @description Deletes all override records for a specific user and flag combination.
|
|
3589
|
+
* This operation cannot be undone and will cause the user to receive the default
|
|
3590
|
+
* flag value or rule-based evaluation on subsequent requests.
|
|
3591
|
+
*
|
|
3592
|
+
* @param {TKey} flagKey - The feature flag key to remove overrides for
|
|
3593
|
+
* @param {string} userId - The user ID to remove overrides for
|
|
3594
|
+
* @returns {Promise<void>} Promise that resolves when overrides are removed
|
|
3595
|
+
*
|
|
3596
|
+
* @example
|
|
3597
|
+
* ```typescript
|
|
3598
|
+
* const repository = new FeatureFlagDatabaseRepository();
|
|
3599
|
+
*
|
|
3600
|
+
* // Remove user override
|
|
3601
|
+
* await repository.removeOverride('BETA_FEATURE', 'user123');
|
|
3602
|
+
* console.log('Override removed, user will get default flag value');
|
|
3603
|
+
*
|
|
3604
|
+
* // Verify removal
|
|
3605
|
+
* const override = await repository.getOverride('BETA_FEATURE', 'user123');
|
|
3606
|
+
* console.log(override === null); // true
|
|
3607
|
+
*
|
|
3608
|
+
* // Bulk cleanup - remove overrides for multiple users
|
|
3609
|
+
* const userIds = ['user1', 'user2', 'user3'];
|
|
3610
|
+
* for (const userId of userIds) {
|
|
3611
|
+
* await repository.removeOverride('OLD_FEATURE', userId);
|
|
3612
|
+
* }
|
|
3613
|
+
* ```
|
|
3614
|
+
*
|
|
3615
|
+
*/
|
|
3616
|
+
async removeOverride(flagKey, userId) {
|
|
3617
|
+
const db = this.connectionManager.getDatabase();
|
|
3618
|
+
const options = {
|
|
3619
|
+
filter: {
|
|
3620
|
+
field: types.DATABASE_FIELDS.FlagKey,
|
|
3621
|
+
operator: "eq",
|
|
3622
|
+
value: flagKey
|
|
3623
|
+
}
|
|
3624
|
+
};
|
|
3625
|
+
const listResult = await db.list(
|
|
3626
|
+
types.DATABASE_TABLE.FeatureFlagOverrides,
|
|
3627
|
+
options
|
|
3628
|
+
);
|
|
3629
|
+
if (!listResult.success || !listResult.value?.data) {
|
|
3630
|
+
return;
|
|
3631
|
+
}
|
|
3632
|
+
const typedOverrides = listResult.value.data;
|
|
3633
|
+
const overridesToDelete = typedOverrides.filter((override) => override.user_id === userId);
|
|
3634
|
+
for (const override of overridesToDelete) {
|
|
3635
|
+
await db.delete(types.DATABASE_TABLE.FeatureFlagOverrides, override.id);
|
|
3636
|
+
}
|
|
3637
|
+
}
|
|
3638
|
+
/**
|
|
3639
|
+
* Maps a database row to a FeatureFlag object
|
|
3640
|
+
*
|
|
3641
|
+
* @description Converts raw database row data to a properly typed FeatureFlag object.
|
|
3642
|
+
* Handles field name variations between database schema and application types,
|
|
3643
|
+
* provides default values for missing fields, and ensures type safety.
|
|
3644
|
+
*
|
|
3645
|
+
* @private
|
|
3646
|
+
* @param {DatabaseFeatureFlagRow} row - Raw database row data
|
|
3647
|
+
* @returns {FeatureFlag<TKey>} Typed FeatureFlag object
|
|
3648
|
+
*
|
|
3649
|
+
* @example
|
|
3650
|
+
* ```typescript
|
|
3651
|
+
* // Database row input:
|
|
3652
|
+
* const dbRow = {
|
|
3653
|
+
* key: 'PREMIUM_FEATURE',
|
|
3654
|
+
* value: { enabled: true, variant: 'blue' },
|
|
3655
|
+
* is_enabled: true,
|
|
3656
|
+
* environments: ['production'],
|
|
3657
|
+
* description: 'Premium feature toggle',
|
|
3658
|
+
* created_at: '2024-01-15T10:30:00Z'
|
|
3659
|
+
* };
|
|
3660
|
+
*
|
|
3661
|
+
* // Mapped output:
|
|
3662
|
+
* const flag = this.mapToFeatureFlag(dbRow);
|
|
3663
|
+
* // {
|
|
3664
|
+
* // key: 'PREMIUM_FEATURE',
|
|
3665
|
+
* // type: 'boolean',
|
|
3666
|
+
* // name: 'PREMIUM_FEATURE',
|
|
3667
|
+
* // value: { enabled: true, variant: 'blue' },
|
|
3668
|
+
* // isEnabled: true,
|
|
3669
|
+
* // environment: 'production',
|
|
3670
|
+
* // description: 'Premium feature toggle',
|
|
3671
|
+
* // createdAt: Date('2024-01-15T10:30:00Z'),
|
|
3672
|
+
* // updatedAt: Date('2024-01-15T10:30:00Z'),
|
|
3673
|
+
* // createdBy: 'system',
|
|
3674
|
+
* // updatedBy: 'system'
|
|
3675
|
+
* // }
|
|
3676
|
+
* ```
|
|
3677
|
+
*
|
|
3678
|
+
*/
|
|
3679
|
+
mapToFeatureFlag(row) {
|
|
3680
|
+
return {
|
|
3681
|
+
key: row.key,
|
|
3682
|
+
type: types.FEATURE_FLAG_TYPES.BOOLEAN,
|
|
3683
|
+
name: row.key,
|
|
3684
|
+
value: row.value,
|
|
3685
|
+
isEnabled: this.getIsEnabled(row),
|
|
3686
|
+
environment: row.environments?.[0] ?? "",
|
|
3687
|
+
description: row.description ?? "",
|
|
3688
|
+
createdAt: this.getCreatedAt(row),
|
|
3689
|
+
updatedAt: this.getUpdatedAt(row),
|
|
3690
|
+
createdBy: types.SYSTEM_USERS.SYSTEM,
|
|
3691
|
+
updatedBy: types.SYSTEM_USERS.SYSTEM
|
|
3692
|
+
};
|
|
3693
|
+
}
|
|
3694
|
+
/**
|
|
3695
|
+
* Extracts the enabled status from a database row
|
|
3696
|
+
*
|
|
3697
|
+
* @private
|
|
3698
|
+
* @param {DatabaseFeatureFlagRow} row - Database row with enabled field variations
|
|
3699
|
+
* @returns {boolean} The enabled status, defaulting to true if not specified
|
|
3700
|
+
*/
|
|
3701
|
+
getIsEnabled(row) {
|
|
3702
|
+
return row.isEnabled ?? row.is_enabled ?? true;
|
|
3703
|
+
}
|
|
3704
|
+
/**
|
|
3705
|
+
* Extracts the creation date from a database row
|
|
3706
|
+
*
|
|
3707
|
+
* @private
|
|
3708
|
+
* @param {DatabaseFeatureFlagRow} row - Database row with creation date field variations
|
|
3709
|
+
* @returns {Date} The creation date, defaulting to current date if not specified
|
|
3710
|
+
*/
|
|
3711
|
+
getCreatedAt(row) {
|
|
3712
|
+
return row.createdAt ?? (row.created_at ? new Date(row.created_at) : /* @__PURE__ */ new Date());
|
|
3713
|
+
}
|
|
3714
|
+
/**
|
|
3715
|
+
* Extracts the update date from a database row
|
|
3716
|
+
*
|
|
3717
|
+
* @private
|
|
3718
|
+
* @param {DatabaseFeatureFlagRow} row - Database row with update date field variations
|
|
3719
|
+
* @returns {Date} The update date, defaulting to current date if not specified
|
|
3720
|
+
*/
|
|
3721
|
+
getUpdatedAt(row) {
|
|
3722
|
+
return row.updatedAt ?? (row.updated_at ? new Date(row.updated_at) : /* @__PURE__ */ new Date());
|
|
3723
|
+
}
|
|
3724
|
+
/**
|
|
3725
|
+
* Maps a database row to a FeatureFlagRule object
|
|
3726
|
+
*
|
|
3727
|
+
* @description Converts raw database rule row data to a properly typed FeatureFlagRule object.
|
|
3728
|
+
* Handles field name variations and ensures type safety for rule evaluation.
|
|
3729
|
+
*
|
|
3730
|
+
* @private
|
|
3731
|
+
* @param {DatabaseFeatureFlagRuleRow} row - Raw database rule row data
|
|
3732
|
+
* @returns {FeatureFlagRule<TKey>} Typed FeatureFlagRule object
|
|
3733
|
+
*
|
|
3734
|
+
*/
|
|
3735
|
+
mapToFeatureFlagRule(row) {
|
|
3736
|
+
return {
|
|
3737
|
+
id: row.id,
|
|
3738
|
+
flagKey: row.flagKey || row.flag_key,
|
|
3739
|
+
name: row.name,
|
|
3740
|
+
conditions: row.conditions,
|
|
3741
|
+
value: row.value,
|
|
3742
|
+
priority: row.priority,
|
|
3743
|
+
isEnabled: row.isEnabled ?? row.is_enabled ?? true
|
|
3744
|
+
};
|
|
3745
|
+
}
|
|
3746
|
+
};
|
|
3747
|
+
|
|
3748
|
+
// src/domain/featureFlags/providers/database.ts
|
|
3749
|
+
var DatabaseFeatureFlagProvider = class extends FeatureFlagProvider {
|
|
3750
|
+
static {
|
|
3751
|
+
__name(this, "DatabaseFeatureFlagProvider");
|
|
3752
|
+
}
|
|
3753
|
+
connectionManager = DatabaseConnectionManager.getInstance();
|
|
3754
|
+
repository = new FeatureFlagDatabaseRepository();
|
|
3755
|
+
/**
|
|
3756
|
+
* Creates a new database feature flag provider.
|
|
3757
|
+
*
|
|
3758
|
+
* @param config - Provider configuration with database settings
|
|
3759
|
+
*/
|
|
3760
|
+
constructor(config, features) {
|
|
3761
|
+
super(config, features);
|
|
3762
|
+
throw new Error("Database provider requires @plyaz/db package implementation");
|
|
3763
|
+
}
|
|
3764
|
+
/**
|
|
3765
|
+
* Initialize database connection
|
|
3766
|
+
*/
|
|
3767
|
+
async initialize() {
|
|
3768
|
+
await this.connectionManager.initialize();
|
|
3769
|
+
await super.initialize();
|
|
3770
|
+
}
|
|
3771
|
+
/**
|
|
3772
|
+
* Fetches flags and rules from the database.
|
|
3773
|
+
*
|
|
3774
|
+
* @protected
|
|
3775
|
+
* @returns Promise with flags and rules from database
|
|
3776
|
+
*/
|
|
3777
|
+
async fetchData() {
|
|
3778
|
+
throw new Error(
|
|
3779
|
+
"Database Provider is not yet implemented.\n\nRequired Database Setup:\n- Install @plyaz/db package\n- Configure database connection\n- Run database migrations\n\nRequired Tables:\n- feature_flags\n- feature_flag_rules\n\nDatabase Schema:\nSee /src/backend/featureFlags/database/schema.ts for table definitions"
|
|
3780
|
+
);
|
|
3781
|
+
}
|
|
3782
|
+
/**
|
|
3783
|
+
* Dispose resources
|
|
3784
|
+
*/
|
|
3785
|
+
dispose() {
|
|
3786
|
+
super.dispose();
|
|
3787
|
+
this.connectionManager.close().catch((error) => {
|
|
3788
|
+
this.log("Error closing database connection:", error);
|
|
3789
|
+
});
|
|
3790
|
+
}
|
|
3791
|
+
/**
|
|
3792
|
+
* Validates the database provider configuration.
|
|
3793
|
+
*
|
|
3794
|
+
* @private
|
|
3795
|
+
* @throws Error if configuration is invalid or incomplete
|
|
3796
|
+
*/
|
|
3797
|
+
validateConfig() {
|
|
3798
|
+
this.validateProviderType();
|
|
3799
|
+
this.validateDatabaseConfig();
|
|
3800
|
+
this.validateConnectionString();
|
|
3801
|
+
this.logConfigurationStatus();
|
|
3802
|
+
}
|
|
3803
|
+
validateProviderType() {
|
|
3804
|
+
if (this.config.provider !== "database") {
|
|
3805
|
+
throw new Error('Database provider requires provider to be set to "database"');
|
|
3806
|
+
}
|
|
3807
|
+
}
|
|
3808
|
+
validateDatabaseConfig() {
|
|
3809
|
+
if (!this.config.databaseConfig) {
|
|
3810
|
+
throw new Error(
|
|
3811
|
+
'Database configuration is required for database provider. Set databaseConfig in your configuration.\nExample: databaseConfig: { connectionString: "postgresql://...", tableName: "feature_flags" }'
|
|
3812
|
+
);
|
|
3813
|
+
}
|
|
3814
|
+
}
|
|
3815
|
+
validateConnectionString() {
|
|
3816
|
+
const { connectionString, tableName } = this.config.databaseConfig;
|
|
3817
|
+
if (!connectionString) {
|
|
3818
|
+
throw new Error(
|
|
3819
|
+
'Database connection string is required. Set connectionString in your databaseConfig.\nExample: connectionString: "postgresql://user:pass@localhost:5432/plyaz"'
|
|
3820
|
+
);
|
|
3821
|
+
}
|
|
3822
|
+
if (!this.isValidPostgresUrl(connectionString) && !this.isValidMysqlUrl(connectionString)) {
|
|
3823
|
+
throw new Error(
|
|
3824
|
+
`Database connection string must be a valid PostgreSQL or MySQL URL. Received: ${connectionString}
|
|
3825
|
+
Examples:
|
|
3826
|
+
- PostgreSQL: "postgresql://user:pass@localhost:5432/plyaz"
|
|
3827
|
+
- MySQL: "mysql://user:pass@localhost:3306/plyaz"`
|
|
3828
|
+
);
|
|
3829
|
+
}
|
|
3830
|
+
if (!tableName || typeof tableName !== "string") {
|
|
3831
|
+
throw new Error("Database provider requires databaseConfig.tableName");
|
|
3832
|
+
}
|
|
3833
|
+
}
|
|
3834
|
+
logConfigurationStatus() {
|
|
3835
|
+
const { connectionString, tableName } = this.config.databaseConfig;
|
|
3836
|
+
this.log("Database provider configuration is valid, but implementation is not ready");
|
|
3837
|
+
this.log("Connection String:", this.maskConnectionString(connectionString));
|
|
3838
|
+
this.log("Table Name:", tableName ?? "feature_flags (default)");
|
|
3839
|
+
}
|
|
3840
|
+
/**
|
|
3841
|
+
* Validates PostgreSQL URL format.
|
|
3842
|
+
*
|
|
3843
|
+
* @private
|
|
3844
|
+
* @param url - URL to validate
|
|
3845
|
+
* @returns True if valid PostgreSQL URL
|
|
3846
|
+
*/
|
|
3847
|
+
isValidPostgresUrl(url) {
|
|
3848
|
+
return url.startsWith("postgresql://") || url.startsWith("postgres://");
|
|
3849
|
+
}
|
|
3850
|
+
/**
|
|
3851
|
+
* Validates MySQL URL format.
|
|
3852
|
+
*
|
|
3853
|
+
* @private
|
|
3854
|
+
* @param url - URL to validate
|
|
3855
|
+
* @returns True if valid MySQL URL
|
|
3856
|
+
*/
|
|
3857
|
+
isValidMysqlUrl(url) {
|
|
3858
|
+
return url.startsWith("mysql://");
|
|
3859
|
+
}
|
|
3860
|
+
/**
|
|
3861
|
+
* Masks sensitive parts of connection string for logging.
|
|
3862
|
+
*
|
|
3863
|
+
* @private
|
|
3864
|
+
* @param connectionString - Original connection string
|
|
3865
|
+
* @returns Masked connection string
|
|
3866
|
+
*/
|
|
3867
|
+
maskConnectionString(connectionString) {
|
|
3868
|
+
try {
|
|
3869
|
+
const url = new URL(connectionString);
|
|
3870
|
+
const masked = `${url.protocol}//${url.username}:****@${url.host}${url.pathname}`;
|
|
3871
|
+
return masked;
|
|
3872
|
+
} catch {
|
|
3873
|
+
return "[INVALID_URL]";
|
|
3874
|
+
}
|
|
3875
|
+
}
|
|
3876
|
+
/**
|
|
3877
|
+
* Gets database provider status and configuration info.
|
|
3878
|
+
*
|
|
3879
|
+
* @returns Database provider status information
|
|
3880
|
+
*/
|
|
3881
|
+
getDatabaseInfo() {
|
|
3882
|
+
return {
|
|
3883
|
+
connectionString: this.config.databaseConfig?.connectionString ? this.maskConnectionString(this.config.databaseConfig.connectionString) : void 0,
|
|
3884
|
+
tableName: this.config.databaseConfig?.tableName ?? "feature_flags",
|
|
3885
|
+
isImplemented: false,
|
|
3886
|
+
requiredPackages: ["@plyaz/db"],
|
|
3887
|
+
recommendedORM: ["drizzle-orm", "prisma"],
|
|
3888
|
+
documentationPath: "/docs/feature-flag-to-implement/database-requirements.md",
|
|
3889
|
+
schemaPath: "/docs/feature-flag-to-implement/database-requirements.md#database-schema"
|
|
3890
|
+
};
|
|
3891
|
+
}
|
|
3892
|
+
};
|
|
3893
|
+
var PROVIDER_REGISTRY = {
|
|
3894
|
+
memory: MemoryFeatureFlagProvider,
|
|
3895
|
+
file: FileFeatureFlagProvider,
|
|
3896
|
+
redis: RedisFeatureFlagProvider,
|
|
3897
|
+
api: ApiFeatureFlagProvider,
|
|
3898
|
+
database: DatabaseFeatureFlagProvider
|
|
3899
|
+
};
|
|
3900
|
+
var FeatureFlagProviderFactory = class {
|
|
3901
|
+
static {
|
|
3902
|
+
__name(this, "FeatureFlagProviderFactory");
|
|
3903
|
+
}
|
|
3904
|
+
/**
|
|
3905
|
+
* Creates a new feature flag provider instance based on configuration.
|
|
3906
|
+
*
|
|
3907
|
+
* @param config - Provider configuration
|
|
3908
|
+
* @param features - Record of feature flag keys to their default values
|
|
3909
|
+
* @returns Configured provider instance
|
|
3910
|
+
* @throws Error if provider type is unsupported or configuration is invalid
|
|
3911
|
+
*/
|
|
3912
|
+
static create(config, features) {
|
|
3913
|
+
this.validateConfig(config);
|
|
3914
|
+
const ProviderClass = PROVIDER_REGISTRY[config.provider];
|
|
3915
|
+
if (!ProviderClass) {
|
|
3916
|
+
throw new Error(
|
|
3917
|
+
`Unsupported provider type: ${config.provider}. Supported types: ${this.getSupportedProviders().join(", ")}`
|
|
3918
|
+
);
|
|
3919
|
+
}
|
|
3920
|
+
try {
|
|
3921
|
+
return new ProviderClass(config, features);
|
|
3922
|
+
} catch (error) {
|
|
3923
|
+
throw new Error(
|
|
3924
|
+
`Failed to create ${config.provider} provider: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
3925
|
+
);
|
|
3926
|
+
}
|
|
3927
|
+
}
|
|
3928
|
+
/**
|
|
3929
|
+
* Creates a provider with automatic initialization.
|
|
3930
|
+
*
|
|
3931
|
+
* @param config - Provider configuration
|
|
3932
|
+
* @param features - Record of feature flag keys to their default values
|
|
3933
|
+
* @returns Promise resolving to initialized provider instance
|
|
3934
|
+
*/
|
|
3935
|
+
static async createAndInitialize(config, features) {
|
|
3936
|
+
const provider = this.create(config, features);
|
|
3937
|
+
await provider.initialize();
|
|
3938
|
+
return provider;
|
|
3939
|
+
}
|
|
3940
|
+
/**
|
|
3941
|
+
* Gets a list of all supported provider types.
|
|
3942
|
+
*
|
|
3943
|
+
* @returns Array of supported provider names
|
|
3944
|
+
*/
|
|
3945
|
+
static getSupportedProviders() {
|
|
3946
|
+
return Object.keys(PROVIDER_REGISTRY);
|
|
3947
|
+
}
|
|
3948
|
+
/**
|
|
3949
|
+
* Checks if a provider type is supported.
|
|
3950
|
+
*
|
|
3951
|
+
* @param providerType - Provider type to check
|
|
3952
|
+
* @returns True if provider type is supported
|
|
3953
|
+
*/
|
|
3954
|
+
static isProviderSupported(providerType) {
|
|
3955
|
+
return providerType in PROVIDER_REGISTRY;
|
|
3956
|
+
}
|
|
3957
|
+
/**
|
|
3958
|
+
* Gets provider information including implementation status.
|
|
3959
|
+
*
|
|
3960
|
+
* @returns Record of provider information
|
|
3961
|
+
*/
|
|
3962
|
+
static getProvidersInfo() {
|
|
3963
|
+
return {
|
|
3964
|
+
memory: {
|
|
2899
3965
|
name: "Memory Provider",
|
|
2900
3966
|
isImplemented: true,
|
|
2901
3967
|
description: "In-memory provider using FEATURES constant"
|
|
@@ -3123,9 +4189,10 @@ exports.FeatureFlagController = class FeatureFlagController {
|
|
|
3123
4189
|
try {
|
|
3124
4190
|
return await this.featureFlagService.evaluateFlag(key, body.context);
|
|
3125
4191
|
} catch (error) {
|
|
3126
|
-
throw new
|
|
3127
|
-
|
|
3128
|
-
|
|
4192
|
+
throw new errors.BaseError(
|
|
4193
|
+
types.ERROR_CODES.API_INVALID_INPUT,
|
|
4194
|
+
types.HTTP_STATUS.BAD_REQUEST,
|
|
4195
|
+
`Failed to evaluate flag: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
3129
4196
|
);
|
|
3130
4197
|
}
|
|
3131
4198
|
}
|
|
@@ -3134,9 +4201,10 @@ exports.FeatureFlagController = class FeatureFlagController {
|
|
|
3134
4201
|
const isEnabled = await this.featureFlagService.isEnabled(key, body.context);
|
|
3135
4202
|
return { isEnabled };
|
|
3136
4203
|
} catch (error) {
|
|
3137
|
-
throw new
|
|
3138
|
-
|
|
3139
|
-
|
|
4204
|
+
throw new errors.BaseError(
|
|
4205
|
+
types.ERROR_CODES.API_INVALID_INPUT,
|
|
4206
|
+
types.HTTP_STATUS.BAD_REQUEST,
|
|
4207
|
+
`Failed to check flag status: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
3140
4208
|
);
|
|
3141
4209
|
}
|
|
3142
4210
|
}
|
|
@@ -3144,9 +4212,10 @@ exports.FeatureFlagController = class FeatureFlagController {
|
|
|
3144
4212
|
try {
|
|
3145
4213
|
return await this.featureFlagService.getAllFlags(body.context);
|
|
3146
4214
|
} catch (error) {
|
|
3147
|
-
throw new
|
|
3148
|
-
|
|
3149
|
-
|
|
4215
|
+
throw new errors.BaseError(
|
|
4216
|
+
types.ERROR_CODES.SERVER_ERROR,
|
|
4217
|
+
types.HTTP_STATUS.INTERNAL_SERVER_ERROR,
|
|
4218
|
+
`Failed to evaluate all flags: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
3150
4219
|
);
|
|
3151
4220
|
}
|
|
3152
4221
|
}
|
|
@@ -3154,9 +4223,10 @@ exports.FeatureFlagController = class FeatureFlagController {
|
|
|
3154
4223
|
try {
|
|
3155
4224
|
return await this.featureFlagService.createFlag(createData);
|
|
3156
4225
|
} catch (error) {
|
|
3157
|
-
throw new
|
|
3158
|
-
|
|
3159
|
-
|
|
4226
|
+
throw new errors.BaseError(
|
|
4227
|
+
types.ERROR_CODES.API_INVALID_INPUT,
|
|
4228
|
+
types.HTTP_STATUS.BAD_REQUEST,
|
|
4229
|
+
`Failed to create flag: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
3160
4230
|
);
|
|
3161
4231
|
}
|
|
3162
4232
|
}
|
|
@@ -3164,9 +4234,10 @@ exports.FeatureFlagController = class FeatureFlagController {
|
|
|
3164
4234
|
try {
|
|
3165
4235
|
return await this.featureFlagService.updateFlag(key, updateData);
|
|
3166
4236
|
} catch (error) {
|
|
3167
|
-
throw new
|
|
3168
|
-
|
|
3169
|
-
|
|
4237
|
+
throw new errors.BaseError(
|
|
4238
|
+
types.ERROR_CODES.API_INVALID_INPUT,
|
|
4239
|
+
types.HTTP_STATUS.BAD_REQUEST,
|
|
4240
|
+
`Failed to update flag: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
3170
4241
|
);
|
|
3171
4242
|
}
|
|
3172
4243
|
}
|
|
@@ -3175,9 +4246,10 @@ exports.FeatureFlagController = class FeatureFlagController {
|
|
|
3175
4246
|
await this.featureFlagService.deleteFlag(key);
|
|
3176
4247
|
return { isSuccessful: true };
|
|
3177
4248
|
} catch (error) {
|
|
3178
|
-
throw new
|
|
3179
|
-
|
|
3180
|
-
|
|
4249
|
+
throw new errors.BaseError(
|
|
4250
|
+
types.ERROR_CODES.API_INVALID_INPUT,
|
|
4251
|
+
types.HTTP_STATUS.BAD_REQUEST,
|
|
4252
|
+
`Failed to delete flag: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
3181
4253
|
);
|
|
3182
4254
|
}
|
|
3183
4255
|
}
|
|
@@ -3186,9 +4258,10 @@ exports.FeatureFlagController = class FeatureFlagController {
|
|
|
3186
4258
|
await this.featureFlagService.setOverride(key, value);
|
|
3187
4259
|
return { isSuccessful: true };
|
|
3188
4260
|
} catch (error) {
|
|
3189
|
-
throw new
|
|
3190
|
-
|
|
3191
|
-
|
|
4261
|
+
throw new errors.BaseError(
|
|
4262
|
+
types.ERROR_CODES.API_INVALID_INPUT,
|
|
4263
|
+
types.HTTP_STATUS.BAD_REQUEST,
|
|
4264
|
+
`Failed to set override: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
3192
4265
|
);
|
|
3193
4266
|
}
|
|
3194
4267
|
}
|
|
@@ -3197,9 +4270,10 @@ exports.FeatureFlagController = class FeatureFlagController {
|
|
|
3197
4270
|
await this.featureFlagService.removeOverride(key);
|
|
3198
4271
|
return { isSuccessful: true };
|
|
3199
4272
|
} catch (error) {
|
|
3200
|
-
throw new
|
|
3201
|
-
|
|
3202
|
-
|
|
4273
|
+
throw new errors.BaseError(
|
|
4274
|
+
types.ERROR_CODES.API_INVALID_INPUT,
|
|
4275
|
+
types.HTTP_STATUS.BAD_REQUEST,
|
|
4276
|
+
`Failed to remove override: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
3203
4277
|
);
|
|
3204
4278
|
}
|
|
3205
4279
|
}
|
|
@@ -3207,9 +4281,10 @@ exports.FeatureFlagController = class FeatureFlagController {
|
|
|
3207
4281
|
try {
|
|
3208
4282
|
return await this.featureFlagService.getAllFeatureFlags(environment);
|
|
3209
4283
|
} catch (error) {
|
|
3210
|
-
throw new
|
|
3211
|
-
|
|
3212
|
-
|
|
4284
|
+
throw new errors.BaseError(
|
|
4285
|
+
types.ERROR_CODES.SERVER_ERROR,
|
|
4286
|
+
types.HTTP_STATUS.INTERNAL_SERVER_ERROR,
|
|
4287
|
+
`Failed to get flags: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
3213
4288
|
);
|
|
3214
4289
|
}
|
|
3215
4290
|
}
|
|
@@ -3217,9 +4292,10 @@ exports.FeatureFlagController = class FeatureFlagController {
|
|
|
3217
4292
|
try {
|
|
3218
4293
|
return await this.featureFlagService.getFlagRules(key);
|
|
3219
4294
|
} catch (error) {
|
|
3220
|
-
throw new
|
|
3221
|
-
|
|
3222
|
-
|
|
4295
|
+
throw new errors.BaseError(
|
|
4296
|
+
types.ERROR_CODES.API_INVALID_INPUT,
|
|
4297
|
+
types.HTTP_STATUS.BAD_REQUEST,
|
|
4298
|
+
`Failed to get flag rules: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
3223
4299
|
);
|
|
3224
4300
|
}
|
|
3225
4301
|
}
|
|
@@ -3228,64 +4304,443 @@ exports.FeatureFlagController = class FeatureFlagController {
|
|
|
3228
4304
|
await this.featureFlagService.refreshCache();
|
|
3229
4305
|
return { isSuccessful: true };
|
|
3230
4306
|
} catch (error) {
|
|
3231
|
-
throw new
|
|
3232
|
-
|
|
3233
|
-
|
|
4307
|
+
throw new errors.BaseError(
|
|
4308
|
+
types.ERROR_CODES.SERVER_ERROR,
|
|
4309
|
+
types.HTTP_STATUS.INTERNAL_SERVER_ERROR,
|
|
4310
|
+
`Failed to refresh cache: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
4311
|
+
);
|
|
4312
|
+
}
|
|
4313
|
+
}
|
|
4314
|
+
};
|
|
4315
|
+
__name(exports.FeatureFlagController, "FeatureFlagController");
|
|
4316
|
+
__decorateClass([
|
|
4317
|
+
common.Post(":key/evaluate"),
|
|
4318
|
+
__decorateParam(0, common.Param("key")),
|
|
4319
|
+
__decorateParam(1, common.Body())
|
|
4320
|
+
], exports.FeatureFlagController.prototype, "evaluateFlag", 1);
|
|
4321
|
+
__decorateClass([
|
|
4322
|
+
common.Post(":key/enabled"),
|
|
4323
|
+
__decorateParam(0, common.Param("key")),
|
|
4324
|
+
__decorateParam(1, common.Body())
|
|
4325
|
+
], exports.FeatureFlagController.prototype, "isEnabled", 1);
|
|
4326
|
+
__decorateClass([
|
|
4327
|
+
common.Post("evaluate-all"),
|
|
4328
|
+
__decorateParam(0, common.Body())
|
|
4329
|
+
], exports.FeatureFlagController.prototype, "evaluateAllFlags", 1);
|
|
4330
|
+
__decorateClass([
|
|
4331
|
+
common.Post(),
|
|
4332
|
+
__decorateParam(0, common.Body())
|
|
4333
|
+
], exports.FeatureFlagController.prototype, "createFlag", 1);
|
|
4334
|
+
__decorateClass([
|
|
4335
|
+
common.Put(":key"),
|
|
4336
|
+
__decorateParam(0, common.Param("key")),
|
|
4337
|
+
__decorateParam(1, common.Body())
|
|
4338
|
+
], exports.FeatureFlagController.prototype, "updateFlag", 1);
|
|
4339
|
+
__decorateClass([
|
|
4340
|
+
common.Delete(":key"),
|
|
4341
|
+
__decorateParam(0, common.Param("key"))
|
|
4342
|
+
], exports.FeatureFlagController.prototype, "deleteFlag", 1);
|
|
4343
|
+
__decorateClass([
|
|
4344
|
+
common.Post(":key/override"),
|
|
4345
|
+
__decorateParam(0, common.Param("key")),
|
|
4346
|
+
__decorateParam(1, common.Body("value"))
|
|
4347
|
+
], exports.FeatureFlagController.prototype, "setOverride", 1);
|
|
4348
|
+
__decorateClass([
|
|
4349
|
+
common.Delete(":key/override"),
|
|
4350
|
+
__decorateParam(0, common.Param("key"))
|
|
4351
|
+
], exports.FeatureFlagController.prototype, "removeOverride", 1);
|
|
4352
|
+
__decorateClass([
|
|
4353
|
+
common.Get(),
|
|
4354
|
+
__decorateParam(0, common.Query("environment"))
|
|
4355
|
+
], exports.FeatureFlagController.prototype, "getAllFeatureFlags", 1);
|
|
4356
|
+
__decorateClass([
|
|
4357
|
+
common.Get(":key/rules"),
|
|
4358
|
+
__decorateParam(0, common.Param("key"))
|
|
4359
|
+
], exports.FeatureFlagController.prototype, "getFlagRules", 1);
|
|
4360
|
+
__decorateClass([
|
|
4361
|
+
common.Post("refresh")
|
|
4362
|
+
], exports.FeatureFlagController.prototype, "refreshCache", 1);
|
|
4363
|
+
exports.FeatureFlagController = __decorateClass([
|
|
4364
|
+
common.Controller("feature-flags")
|
|
4365
|
+
], exports.FeatureFlagController);
|
|
4366
|
+
var FeatureFlagConfigValidator = class {
|
|
4367
|
+
static {
|
|
4368
|
+
__name(this, "FeatureFlagConfigValidator");
|
|
4369
|
+
}
|
|
4370
|
+
/**
|
|
4371
|
+
* Validates feature flag configuration and returns detailed results
|
|
4372
|
+
*
|
|
4373
|
+
* @description Performs comprehensive validation of feature flag configuration,
|
|
4374
|
+
* checking all aspects including provider settings, cache configuration,
|
|
4375
|
+
* database connections, and environment-specific settings.
|
|
4376
|
+
*
|
|
4377
|
+
* @param {FeatureFlagEnvironmentConfig} config - Configuration object to validate
|
|
4378
|
+
* @returns {ValidationResult} Validation result with errors and warnings
|
|
4379
|
+
*
|
|
4380
|
+
* @example Successful Validation
|
|
4381
|
+
* ```typescript
|
|
4382
|
+
* const validConfig = {
|
|
4383
|
+
* provider: 'memory',
|
|
4384
|
+
* isCacheEnabled: true,
|
|
4385
|
+
* cacheTtl: 300,
|
|
4386
|
+
* refreshInterval: 60
|
|
4387
|
+
* };
|
|
4388
|
+
*
|
|
4389
|
+
* const result = FeatureFlagConfigValidator.validate(validConfig);
|
|
4390
|
+
* // result.isValid = true
|
|
4391
|
+
* // result.errors = []
|
|
4392
|
+
* // result.warnings = []
|
|
4393
|
+
* ```
|
|
4394
|
+
*
|
|
4395
|
+
* @example Validation with Errors
|
|
4396
|
+
* ```typescript
|
|
4397
|
+
* const invalidConfig = {
|
|
4398
|
+
* provider: 'invalid_provider', // Error: invalid provider
|
|
4399
|
+
* cacheTtl: -100, // Error: negative TTL
|
|
4400
|
+
* databaseConfig: { // Error: missing connection string
|
|
4401
|
+
* tableName: 'flags'
|
|
4402
|
+
* }
|
|
4403
|
+
* };
|
|
4404
|
+
*
|
|
4405
|
+
* const result = FeatureFlagConfigValidator.validate(invalidConfig);
|
|
4406
|
+
* // result.isValid = false
|
|
4407
|
+
* // result.errors = [
|
|
4408
|
+
* // { field: 'provider', message: 'Invalid provider...', code: 'INVALID_PROVIDER' },
|
|
4409
|
+
* // { field: 'cacheTtl', message: 'Cache TTL must be non-negative', code: 'INVALID_CACHE_TTL' }
|
|
4410
|
+
* // ]
|
|
4411
|
+
* ```
|
|
4412
|
+
*
|
|
4413
|
+
* @example Validation with Warnings
|
|
4414
|
+
* ```typescript
|
|
4415
|
+
* const configWithWarnings = {
|
|
4416
|
+
* provider: 'database',
|
|
4417
|
+
* cacheTtl: 7200, // Warning: very high TTL
|
|
4418
|
+
* isLoggingEnabled: true, // Warning: logging in production
|
|
4419
|
+
* databaseConfig: {
|
|
4420
|
+
* connectionString: 'https://project.supabase.co',
|
|
4421
|
+
* tableName: 'feature_flags'
|
|
4422
|
+
* }
|
|
4423
|
+
* };
|
|
4424
|
+
*
|
|
4425
|
+
* process.env.NODE_ENV = 'production';
|
|
4426
|
+
* const result = FeatureFlagConfigValidator.validate(configWithWarnings);
|
|
4427
|
+
* // result.isValid = true (warnings don't fail validation)
|
|
4428
|
+
* // result.warnings = [
|
|
4429
|
+
* // { field: 'cacheTtl', message: 'Cache TTL is very high...', code: 'HIGH_CACHE_TTL' },
|
|
4430
|
+
* // { field: 'isLoggingEnabled', message: 'Logging is enabled in production...', code: 'PRODUCTION_LOGGING_ENABLED' }
|
|
4431
|
+
* // ]
|
|
4432
|
+
* ```
|
|
4433
|
+
*/
|
|
4434
|
+
static validate(config$1) {
|
|
4435
|
+
try {
|
|
4436
|
+
this.validateProvider(config$1);
|
|
4437
|
+
this.validateCacheSettings(config$1);
|
|
4438
|
+
if (config$1.provider === config.FEATURE_FLAG_PROVIDERS.DATABASE) {
|
|
4439
|
+
this.validateDatabaseConfig(config$1);
|
|
4440
|
+
}
|
|
4441
|
+
return {
|
|
4442
|
+
isValid: true,
|
|
4443
|
+
errors: [],
|
|
4444
|
+
warnings: this.getWarnings(config$1)
|
|
4445
|
+
};
|
|
4446
|
+
} catch (error) {
|
|
4447
|
+
return {
|
|
4448
|
+
isValid: false,
|
|
4449
|
+
errors: [
|
|
4450
|
+
{
|
|
4451
|
+
field: "config",
|
|
4452
|
+
message: error instanceof Error ? error.message : "Validation failed",
|
|
4453
|
+
code: error instanceof errors.BaseError ? error.code : "VALIDATION_ERROR"
|
|
4454
|
+
}
|
|
4455
|
+
],
|
|
4456
|
+
warnings: []
|
|
4457
|
+
};
|
|
4458
|
+
}
|
|
4459
|
+
}
|
|
4460
|
+
static validateProvider(config) {
|
|
4461
|
+
const validProviders = getValidProviders();
|
|
4462
|
+
if (!validProviders.includes(config.provider)) {
|
|
4463
|
+
throw new errors.ValidationError(
|
|
4464
|
+
types.ERROR_CODES.CLIENT_INVALID_CONFIG,
|
|
4465
|
+
types.HTTP_STATUS.BAD_REQUEST,
|
|
4466
|
+
`Invalid provider: ${config.provider}. Must be one of: ${validProviders.join(", ")}`
|
|
4467
|
+
);
|
|
4468
|
+
}
|
|
4469
|
+
}
|
|
4470
|
+
static validateCacheSettings(config) {
|
|
4471
|
+
if (config.cacheTtl < 0) {
|
|
4472
|
+
throw new errors.ValidationError(
|
|
4473
|
+
types.ERROR_CODES.CLIENT_INVALID_CONFIG,
|
|
4474
|
+
types.HTTP_STATUS.BAD_REQUEST,
|
|
4475
|
+
"Cache TTL must be non-negative"
|
|
4476
|
+
);
|
|
4477
|
+
}
|
|
4478
|
+
if (config.refreshInterval < 0) {
|
|
4479
|
+
throw new errors.ValidationError(
|
|
4480
|
+
types.ERROR_CODES.CLIENT_INVALID_CONFIG,
|
|
4481
|
+
types.HTTP_STATUS.BAD_REQUEST,
|
|
4482
|
+
"Refresh interval must be non-negative"
|
|
4483
|
+
);
|
|
4484
|
+
}
|
|
4485
|
+
}
|
|
4486
|
+
static validateDatabaseConfig(config) {
|
|
4487
|
+
if (!config.databaseConfig) {
|
|
4488
|
+
throw new errors.ValidationError(
|
|
4489
|
+
types.ERROR_CODES.DB_CONFIG_REQUIRED,
|
|
4490
|
+
types.HTTP_STATUS.BAD_REQUEST,
|
|
4491
|
+
"Database configuration is required for database provider"
|
|
4492
|
+
);
|
|
4493
|
+
}
|
|
4494
|
+
const { connectionString, tableName, poolSize, timeout } = config.databaseConfig;
|
|
4495
|
+
this.validateConnectionString(connectionString);
|
|
4496
|
+
this.validateTableName(tableName);
|
|
4497
|
+
this.validatePoolSize(poolSize);
|
|
4498
|
+
this.validateTimeout(timeout);
|
|
4499
|
+
}
|
|
4500
|
+
static validateConnectionString(connectionString) {
|
|
4501
|
+
if (!connectionString) {
|
|
4502
|
+
throw new errors.ValidationError(
|
|
4503
|
+
types.ERROR_CODES.DB_CONFIG_REQUIRED,
|
|
4504
|
+
types.HTTP_STATUS.BAD_REQUEST,
|
|
4505
|
+
"Database connection string is required"
|
|
4506
|
+
);
|
|
4507
|
+
}
|
|
4508
|
+
try {
|
|
4509
|
+
new URL(connectionString);
|
|
4510
|
+
} catch {
|
|
4511
|
+
throw new errors.ValidationError(
|
|
4512
|
+
types.ERROR_CODES.CLIENT_INVALID_CONFIG,
|
|
4513
|
+
types.HTTP_STATUS.BAD_REQUEST,
|
|
4514
|
+
"Invalid database connection string format"
|
|
4515
|
+
);
|
|
4516
|
+
}
|
|
4517
|
+
}
|
|
4518
|
+
static validateTableName(tableName) {
|
|
4519
|
+
if (tableName && !isString(tableName)) {
|
|
4520
|
+
throw new errors.ValidationError(
|
|
4521
|
+
types.ERROR_CODES.CLIENT_INVALID_CONFIG,
|
|
4522
|
+
types.HTTP_STATUS.BAD_REQUEST,
|
|
4523
|
+
"Table name must be a string"
|
|
4524
|
+
);
|
|
4525
|
+
}
|
|
4526
|
+
}
|
|
4527
|
+
static validatePoolSize(poolSize) {
|
|
4528
|
+
if (isDefined(poolSize) && (poolSize < config.NUMERIC_CONSTANTS.MIN_POOL_SIZE || poolSize > config.NUMERIC_CONSTANTS.MAX_POOL_SIZE)) {
|
|
4529
|
+
throw new errors.ValidationError(
|
|
4530
|
+
types.ERROR_CODES.CLIENT_INVALID_CONFIG,
|
|
4531
|
+
types.HTTP_STATUS.BAD_REQUEST,
|
|
4532
|
+
"Pool size must be between 1 and 100"
|
|
4533
|
+
);
|
|
4534
|
+
}
|
|
4535
|
+
}
|
|
4536
|
+
static validateTimeout(timeout) {
|
|
4537
|
+
if (isDefined(timeout) && timeout < config.NUMERIC_CONSTANTS.MIN_TIMEOUT_MS) {
|
|
4538
|
+
throw new errors.ValidationError(
|
|
4539
|
+
types.ERROR_CODES.CLIENT_INVALID_CONFIG,
|
|
4540
|
+
types.HTTP_STATUS.BAD_REQUEST,
|
|
4541
|
+
"Timeout must be at least 1000ms"
|
|
4542
|
+
);
|
|
4543
|
+
}
|
|
4544
|
+
}
|
|
4545
|
+
static getWarnings(config$1) {
|
|
4546
|
+
const env = process.env.NODE_ENV;
|
|
4547
|
+
if (config$1.cacheTtl > config.NUMERIC_CONSTANTS.ONE_HOUR_SECONDS) {
|
|
4548
|
+
common.Logger.warn(
|
|
4549
|
+
"Cache TTL is very high (>1 hour). Consider reducing for better responsiveness.",
|
|
4550
|
+
{
|
|
4551
|
+
field: "cacheTtl",
|
|
4552
|
+
value: config$1.cacheTtl,
|
|
4553
|
+
code: "HIGH_CACHE_TTL"
|
|
4554
|
+
}
|
|
4555
|
+
);
|
|
4556
|
+
}
|
|
4557
|
+
if (env === types.NODE_ENVIRONMENTS.PRODUCTION && config$1.isLoggingEnabled) {
|
|
4558
|
+
common.Logger.warn("Logging is enabled in production. Consider disabling for performance.", {
|
|
4559
|
+
field: "isLoggingEnabled",
|
|
4560
|
+
code: "PRODUCTION_LOGGING_ENABLED"
|
|
4561
|
+
});
|
|
4562
|
+
}
|
|
4563
|
+
if (env === types.NODE_ENVIRONMENTS.DEVELOPMENT && !config$1.isCacheEnabled) {
|
|
4564
|
+
common.Logger.warn("Cache is disabled in development. This may impact performance testing.", {
|
|
4565
|
+
field: "isCacheEnabled",
|
|
4566
|
+
code: "DEVELOPMENT_CACHE_DISABLED"
|
|
4567
|
+
});
|
|
4568
|
+
}
|
|
4569
|
+
return [];
|
|
4570
|
+
}
|
|
4571
|
+
/**
|
|
4572
|
+
* Validates configuration and throws error if validation fails
|
|
4573
|
+
*
|
|
4574
|
+
* @description Convenience method that validates configuration and throws a detailed
|
|
4575
|
+
* error if validation fails. This is typically used during application startup
|
|
4576
|
+
* to ensure the system doesn't start with invalid configuration.
|
|
4577
|
+
*
|
|
4578
|
+
* @param {FeatureFlagEnvironmentConfig} config - Configuration to validate
|
|
4579
|
+
* @throws {DatabaseError} When validation fails with detailed error messages
|
|
4580
|
+
*
|
|
4581
|
+
* @example Startup Validation
|
|
4582
|
+
* ```typescript
|
|
4583
|
+
* // In FeatureFlagService.onModuleInit()
|
|
4584
|
+
* try {
|
|
4585
|
+
* const config = FeatureFlagConfigFactory.fromEnvironment();
|
|
4586
|
+
* FeatureFlagConfigValidator.validateOrThrow(config);
|
|
4587
|
+
*
|
|
4588
|
+
* // Configuration is valid, proceed with initialization
|
|
4589
|
+
* this.provider = FeatureFlagProviderFactory.create(config, FEATURES);
|
|
4590
|
+
* await this.provider.initialize();
|
|
4591
|
+
* } catch (error) {
|
|
4592
|
+
* this.logger.error('Configuration validation failed:', error.message);
|
|
4593
|
+
* throw error; // Prevent service from starting
|
|
4594
|
+
* }
|
|
4595
|
+
* ```
|
|
4596
|
+
*
|
|
4597
|
+
* @example Error Output
|
|
4598
|
+
* ```typescript
|
|
4599
|
+
* // When validation fails, throws DatabaseError with message:
|
|
4600
|
+
* // "Configuration validation failed:
|
|
4601
|
+
* // provider: Invalid provider: invalid_type. Must be one of: memory, file, redis, api, database
|
|
4602
|
+
* // cacheTtl: Cache TTL must be non-negative
|
|
4603
|
+
* // databaseConfig.connectionString: Database connection string is required"
|
|
4604
|
+
* ```
|
|
4605
|
+
*
|
|
4606
|
+
* @example Environment Variable Validation
|
|
4607
|
+
* ```bash
|
|
4608
|
+
* # Missing required environment variables:
|
|
4609
|
+
* # SUPABASE_URL= # Empty/missing
|
|
4610
|
+
* # FEATURE_FLAG_PROVIDER=database
|
|
4611
|
+
*
|
|
4612
|
+
* # Results in error:
|
|
4613
|
+
* # "Configuration validation failed:
|
|
4614
|
+
* # databaseConfig.connectionString: Database connection string is required"
|
|
4615
|
+
* ```
|
|
4616
|
+
*/
|
|
4617
|
+
static validateOrThrow(config$1) {
|
|
4618
|
+
this.validateProvider(config$1);
|
|
4619
|
+
this.validateCacheSettings(config$1);
|
|
4620
|
+
if (config$1.provider === config.FEATURE_FLAG_PROVIDERS.DATABASE) {
|
|
4621
|
+
this.validateDatabaseConfig(config$1);
|
|
4622
|
+
}
|
|
4623
|
+
}
|
|
4624
|
+
};
|
|
4625
|
+
var FeatureFlagConfigFactory = class {
|
|
4626
|
+
static {
|
|
4627
|
+
__name(this, "FeatureFlagConfigFactory");
|
|
4628
|
+
}
|
|
4629
|
+
/**
|
|
4630
|
+
* Creates configuration from environment variables
|
|
4631
|
+
*
|
|
4632
|
+
* **EXECUTION ORDER: This is called FIRST in the initialization chain**
|
|
4633
|
+
*
|
|
4634
|
+
* Flow:
|
|
4635
|
+
* 1. NestJS starts → FeatureFlagModule loads
|
|
4636
|
+
* 2. FeatureFlagService.onModuleInit() called
|
|
4637
|
+
* 3. → initializeProvider() called
|
|
4638
|
+
* 4. → **THIS METHOD CALLED** ← YOU ARE HERE
|
|
4639
|
+
* 5. → Configuration validated
|
|
4640
|
+
* 6. → Provider created and initialized
|
|
4641
|
+
* 7. → Database connection established
|
|
4642
|
+
*
|
|
4643
|
+
* @returns Validated configuration object ready for provider creation
|
|
4644
|
+
* @throws {FeatureFlagConfigError} If environment variables are missing or invalid
|
|
4645
|
+
*
|
|
4646
|
+
* @example
|
|
4647
|
+
* ```typescript
|
|
4648
|
+
* // This method reads from process.env and creates:
|
|
4649
|
+
* {
|
|
4650
|
+
* provider: 'database',
|
|
4651
|
+
* isCacheEnabled: true,
|
|
4652
|
+
* cacheTtl: 300,
|
|
4653
|
+
* databaseConfig: {
|
|
4654
|
+
* connectionString: 'https://your-project.supabase.co',
|
|
4655
|
+
* tableName: 'feature_flags',
|
|
4656
|
+
* poolSize: 10,
|
|
4657
|
+
* timeout: 30000
|
|
4658
|
+
* }
|
|
4659
|
+
* }
|
|
4660
|
+
* ```
|
|
4661
|
+
*/
|
|
4662
|
+
static fromEnvironment() {
|
|
4663
|
+
const config = {
|
|
4664
|
+
provider: this.getProvider(),
|
|
4665
|
+
isCacheEnabled: this.getCacheEnabled(),
|
|
4666
|
+
cacheTtl: this.getCacheTtl(),
|
|
4667
|
+
refreshInterval: this.getRefreshInterval(),
|
|
4668
|
+
shouldFallbackToDefaults: true,
|
|
4669
|
+
isLoggingEnabled: this.getLoggingEnabled()
|
|
4670
|
+
};
|
|
4671
|
+
if (config.provider === types.FEATURE_FLAG_PROVIDERS.DATABASE) {
|
|
4672
|
+
config.databaseConfig = this.getDatabaseConfig();
|
|
4673
|
+
}
|
|
4674
|
+
FeatureFlagConfigValidator.validateOrThrow(config);
|
|
4675
|
+
return config;
|
|
4676
|
+
}
|
|
4677
|
+
/**
|
|
4678
|
+
* Determines which provider to use - now uses constants instead of environment variables
|
|
4679
|
+
*
|
|
4680
|
+
* @private
|
|
4681
|
+
* @returns Provider type from constants
|
|
4682
|
+
*/
|
|
4683
|
+
static getProvider() {
|
|
4684
|
+
return types.FEATURE_FLAG_PROVIDERS.MEMORY;
|
|
4685
|
+
}
|
|
4686
|
+
static getCacheEnabled() {
|
|
4687
|
+
return true;
|
|
4688
|
+
}
|
|
4689
|
+
static getCacheTtl() {
|
|
4690
|
+
return types.FEATURE_FLAG_DEFAULTS.CACHE_TTL;
|
|
4691
|
+
}
|
|
4692
|
+
static getRefreshInterval() {
|
|
4693
|
+
return types.FEATURE_FLAG_DEFAULTS.REFRESH_INTERVAL;
|
|
4694
|
+
}
|
|
4695
|
+
static getLoggingEnabled() {
|
|
4696
|
+
return process.env.NODE_ENV === types.NODE_ENVIRONMENTS.DEVELOPMENT || process.env.FEATURE_FLAG_LOGGING === "true";
|
|
4697
|
+
}
|
|
4698
|
+
/**
|
|
4699
|
+
* Creates database configuration from environment variables
|
|
4700
|
+
*
|
|
4701
|
+
* **CRITICAL**: This method requires SUPABASE_URL to be set in .env.local
|
|
4702
|
+
*
|
|
4703
|
+
* @private
|
|
4704
|
+
* @returns Database configuration object
|
|
4705
|
+
* @throws {FeatureFlagConfigError} If SUPABASE_URL is missing
|
|
4706
|
+
*
|
|
4707
|
+
* @example Environment Variables Required:
|
|
4708
|
+
* ```bash
|
|
4709
|
+
* SUPABASE_URL=https://your-project.supabase.co # REQUIRED
|
|
4710
|
+
* SUPABASE_ANON_PUBLIC_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... # Used in connection
|
|
4711
|
+
* FEATURE_FLAG_TABLE_NAME=feature_flags # Optional, defaults to 'feature_flags'
|
|
4712
|
+
* DB_POOL_SIZE=10 # Optional, defaults to 10
|
|
4713
|
+
* DB_TIMEOUT=30000 # Optional, defaults to 30000ms
|
|
4714
|
+
* ```
|
|
4715
|
+
*
|
|
4716
|
+
* @example Generated Configuration:
|
|
4717
|
+
* ```typescript
|
|
4718
|
+
* {
|
|
4719
|
+
* connectionString: 'https://your-project.supabase.co',
|
|
4720
|
+
* tableName: 'feature_flags',
|
|
4721
|
+
* poolSize: 10,
|
|
4722
|
+
* timeout: 30000
|
|
4723
|
+
* }
|
|
4724
|
+
* ```
|
|
4725
|
+
*/
|
|
4726
|
+
static getDatabaseConfig() {
|
|
4727
|
+
const connectionString = process.env.SUPABASE_URL;
|
|
4728
|
+
if (!connectionString) {
|
|
4729
|
+
throw new errors.DatabaseError(
|
|
4730
|
+
"SUPABASE_URL is required for database provider",
|
|
4731
|
+
types.DATABASE_ERROR_CODES.CONFIG_REQUIRED
|
|
3234
4732
|
);
|
|
3235
4733
|
}
|
|
4734
|
+
return {
|
|
4735
|
+
connectionString,
|
|
4736
|
+
tableName: types.FEATURE_FLAG_DEFAULTS.TABLE_NAME,
|
|
4737
|
+
poolSize: types.FEATURE_FLAG_DEFAULTS.POOL_SIZE,
|
|
4738
|
+
timeout: types.FEATURE_FLAG_DEFAULTS.TIMEOUT
|
|
4739
|
+
};
|
|
3236
4740
|
}
|
|
3237
4741
|
};
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
common.Post(":key/evaluate"),
|
|
3241
|
-
__decorateParam(0, common.Param("key")),
|
|
3242
|
-
__decorateParam(1, common.Body())
|
|
3243
|
-
], exports.FeatureFlagController.prototype, "evaluateFlag", 1);
|
|
3244
|
-
__decorateClass([
|
|
3245
|
-
common.Post(":key/enabled"),
|
|
3246
|
-
__decorateParam(0, common.Param("key")),
|
|
3247
|
-
__decorateParam(1, common.Body())
|
|
3248
|
-
], exports.FeatureFlagController.prototype, "isEnabled", 1);
|
|
3249
|
-
__decorateClass([
|
|
3250
|
-
common.Post("evaluate-all"),
|
|
3251
|
-
__decorateParam(0, common.Body())
|
|
3252
|
-
], exports.FeatureFlagController.prototype, "evaluateAllFlags", 1);
|
|
3253
|
-
__decorateClass([
|
|
3254
|
-
common.Post(),
|
|
3255
|
-
__decorateParam(0, common.Body())
|
|
3256
|
-
], exports.FeatureFlagController.prototype, "createFlag", 1);
|
|
3257
|
-
__decorateClass([
|
|
3258
|
-
common.Put(":key"),
|
|
3259
|
-
__decorateParam(0, common.Param("key")),
|
|
3260
|
-
__decorateParam(1, common.Body())
|
|
3261
|
-
], exports.FeatureFlagController.prototype, "updateFlag", 1);
|
|
3262
|
-
__decorateClass([
|
|
3263
|
-
common.Delete(":key"),
|
|
3264
|
-
__decorateParam(0, common.Param("key"))
|
|
3265
|
-
], exports.FeatureFlagController.prototype, "deleteFlag", 1);
|
|
3266
|
-
__decorateClass([
|
|
3267
|
-
common.Post(":key/override"),
|
|
3268
|
-
__decorateParam(0, common.Param("key")),
|
|
3269
|
-
__decorateParam(1, common.Body("value"))
|
|
3270
|
-
], exports.FeatureFlagController.prototype, "setOverride", 1);
|
|
3271
|
-
__decorateClass([
|
|
3272
|
-
common.Delete(":key/override"),
|
|
3273
|
-
__decorateParam(0, common.Param("key"))
|
|
3274
|
-
], exports.FeatureFlagController.prototype, "removeOverride", 1);
|
|
3275
|
-
__decorateClass([
|
|
3276
|
-
common.Get(),
|
|
3277
|
-
__decorateParam(0, common.Query("environment"))
|
|
3278
|
-
], exports.FeatureFlagController.prototype, "getAllFeatureFlags", 1);
|
|
3279
|
-
__decorateClass([
|
|
3280
|
-
common.Get(":key/rules"),
|
|
3281
|
-
__decorateParam(0, common.Param("key"))
|
|
3282
|
-
], exports.FeatureFlagController.prototype, "getFlagRules", 1);
|
|
3283
|
-
__decorateClass([
|
|
3284
|
-
common.Post("refresh")
|
|
3285
|
-
], exports.FeatureFlagController.prototype, "refreshCache", 1);
|
|
3286
|
-
exports.FeatureFlagController = __decorateClass([
|
|
3287
|
-
common.Controller("feature-flags")
|
|
3288
|
-
], exports.FeatureFlagController);
|
|
4742
|
+
|
|
4743
|
+
// src/backend/featureFlags/feature-flag.service.ts
|
|
3289
4744
|
exports.FeatureFlagService = class FeatureFlagService {
|
|
3290
4745
|
constructor(featureFlagRepository) {
|
|
3291
4746
|
this.featureFlagRepository = featureFlagRepository;
|
|
@@ -3293,7 +4748,35 @@ exports.FeatureFlagService = class FeatureFlagService {
|
|
|
3293
4748
|
logger = new common.Logger(exports.FeatureFlagService.name);
|
|
3294
4749
|
provider;
|
|
3295
4750
|
/**
|
|
3296
|
-
*
|
|
4751
|
+
* **FIRST METHOD CALLED** - NestJS lifecycle hook for module initialization
|
|
4752
|
+
*
|
|
4753
|
+
* **EXECUTION ORDER:**
|
|
4754
|
+
* 1. NestJS creates FeatureFlagModule
|
|
4755
|
+
* 2. NestJS instantiates FeatureFlagService
|
|
4756
|
+
* 3. **THIS METHOD CALLED AUTOMATICALLY** ← YOU ARE HERE
|
|
4757
|
+
* 4. → initializeProvider() called
|
|
4758
|
+
* 5. → Configuration loaded from .env.local
|
|
4759
|
+
* 6. → Database provider created and initialized
|
|
4760
|
+
* 7. → Database connection established
|
|
4761
|
+
* 8. → Initial data loaded from database
|
|
4762
|
+
* 9. System ready to serve requests
|
|
4763
|
+
*
|
|
4764
|
+
* @throws {Error} If provider initialization fails
|
|
4765
|
+
*
|
|
4766
|
+
* @example What happens during initialization:
|
|
4767
|
+
* ```typescript
|
|
4768
|
+
* // 1. Load config from environment
|
|
4769
|
+
* const config = FeatureFlagConfigFactory.fromEnvironment();
|
|
4770
|
+
*
|
|
4771
|
+
* // 2. Create database provider
|
|
4772
|
+
* this.provider = FeatureFlagProviderFactory.create(config, FEATURES);
|
|
4773
|
+
*
|
|
4774
|
+
* // 3. Initialize database connection
|
|
4775
|
+
* await this.provider.initialize();
|
|
4776
|
+
*
|
|
4777
|
+
* // 4. Load flags and rules from database
|
|
4778
|
+
* const { flags, rules } = await this.provider.fetchData();
|
|
4779
|
+
* ```
|
|
3297
4780
|
*/
|
|
3298
4781
|
async onModuleInit() {
|
|
3299
4782
|
try {
|
|
@@ -3312,36 +4795,115 @@ exports.FeatureFlagService = class FeatureFlagService {
|
|
|
3312
4795
|
this.logger.log("Feature flag service disposed");
|
|
3313
4796
|
}
|
|
3314
4797
|
/**
|
|
3315
|
-
* Initializes the feature flag provider
|
|
4798
|
+
* **SECOND METHOD CALLED** - Initializes the feature flag provider
|
|
4799
|
+
*
|
|
4800
|
+
* **EXECUTION FLOW:**
|
|
4801
|
+
* 1. onModuleInit() called by NestJS
|
|
4802
|
+
* 2. **THIS METHOD CALLED** ← YOU ARE HERE
|
|
4803
|
+
* 3. → FeatureFlagConfigFactory.fromEnvironment() - Loads .env.local
|
|
4804
|
+
* 4. → FeatureFlagProviderFactory.create() - Creates database provider
|
|
4805
|
+
* 5. → provider.initialize() - Establishes database connection
|
|
4806
|
+
*
|
|
4807
|
+
* @private
|
|
4808
|
+
* @throws {Error} If configuration is invalid or database connection fails
|
|
4809
|
+
*
|
|
4810
|
+
* @example Configuration Loading Process:
|
|
4811
|
+
* ```typescript
|
|
4812
|
+
* // Reads from .env.local:
|
|
4813
|
+
* // SUPABASE_URL=https://your-project.supabase.co
|
|
4814
|
+
* // FEATURE_FLAG_PROVIDER=database
|
|
4815
|
+
* // FEATURE_FLAG_CACHE_ENABLED=true
|
|
4816
|
+
*
|
|
4817
|
+
* const config = {
|
|
4818
|
+
* provider: 'database',
|
|
4819
|
+
* isCacheEnabled: true,
|
|
4820
|
+
* databaseConfig: {
|
|
4821
|
+
* connectionString: 'https://your-project.supabase.co',
|
|
4822
|
+
* tableName: 'feature_flags'
|
|
4823
|
+
* }
|
|
4824
|
+
* };
|
|
4825
|
+
* ```
|
|
3316
4826
|
*/
|
|
3317
4827
|
async initializeProvider() {
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
provider
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
4828
|
+
try {
|
|
4829
|
+
const config$1 = FeatureFlagConfigFactory.fromEnvironment();
|
|
4830
|
+
this.provider = FeatureFlagProviderFactory.create(config$1, config.FEATURES);
|
|
4831
|
+
await this.provider.initialize();
|
|
4832
|
+
this.logger.log(`Feature flag provider initialized: ${config$1.provider}`);
|
|
4833
|
+
} catch (error) {
|
|
4834
|
+
this.logger.error("Failed to initialize feature flag provider", error);
|
|
4835
|
+
throw new errors.BaseError(
|
|
4836
|
+
types.ERROR_CODES.CLIENT_INITIALIZATION_FAILED,
|
|
4837
|
+
types.HTTP_STATUS.INTERNAL_SERVER_ERROR,
|
|
4838
|
+
"Failed to initialize feature flag provider"
|
|
4839
|
+
);
|
|
4840
|
+
}
|
|
3329
4841
|
}
|
|
3330
4842
|
/**
|
|
3331
4843
|
* Gets the current provider instance.
|
|
3332
4844
|
*/
|
|
3333
4845
|
getProvider() {
|
|
3334
4846
|
if (!this.provider) {
|
|
3335
|
-
throw new
|
|
4847
|
+
throw new errors.BaseError(
|
|
4848
|
+
types.ERROR_CODES.ERROR_SYSTEM_NOT_INITIALIZED,
|
|
4849
|
+
types.HTTP_STATUS.INTERNAL_SERVER_ERROR,
|
|
4850
|
+
"Feature flag provider not initialized"
|
|
4851
|
+
);
|
|
3336
4852
|
}
|
|
3337
4853
|
return this.provider;
|
|
3338
4854
|
}
|
|
3339
4855
|
/**
|
|
3340
|
-
* Evaluates a feature flag for the given context
|
|
4856
|
+
* **MAIN RUNTIME METHOD** - Evaluates a feature flag for the given context
|
|
3341
4857
|
*
|
|
3342
|
-
*
|
|
3343
|
-
*
|
|
3344
|
-
*
|
|
4858
|
+
* **RUNTIME EXECUTION FLOW:**
|
|
4859
|
+
* 1. Client makes HTTP request to controller
|
|
4860
|
+
* 2. Controller calls **THIS METHOD** ← RUNTIME ENTRY POINT
|
|
4861
|
+
* 3. → getProvider() - Gets initialized provider
|
|
4862
|
+
* 4. → provider.getFlag() - Evaluates flag with context
|
|
4863
|
+
* 5. → Provider checks cache, rules, overrides
|
|
4864
|
+
* 6. → Database query if needed
|
|
4865
|
+
* 7. ← Returns evaluation result
|
|
4866
|
+
*
|
|
4867
|
+
* @param key - Feature flag key (e.g., 'PREMIUM_FEATURE', 'NEW_UI')
|
|
4868
|
+
* @param context - Evaluation context for targeting rules
|
|
4869
|
+
* @returns Feature flag evaluation result with value, reason, and metadata
|
|
4870
|
+
*
|
|
4871
|
+
* @example Simple Usage:
|
|
4872
|
+
* ```typescript
|
|
4873
|
+
* const evaluation = await this.featureFlagService.evaluateFlag('NEW_CHECKOUT');
|
|
4874
|
+
* console.log(evaluation.isEnabled); // true/false
|
|
4875
|
+
* console.log(evaluation.reason); // 'default_value' | 'rule_match' | 'override'
|
|
4876
|
+
* ```
|
|
4877
|
+
*
|
|
4878
|
+
* @example With User Context (for targeting rules):
|
|
4879
|
+
* ```typescript
|
|
4880
|
+
* const evaluation = await this.featureFlagService.evaluateFlag('BETA_FEATURE', {
|
|
4881
|
+
* userId: 'user123',
|
|
4882
|
+
* userRole: 'premium',
|
|
4883
|
+
* environment: 'production',
|
|
4884
|
+
* customAttributes: {
|
|
4885
|
+
* subscriptionTier: 'pro',
|
|
4886
|
+
* region: 'us-east'
|
|
4887
|
+
* }
|
|
4888
|
+
* });
|
|
4889
|
+
*
|
|
4890
|
+
* // Provider will:
|
|
4891
|
+
* // 1. Check for user-specific overrides
|
|
4892
|
+
* // 2. Evaluate targeting rules against context
|
|
4893
|
+
* // 3. Return appropriate value with reason
|
|
4894
|
+
* ```
|
|
4895
|
+
*
|
|
4896
|
+
* @example Evaluation Result:
|
|
4897
|
+
* ```typescript
|
|
4898
|
+
* {
|
|
4899
|
+
* key: 'BETA_FEATURE',
|
|
4900
|
+
* isEnabled: true,
|
|
4901
|
+
* value: { enabled: true, variant: 'blue' },
|
|
4902
|
+
* reason: 'rule_match',
|
|
4903
|
+
* ruleId: 'premium-users-rule',
|
|
4904
|
+
* evaluatedAt: '2024-01-15T10:30:00Z'
|
|
4905
|
+
* }
|
|
4906
|
+
* ```
|
|
3345
4907
|
*/
|
|
3346
4908
|
async evaluateFlag(key, context) {
|
|
3347
4909
|
try {
|
|
@@ -3520,10 +5082,8 @@ exports.FeatureFlagService = class FeatureFlagService {
|
|
|
3520
5082
|
async getHealthStatus() {
|
|
3521
5083
|
return {
|
|
3522
5084
|
isInitialized: !!this.provider,
|
|
3523
|
-
provider:
|
|
3524
|
-
// or get from config
|
|
5085
|
+
provider: types.FEATURE_FLAG_PROVIDERS.DATABASE,
|
|
3525
5086
|
isCacheEnabled: true
|
|
3526
|
-
// or get from config
|
|
3527
5087
|
};
|
|
3528
5088
|
}
|
|
3529
5089
|
};
|
|
@@ -3762,8 +5322,8 @@ exports.FeatureFlagModule = class FeatureFlagModule {
|
|
|
3762
5322
|
* @Module({
|
|
3763
5323
|
* imports: [
|
|
3764
5324
|
* FeatureFlagModule.forRoot({
|
|
3765
|
-
* provider: '
|
|
3766
|
-
*
|
|
5325
|
+
* provider: 'database',
|
|
5326
|
+
* isCacheEnabled: true,
|
|
3767
5327
|
* cacheTtl: 600,
|
|
3768
5328
|
* })
|
|
3769
5329
|
* ],
|
|
@@ -3772,12 +5332,14 @@ exports.FeatureFlagModule = class FeatureFlagModule {
|
|
|
3772
5332
|
* ```
|
|
3773
5333
|
*/
|
|
3774
5334
|
static forRoot(options) {
|
|
5335
|
+
const config = FeatureFlagConfigFactory.fromEnvironment();
|
|
5336
|
+
const mergedConfig = { ...config, ...options };
|
|
3775
5337
|
return {
|
|
3776
5338
|
module: exports.FeatureFlagModule,
|
|
3777
5339
|
providers: [
|
|
3778
5340
|
{
|
|
3779
5341
|
provide: "FEATURE_FLAG_CONFIG",
|
|
3780
|
-
useValue:
|
|
5342
|
+
useValue: mergedConfig
|
|
3781
5343
|
},
|
|
3782
5344
|
exports.FeatureFlagService,
|
|
3783
5345
|
exports.FeatureFlagRepository
|
|
@@ -3835,20 +5397,193 @@ exports.FeatureFlagModule = __decorateClass([
|
|
|
3835
5397
|
exports: [exports.FeatureFlagService, exports.FeatureFlagRepository]
|
|
3836
5398
|
})
|
|
3837
5399
|
], exports.FeatureFlagModule);
|
|
5400
|
+
function FeatureFlag(key, expected = true) {
|
|
5401
|
+
return common.SetMetadata(types.FEATURE_FLAG_METADATA.FLAG_CHECK, { key, expected });
|
|
5402
|
+
}
|
|
5403
|
+
__name(FeatureFlag, "FeatureFlag");
|
|
3838
5404
|
|
|
3839
|
-
// src/backend/featureFlags/
|
|
3840
|
-
function
|
|
3841
|
-
return
|
|
3842
|
-
return descriptor;
|
|
3843
|
-
};
|
|
5405
|
+
// src/backend/featureFlags/decorators/feature-disabled.decorator.ts
|
|
5406
|
+
function FeatureDisabled(key) {
|
|
5407
|
+
return FeatureFlag(key, false);
|
|
3844
5408
|
}
|
|
3845
|
-
__name(
|
|
3846
|
-
|
|
3847
|
-
|
|
3848
|
-
|
|
3849
|
-
|
|
5409
|
+
__name(FeatureDisabled, "FeatureDisabled");
|
|
5410
|
+
|
|
5411
|
+
// src/backend/featureFlags/decorators/feature-enabled.decorator.ts
|
|
5412
|
+
function FeatureEnabled(key) {
|
|
5413
|
+
return FeatureFlag(key, true);
|
|
3850
5414
|
}
|
|
3851
|
-
__name(
|
|
5415
|
+
__name(FeatureEnabled, "FeatureEnabled");
|
|
5416
|
+
exports.FeatureFlagGuard = class FeatureFlagGuard {
|
|
5417
|
+
constructor(reflector, featureFlagService) {
|
|
5418
|
+
this.reflector = reflector;
|
|
5419
|
+
this.featureFlagService = featureFlagService;
|
|
5420
|
+
}
|
|
5421
|
+
logger = new common.Logger(exports.FeatureFlagGuard.name);
|
|
5422
|
+
async canActivate(context) {
|
|
5423
|
+
const meta = this.reflector.get(
|
|
5424
|
+
types.FEATURE_FLAG_METADATA.FLAG_CHECK,
|
|
5425
|
+
context.getHandler()
|
|
5426
|
+
// to get keys
|
|
5427
|
+
);
|
|
5428
|
+
if (!meta) return true;
|
|
5429
|
+
const { key, expected } = meta;
|
|
5430
|
+
const evaluation = await this.featureFlagService.evaluateFlag(key);
|
|
5431
|
+
const actual = evaluation.value ?? evaluation.isEnabled;
|
|
5432
|
+
if (!this.matches(expected, actual)) {
|
|
5433
|
+
this.logger.log(
|
|
5434
|
+
`FeatureFlagGuard denied: key=${key}, expected=${expected}, actual=${actual}`
|
|
5435
|
+
);
|
|
5436
|
+
throw new errors.BaseError(
|
|
5437
|
+
types.ERROR_CODES.AUTH_FORBIDDEN,
|
|
5438
|
+
types.HTTP_STATUS.FORBIDDEN,
|
|
5439
|
+
`Feature ${key} not in required state`
|
|
5440
|
+
);
|
|
5441
|
+
}
|
|
5442
|
+
return true;
|
|
5443
|
+
}
|
|
5444
|
+
/**
|
|
5445
|
+
* Compare expected vs actual values.
|
|
5446
|
+
*/
|
|
5447
|
+
matches(expected, actual) {
|
|
5448
|
+
if (expected === void 0 || expected === null) return Boolean(actual);
|
|
5449
|
+
if (typeof actual !== "object" || actual === null) {
|
|
5450
|
+
return String(actual) === String(expected);
|
|
5451
|
+
}
|
|
5452
|
+
return JSON.stringify(actual) === JSON.stringify(expected);
|
|
5453
|
+
}
|
|
5454
|
+
};
|
|
5455
|
+
__name(exports.FeatureFlagGuard, "FeatureFlagGuard");
|
|
5456
|
+
exports.FeatureFlagGuard = __decorateClass([
|
|
5457
|
+
common.Injectable()
|
|
5458
|
+
], exports.FeatureFlagGuard);
|
|
5459
|
+
function isFeatureFlagKey(value) {
|
|
5460
|
+
return Object.keys(config.FEATURES).includes(value);
|
|
5461
|
+
}
|
|
5462
|
+
__name(isFeatureFlagKey, "isFeatureFlagKey");
|
|
5463
|
+
exports.FeatureFlagMiddleware = class FeatureFlagMiddleware {
|
|
5464
|
+
constructor(featureFlagService) {
|
|
5465
|
+
this.featureFlagService = featureFlagService;
|
|
5466
|
+
}
|
|
5467
|
+
logger = new common.Logger(exports.FeatureFlagMiddleware.name);
|
|
5468
|
+
/**
|
|
5469
|
+
* Main middleware execution method
|
|
5470
|
+
*
|
|
5471
|
+
* @description Processes incoming requests to evaluate and attach feature flags.
|
|
5472
|
+
* Handles flag key extraction from multiple sources and graceful error handling.
|
|
5473
|
+
*
|
|
5474
|
+
* @param {FeatureFlagRequest} req - Express request object with feature flag extensions
|
|
5475
|
+
* @param {unknown} res - Express response object (not used in this middleware)
|
|
5476
|
+
* @param {Function} next - Next middleware function in the chain
|
|
5477
|
+
*
|
|
5478
|
+
* @throws {BaseError} When flag key is invalid or evaluation fails
|
|
5479
|
+
*
|
|
5480
|
+
* @example Request Processing Flow
|
|
5481
|
+
* ```typescript
|
|
5482
|
+
* // 1. Extract flag key from request
|
|
5483
|
+
* const flagKey = req.query.flag || req.headers['x-feature-flag'] || 'AUTH_GOOGLE';
|
|
5484
|
+
*
|
|
5485
|
+
* // 2. Validate flag key exists in system
|
|
5486
|
+
* if (!isFeatureFlagKey(flagKey)) {
|
|
5487
|
+
* throw new Error('Invalid flag key');
|
|
5488
|
+
* }
|
|
5489
|
+
*
|
|
5490
|
+
* // 3. Evaluate flag using service
|
|
5491
|
+
* const evaluation = await this.featureFlagService.evaluateFlag(flagKey);
|
|
5492
|
+
*
|
|
5493
|
+
* // 4. Attach to request object
|
|
5494
|
+
* req.featureFlags = { [flagKey]: evaluation.isEnabled };
|
|
5495
|
+
*
|
|
5496
|
+
* // 5. Continue to next middleware
|
|
5497
|
+
* next();
|
|
5498
|
+
* ```
|
|
5499
|
+
*/
|
|
5500
|
+
// Use a simple next type to avoid conflicts with differing framework types across environments
|
|
5501
|
+
async use(req, res, next) {
|
|
5502
|
+
try {
|
|
5503
|
+
const flagKey = (
|
|
5504
|
+
// Prefer query param -> header -> default
|
|
5505
|
+
(req.query instanceof URLSearchParams ? req.query.get("flag") : req.query?.["flag"]) ?? req.headers?.["x-feature-flag"] ?? "AUTH_GOOGLE"
|
|
5506
|
+
);
|
|
5507
|
+
if (!isFeatureFlagKey(flagKey)) {
|
|
5508
|
+
throw new errors.BaseError(
|
|
5509
|
+
types.ERROR_CODES.RETRY_FAILED,
|
|
5510
|
+
config.HTTP_STATUS.NOT_FOUND,
|
|
5511
|
+
`Invalid feature flag key: ${flagKey}`
|
|
5512
|
+
);
|
|
5513
|
+
}
|
|
5514
|
+
const flagEvaluation = await this.featureFlagService.evaluateFlag(flagKey);
|
|
5515
|
+
req.featureFlags = {
|
|
5516
|
+
...req.featureFlags,
|
|
5517
|
+
[flagKey]: flagEvaluation.isEnabled
|
|
5518
|
+
};
|
|
5519
|
+
this.logger.debug(`Feature flags attached to request: ${JSON.stringify(req.featureFlags)}`);
|
|
5520
|
+
next();
|
|
5521
|
+
} catch (error) {
|
|
5522
|
+
this.logger.error("Error evaluating feature flags", error);
|
|
5523
|
+
throw new errors.BaseError(
|
|
5524
|
+
types.ERROR_CODES.RETRY_FAILED,
|
|
5525
|
+
config.HTTP_STATUS.SERVICE_UNAVAILABLE,
|
|
5526
|
+
"Failed to evaluate feature flag"
|
|
5527
|
+
);
|
|
5528
|
+
}
|
|
5529
|
+
}
|
|
5530
|
+
};
|
|
5531
|
+
__name(exports.FeatureFlagMiddleware, "FeatureFlagMiddleware");
|
|
5532
|
+
exports.FeatureFlagMiddleware = __decorateClass([
|
|
5533
|
+
common.Injectable()
|
|
5534
|
+
], exports.FeatureFlagMiddleware);
|
|
5535
|
+
exports.FeatureFlagLoggingInterceptor = class FeatureFlagLoggingInterceptor {
|
|
5536
|
+
logger = new common.Logger(exports.FeatureFlagLoggingInterceptor.name);
|
|
5537
|
+
intercept(context, next) {
|
|
5538
|
+
const request = context.switchToHttp().getRequest();
|
|
5539
|
+
const path2 = request.url;
|
|
5540
|
+
const flags = request.featureFlags ?? {};
|
|
5541
|
+
const featureToTrack = request.headers?.["x-feature-flag"];
|
|
5542
|
+
if (featureToTrack && flags[featureToTrack]) {
|
|
5543
|
+
this.trackFeatureUsage(featureToTrack, path2);
|
|
5544
|
+
} else {
|
|
5545
|
+
for (const [flag, enabled] of Object.entries(flags)) {
|
|
5546
|
+
if (enabled) this.trackFeatureUsage(flag, path2);
|
|
5547
|
+
}
|
|
5548
|
+
}
|
|
5549
|
+
return next.handle().pipe(
|
|
5550
|
+
rxjs.tap(() => {
|
|
5551
|
+
this.logger.log(`Completed request: ${path2}`);
|
|
5552
|
+
})
|
|
5553
|
+
);
|
|
5554
|
+
}
|
|
5555
|
+
trackFeatureUsage(flag, path2) {
|
|
5556
|
+
this.logger.log(`Feature "${flag}" used on endpoint "${path2}"`);
|
|
5557
|
+
}
|
|
5558
|
+
};
|
|
5559
|
+
__name(exports.FeatureFlagLoggingInterceptor, "FeatureFlagLoggingInterceptor");
|
|
5560
|
+
exports.FeatureFlagLoggingInterceptor = __decorateClass([
|
|
5561
|
+
common.Injectable()
|
|
5562
|
+
], exports.FeatureFlagLoggingInterceptor);
|
|
5563
|
+
exports.ErrorHandlingInterceptor = class ErrorHandlingInterceptor {
|
|
5564
|
+
logger = new common.Logger(exports.ErrorHandlingInterceptor.name);
|
|
5565
|
+
intercept(context, next) {
|
|
5566
|
+
const req = context.switchToHttp().getRequest();
|
|
5567
|
+
const path2 = req.url;
|
|
5568
|
+
const flags = req.featureFlags ?? {};
|
|
5569
|
+
const featureToCheck = req.headers?.["x-feature-flag"] ?? "GLOBAL_FEATURE";
|
|
5570
|
+
if (!flags[featureToCheck]) {
|
|
5571
|
+
this.logger.warn(`Request to ${path2} blocked: ${featureToCheck} is disabled`);
|
|
5572
|
+
throw new errors.BaseError("RETRY_FAILED", types.HTTP_STATUS.FORBIDDEN);
|
|
5573
|
+
}
|
|
5574
|
+
this.logger.debug(`Request to ${path2} allowed: ${featureToCheck} is enabled`);
|
|
5575
|
+
return next.handle().pipe(
|
|
5576
|
+
operators.catchError((error) => {
|
|
5577
|
+
this.logger.error(`Error processing request to ${path2}`, error);
|
|
5578
|
+
throw new errors.BaseError("INTERNAL_SERVER_ERROR", types.HTTP_STATUS.INTERNAL_SERVER_ERROR, error);
|
|
5579
|
+
})
|
|
5580
|
+
);
|
|
5581
|
+
}
|
|
5582
|
+
};
|
|
5583
|
+
__name(exports.ErrorHandlingInterceptor, "ErrorHandlingInterceptor");
|
|
5584
|
+
exports.ErrorHandlingInterceptor = __decorateClass([
|
|
5585
|
+
common.Injectable()
|
|
5586
|
+
], exports.ErrorHandlingInterceptor);
|
|
3852
5587
|
var FeatureFlagContext = React.createContext(
|
|
3853
5588
|
null
|
|
3854
5589
|
);
|
|
@@ -4334,19 +6069,330 @@ function useFeatureFlagHelpers() {
|
|
|
4334
6069
|
);
|
|
4335
6070
|
}
|
|
4336
6071
|
__name(useFeatureFlagHelpers, "useFeatureFlagHelpers");
|
|
6072
|
+
var MIN_RETRY_ATTEMPTS_PRODUCTION = 3;
|
|
6073
|
+
function getConfigForEnvironment(env) {
|
|
6074
|
+
switch (env) {
|
|
6075
|
+
case "production":
|
|
6076
|
+
return config.PRODUCTION_CONFIG;
|
|
6077
|
+
case "staging":
|
|
6078
|
+
return config.STAGING_CONFIG;
|
|
6079
|
+
case "development":
|
|
6080
|
+
case "test":
|
|
6081
|
+
default:
|
|
6082
|
+
return config.DEVELOPMENT_CONFIG;
|
|
6083
|
+
}
|
|
6084
|
+
}
|
|
6085
|
+
__name(getConfigForEnvironment, "getConfigForEnvironment");
|
|
6086
|
+
function validateBaseURL(mergedConfig, errors) {
|
|
6087
|
+
if (!mergedConfig.baseURL) {
|
|
6088
|
+
errors.push("baseURL is required in API configuration (apiConfig parameter)");
|
|
6089
|
+
}
|
|
6090
|
+
}
|
|
6091
|
+
__name(validateBaseURL, "validateBaseURL");
|
|
6092
|
+
function validateProductionEncryption(mergedConfig, errors, warnings) {
|
|
6093
|
+
if (mergedConfig.encryption?.enabled) {
|
|
6094
|
+
if (!mergedConfig.encryption?.key) {
|
|
6095
|
+
errors.push(
|
|
6096
|
+
'encryption.key is REQUIRED when encryption is enabled in production. Pass it in apiConfig: { encryption: { key: { id: "prod-key-v1", key: process.env.ENCRYPTION_KEY!, algorithm: "AES-GCM", format: "raw" } } }'
|
|
6097
|
+
);
|
|
6098
|
+
}
|
|
6099
|
+
} else {
|
|
6100
|
+
warnings.push(
|
|
6101
|
+
"[SECURITY WARNING] Encryption is disabled in production. This is not recommended for handling sensitive data (PII, payment info, etc.)."
|
|
6102
|
+
);
|
|
6103
|
+
}
|
|
6104
|
+
}
|
|
6105
|
+
__name(validateProductionEncryption, "validateProductionEncryption");
|
|
6106
|
+
function validateProductionPerformance(mergedConfig, warnings) {
|
|
6107
|
+
if (!mergedConfig.networkAware?.enabled) {
|
|
6108
|
+
warnings.push(
|
|
6109
|
+
"[PERFORMANCE WARNING] networkAware is disabled in production. Enable it for better user experience on varying network conditions."
|
|
6110
|
+
);
|
|
6111
|
+
}
|
|
6112
|
+
if (!mergedConfig.tracking?.telemetry) {
|
|
6113
|
+
warnings.push(
|
|
6114
|
+
"[MONITORING WARNING] telemetry is disabled in production. Enable it for production monitoring and alerting."
|
|
6115
|
+
);
|
|
6116
|
+
}
|
|
6117
|
+
if (mergedConfig.retry && mergedConfig.retry.attempts !== void 0 && mergedConfig.retry.attempts < MIN_RETRY_ATTEMPTS_PRODUCTION) {
|
|
6118
|
+
warnings.push(
|
|
6119
|
+
`[RELIABILITY WARNING] Only ${mergedConfig.retry.attempts} retry attempts configured. Consider increasing to 3-5 for better reliability in production.`
|
|
6120
|
+
);
|
|
6121
|
+
}
|
|
6122
|
+
}
|
|
6123
|
+
__name(validateProductionPerformance, "validateProductionPerformance");
|
|
6124
|
+
function validateStagingEncryption(mergedConfig, errors, warnings) {
|
|
6125
|
+
if (mergedConfig.encryption?.enabled) {
|
|
6126
|
+
if (!mergedConfig.encryption?.key) {
|
|
6127
|
+
errors.push(
|
|
6128
|
+
'encryption.key is REQUIRED in staging (aligned with production). Pass it in apiConfig: { encryption: { key: { id: "staging-key-v1", key: process.env.ENCRYPTION_KEY!, algorithm: "AES-GCM", format: "raw" } } }'
|
|
6129
|
+
);
|
|
6130
|
+
}
|
|
6131
|
+
} else {
|
|
6132
|
+
warnings.push(
|
|
6133
|
+
"[SECURITY WARNING] Encryption is disabled in staging. Staging should mirror production for accurate testing."
|
|
6134
|
+
);
|
|
6135
|
+
}
|
|
6136
|
+
if (!mergedConfig.tracking?.telemetry) {
|
|
6137
|
+
warnings.push(
|
|
6138
|
+
"[MONITORING WARNING] telemetry is disabled in staging. Enable it to test monitoring before production deployment."
|
|
6139
|
+
);
|
|
6140
|
+
}
|
|
6141
|
+
}
|
|
6142
|
+
__name(validateStagingEncryption, "validateStagingEncryption");
|
|
6143
|
+
function validateDevelopmentEncryption(mergedConfig, warnings) {
|
|
6144
|
+
if (mergedConfig.encryption?.enabled && !mergedConfig.encryption?.key) {
|
|
6145
|
+
warnings.push(
|
|
6146
|
+
"[DEV INFO] Encryption is enabled but no key provided in apiConfig. Encryption will be skipped in development (this is normal)."
|
|
6147
|
+
);
|
|
6148
|
+
}
|
|
6149
|
+
}
|
|
6150
|
+
__name(validateDevelopmentEncryption, "validateDevelopmentEncryption");
|
|
6151
|
+
function mapEnvironmentMetadata(envConfig, envDefaults) {
|
|
6152
|
+
const mapped = {};
|
|
6153
|
+
if (envConfig.apiKey) {
|
|
6154
|
+
const existingStatic = envDefaults.headers && typeof envDefaults.headers === "object" && "static" in envDefaults.headers && typeof envDefaults.headers.static === "object" ? envDefaults.headers.static : {};
|
|
6155
|
+
mapped.headers = {
|
|
6156
|
+
static: { ...existingStatic ?? {}, "X-API-Key": envConfig.apiKey }
|
|
6157
|
+
};
|
|
6158
|
+
}
|
|
6159
|
+
return mapped;
|
|
6160
|
+
}
|
|
6161
|
+
__name(mapEnvironmentMetadata, "mapEnvironmentMetadata");
|
|
6162
|
+
function applyDefaultClientSetting(client, envConfig) {
|
|
6163
|
+
const shouldSetAsDefault = envConfig.setAsDefault !== false;
|
|
6164
|
+
if (shouldSetAsDefault) {
|
|
6165
|
+
api.setDefaultApiClient(client);
|
|
6166
|
+
}
|
|
6167
|
+
}
|
|
6168
|
+
__name(applyDefaultClientSetting, "applyDefaultClientSetting");
|
|
6169
|
+
function validateEnvironmentConfig(envConfig, mergedConfig) {
|
|
6170
|
+
const errors = [];
|
|
6171
|
+
const warnings = [];
|
|
6172
|
+
validateBaseURL(mergedConfig, errors);
|
|
6173
|
+
if (envConfig.env === "production") {
|
|
6174
|
+
validateProductionEncryption(mergedConfig, errors, warnings);
|
|
6175
|
+
validateProductionPerformance(mergedConfig, warnings);
|
|
6176
|
+
} else if (envConfig.env === "staging") {
|
|
6177
|
+
validateStagingEncryption(mergedConfig, errors, warnings);
|
|
6178
|
+
} else if (envConfig.env === "development") {
|
|
6179
|
+
validateDevelopmentEncryption(mergedConfig, warnings);
|
|
6180
|
+
}
|
|
6181
|
+
if (warnings.length > 0) {
|
|
6182
|
+
globalThis.console.warn("[ApiClientService] Configuration warnings:", warnings);
|
|
6183
|
+
}
|
|
6184
|
+
if (errors.length > 0) {
|
|
6185
|
+
throw new api.ApiPackageError(
|
|
6186
|
+
"service.validation.failed",
|
|
6187
|
+
types.PACKAGE_STATUS_CODES.INVALID_CONFIGURATION,
|
|
6188
|
+
types.API_ERROR_CODES.CONFIG_VALIDATION_FAILED,
|
|
6189
|
+
{
|
|
6190
|
+
context: {
|
|
6191
|
+
operation: types.OPERATIONS.VALIDATION,
|
|
6192
|
+
// Ensure error details conform to ErrorDetail shape (include errorCode)
|
|
6193
|
+
errors: errors.map((err) => ({
|
|
6194
|
+
field: "config",
|
|
6195
|
+
message: err,
|
|
6196
|
+
errorCode: String(types.API_ERROR_CODES.CONFIG_VALIDATION_FAILED)
|
|
6197
|
+
})),
|
|
6198
|
+
i18n: {
|
|
6199
|
+
errors: errors.join("; "),
|
|
6200
|
+
warnings: warnings.join("; ")
|
|
6201
|
+
}
|
|
6202
|
+
}
|
|
6203
|
+
}
|
|
6204
|
+
);
|
|
6205
|
+
}
|
|
6206
|
+
}
|
|
6207
|
+
__name(validateEnvironmentConfig, "validateEnvironmentConfig");
|
|
6208
|
+
var ApiClientService = class {
|
|
6209
|
+
static {
|
|
6210
|
+
__name(this, "ApiClientService");
|
|
6211
|
+
}
|
|
6212
|
+
static instance = null;
|
|
6213
|
+
static isInitializing = false;
|
|
6214
|
+
static initPromise = null;
|
|
6215
|
+
/**
|
|
6216
|
+
* Initialize the API client with environment config and API options
|
|
6217
|
+
*
|
|
6218
|
+
* @param envConfig - Environment metadata (env, apiKey)
|
|
6219
|
+
* @param apiConfig - API configuration (baseURL, encryption, timeout, event handlers, etc.)
|
|
6220
|
+
* @returns Promise that resolves to the initialized client
|
|
6221
|
+
*/
|
|
6222
|
+
static async init(envConfig, apiConfig) {
|
|
6223
|
+
if (this.instance) {
|
|
6224
|
+
globalThis.console.warn(
|
|
6225
|
+
"[ApiClientService] Client already initialized. Returning existing instance."
|
|
6226
|
+
);
|
|
6227
|
+
return this.instance;
|
|
6228
|
+
}
|
|
6229
|
+
if (this.isInitializing && this.initPromise) {
|
|
6230
|
+
await this.initPromise;
|
|
6231
|
+
return this.instance;
|
|
6232
|
+
}
|
|
6233
|
+
this.isInitializing = true;
|
|
6234
|
+
this.initPromise = this.createClient(envConfig, apiConfig);
|
|
6235
|
+
try {
|
|
6236
|
+
await this.initPromise;
|
|
6237
|
+
return this.instance;
|
|
6238
|
+
} finally {
|
|
6239
|
+
this.isInitializing = false;
|
|
6240
|
+
this.initPromise = null;
|
|
6241
|
+
}
|
|
6242
|
+
}
|
|
6243
|
+
/**
|
|
6244
|
+
* Internal initialization logic
|
|
6245
|
+
* Merges environment-specific defaults with API configuration
|
|
6246
|
+
*
|
|
6247
|
+
* Merge Priority (lowest to highest):
|
|
6248
|
+
* 1. Environment defaults (PRODUCTION_CONFIG / STAGING_CONFIG / DEVELOPMENT_CONFIG)
|
|
6249
|
+
* 2. Environment metadata (envConfig - apiKey)
|
|
6250
|
+
* 3. API configuration (apiConfig - baseURL, encryption, timeout, etc.)
|
|
6251
|
+
*/
|
|
6252
|
+
static async createClient(envConfig, apiConfig) {
|
|
6253
|
+
try {
|
|
6254
|
+
const envDefaults = getConfigForEnvironment(envConfig.env);
|
|
6255
|
+
const envMetadataMapped = mapEnvironmentMetadata(envConfig, envDefaults);
|
|
6256
|
+
const mergedOptions = api.mergeConfigs(
|
|
6257
|
+
envDefaults,
|
|
6258
|
+
// Environment defaults (lowest priority)
|
|
6259
|
+
envMetadataMapped,
|
|
6260
|
+
// Environment metadata (medium priority)
|
|
6261
|
+
apiConfig ?? {}
|
|
6262
|
+
// API configuration (highest priority - includes baseURL, encryption, etc.)
|
|
6263
|
+
);
|
|
6264
|
+
validateEnvironmentConfig(envConfig, mergedOptions);
|
|
6265
|
+
this.instance = await api.createApiClient(mergedOptions);
|
|
6266
|
+
applyDefaultClientSetting(this.instance, envConfig);
|
|
6267
|
+
} catch (error) {
|
|
6268
|
+
throw new api.ApiPackageError(
|
|
6269
|
+
"service.initialization.failed",
|
|
6270
|
+
types.PACKAGE_STATUS_CODES.INITIALIZATION_FAILED,
|
|
6271
|
+
types.API_ERROR_CODES.CLIENT_INITIALIZATION_FAILED,
|
|
6272
|
+
{
|
|
6273
|
+
cause: error instanceof Error ? error : void 0,
|
|
6274
|
+
context: {
|
|
6275
|
+
operation: types.OPERATIONS.INITIALIZATION,
|
|
6276
|
+
originalError: error instanceof Error ? error.message : String(error),
|
|
6277
|
+
i18n: {
|
|
6278
|
+
error: error instanceof Error ? error.message : String(error)
|
|
6279
|
+
}
|
|
6280
|
+
}
|
|
6281
|
+
}
|
|
6282
|
+
);
|
|
6283
|
+
}
|
|
6284
|
+
}
|
|
6285
|
+
/**
|
|
6286
|
+
* Get the initialized client instance
|
|
6287
|
+
*
|
|
6288
|
+
* @throws {ApiPackageError} If client not initialized
|
|
6289
|
+
*/
|
|
6290
|
+
static getClient() {
|
|
6291
|
+
if (!this.instance) {
|
|
6292
|
+
throw new api.ApiPackageError(
|
|
6293
|
+
"service.not_initialized",
|
|
6294
|
+
types.PACKAGE_STATUS_CODES.INITIALIZATION_FAILED,
|
|
6295
|
+
types.API_ERROR_CODES.CLIENT_INITIALIZATION_FAILED,
|
|
6296
|
+
{
|
|
6297
|
+
context: {
|
|
6298
|
+
operation: types.OPERATIONS.INITIALIZATION,
|
|
6299
|
+
i18n: {
|
|
6300
|
+
hint: "Call ApiClientService.init(envConfig, apiConfig) before accessing the client"
|
|
6301
|
+
}
|
|
6302
|
+
}
|
|
6303
|
+
}
|
|
6304
|
+
);
|
|
6305
|
+
}
|
|
6306
|
+
return this.instance;
|
|
6307
|
+
}
|
|
6308
|
+
/**
|
|
6309
|
+
* Check if client is initialized
|
|
6310
|
+
*/
|
|
6311
|
+
static isInitialized() {
|
|
6312
|
+
return this.instance !== null;
|
|
6313
|
+
}
|
|
6314
|
+
/**
|
|
6315
|
+
* Reinitialize with new config and options
|
|
6316
|
+
*/
|
|
6317
|
+
static async reinitialize(envConfig, apiConfig) {
|
|
6318
|
+
this.dispose();
|
|
6319
|
+
return this.init(envConfig, apiConfig);
|
|
6320
|
+
}
|
|
6321
|
+
/**
|
|
6322
|
+
* Dispose of the client instance
|
|
6323
|
+
*/
|
|
6324
|
+
static dispose() {
|
|
6325
|
+
if (this.instance && "dispose" in this.instance) {
|
|
6326
|
+
this.instance.dispose?.();
|
|
6327
|
+
}
|
|
6328
|
+
this.instance = null;
|
|
6329
|
+
this.isInitializing = false;
|
|
6330
|
+
this.initPromise = null;
|
|
6331
|
+
}
|
|
6332
|
+
};
|
|
6333
|
+
var getApiClient = /* @__PURE__ */ __name(() => ApiClientService.getClient(), "getApiClient");
|
|
6334
|
+
var initApiClient = /* @__PURE__ */ __name((envConfig, apiConfig) => ApiClientService.init(envConfig, apiConfig), "initApiClient");
|
|
6335
|
+
function ApiProvider({
|
|
6336
|
+
children,
|
|
6337
|
+
envConfig,
|
|
6338
|
+
apiConfig,
|
|
6339
|
+
loadingComponent,
|
|
6340
|
+
errorComponent,
|
|
6341
|
+
onInitialized,
|
|
6342
|
+
onError
|
|
6343
|
+
}) {
|
|
6344
|
+
const [isReady, setIsReady] = React.useState(false);
|
|
6345
|
+
const [error, setError] = React.useState(null);
|
|
6346
|
+
React.useEffect(() => {
|
|
6347
|
+
ApiClientService.init(envConfig, apiConfig).then(() => {
|
|
6348
|
+
setIsReady(true);
|
|
6349
|
+
onInitialized?.();
|
|
6350
|
+
}).catch((err) => {
|
|
6351
|
+
const error2 = err instanceof Error ? err : new Error(String(err));
|
|
6352
|
+
setError(error2);
|
|
6353
|
+
onError?.(error2);
|
|
6354
|
+
globalThis.console.error("[ApiProvider] Failed to initialize API client:", error2);
|
|
6355
|
+
});
|
|
6356
|
+
return () => {
|
|
6357
|
+
};
|
|
6358
|
+
}, []);
|
|
6359
|
+
if (error) {
|
|
6360
|
+
if (errorComponent) {
|
|
6361
|
+
return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: errorComponent(error) });
|
|
6362
|
+
}
|
|
6363
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { padding: "20px", color: "red" }, children: [
|
|
6364
|
+
/* @__PURE__ */ jsxRuntime.jsx("h2", { children: "API Client Initialization Failed" }),
|
|
6365
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { children: error.message })
|
|
6366
|
+
] });
|
|
6367
|
+
}
|
|
6368
|
+
if (!isReady) {
|
|
6369
|
+
if (loadingComponent) {
|
|
6370
|
+
return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: loadingComponent });
|
|
6371
|
+
}
|
|
6372
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { style: { padding: "20px" }, children: /* @__PURE__ */ jsxRuntime.jsx("p", { children: "Initializing API client..." }) });
|
|
6373
|
+
}
|
|
6374
|
+
return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children });
|
|
6375
|
+
}
|
|
6376
|
+
__name(ApiProvider, "ApiProvider");
|
|
4337
6377
|
|
|
6378
|
+
exports.ApiClientService = ApiClientService;
|
|
4338
6379
|
exports.ApiFeatureFlagProvider = ApiFeatureFlagProvider;
|
|
6380
|
+
exports.ApiProvider = ApiProvider;
|
|
4339
6381
|
exports.CacheManager = CacheManager;
|
|
4340
6382
|
exports.ConditionUtils = ConditionUtils;
|
|
4341
6383
|
exports.ContextUtils = ContextUtils;
|
|
4342
6384
|
exports.DEFAULT_FEATURE_FLAG_CONFIG = DEFAULT_FEATURE_FLAG_CONFIG;
|
|
6385
|
+
exports.DatabaseConnectionManager = DatabaseConnectionManager;
|
|
4343
6386
|
exports.DatabaseFeatureFlagProvider = DatabaseFeatureFlagProvider;
|
|
4344
|
-
exports.
|
|
6387
|
+
exports.FeatureDisabled = FeatureDisabled;
|
|
6388
|
+
exports.FeatureEnabled = FeatureEnabled;
|
|
4345
6389
|
exports.FeatureFlagAppProvider = FeatureFlagAppProvider;
|
|
6390
|
+
exports.FeatureFlagConfigFactory = FeatureFlagConfigFactory;
|
|
6391
|
+
exports.FeatureFlagConfigValidator = FeatureFlagConfigValidator;
|
|
4346
6392
|
exports.FeatureFlagContext = FeatureFlagContext;
|
|
4347
6393
|
exports.FeatureFlagContextBuilder = FeatureFlagContextBuilder;
|
|
6394
|
+
exports.FeatureFlagDatabaseRepository = FeatureFlagDatabaseRepository;
|
|
4348
6395
|
exports.FeatureFlagEngine = FeatureFlagEngine;
|
|
4349
|
-
exports.FeatureFlagGuard = FeatureFlagGuard;
|
|
4350
6396
|
exports.FeatureFlagProvider = FeatureFlagProvider;
|
|
4351
6397
|
exports.FeatureFlagProviderFactory = FeatureFlagProviderFactory;
|
|
4352
6398
|
exports.FeatureFlagSystem = FeatureFlagSystem;
|
|
@@ -4364,11 +6410,17 @@ exports.evaluateConditionOperator = evaluateConditionOperator;
|
|
|
4364
6410
|
exports.evaluateEqualityOperator = evaluateEqualityOperator;
|
|
4365
6411
|
exports.evaluateNumericOperator = evaluateNumericOperator;
|
|
4366
6412
|
exports.evaluateStringOperator = evaluateStringOperator;
|
|
6413
|
+
exports.getApiClient = getApiClient;
|
|
6414
|
+
exports.getValidProviders = getValidProviders;
|
|
4367
6415
|
exports.hashString = hashString;
|
|
6416
|
+
exports.initApiClient = initApiClient;
|
|
4368
6417
|
exports.isArrayOperator = isArrayOperator;
|
|
6418
|
+
exports.isDefined = isDefined;
|
|
4369
6419
|
exports.isEqualityOperator = isEqualityOperator;
|
|
4370
6420
|
exports.isInRollout = isInRollout;
|
|
6421
|
+
exports.isNumber = isNumber;
|
|
4371
6422
|
exports.isNumericOperator = isNumericOperator;
|
|
6423
|
+
exports.isString = isString;
|
|
4372
6424
|
exports.isStringOperator = isStringOperator;
|
|
4373
6425
|
exports.isTruthy = isTruthy;
|
|
4374
6426
|
exports.toBoolean = toBoolean;
|