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