@plyaz/core 1.0.1 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +51 -9
- package/dist/base/cache/index.d.ts.map +1 -0
- package/dist/base/cache/strategies/memory.d.ts.map +1 -0
- package/dist/base/cache/strategies/redis.d.ts.map +1 -0
- package/dist/base/index.d.ts +5 -0
- package/dist/base/index.d.ts.map +1 -0
- package/dist/domain/featureFlags/provider.d.ts +9 -1
- package/dist/domain/featureFlags/provider.d.ts.map +1 -1
- package/dist/domain/featureFlags/providers/factory.d.ts +30 -5
- package/dist/domain/featureFlags/providers/factory.d.ts.map +1 -1
- package/dist/domain/featureFlags/providers/file.d.ts +97 -1
- package/dist/domain/featureFlags/providers/file.d.ts.map +1 -1
- package/dist/domain/featureFlags/providers/memory.d.ts +12 -5
- package/dist/domain/featureFlags/providers/memory.d.ts.map +1 -1
- package/dist/engine/featureFlags/engine.d.ts +7 -0
- package/dist/engine/featureFlags/engine.d.ts.map +1 -1
- package/dist/index.cjs +501 -62
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +476 -59
- package/dist/index.mjs.map +1 -1
- package/package.json +6 -5
- package/dist/cache/index.d.ts.map +0 -1
- package/dist/cache/strategies/memory.d.ts.map +0 -1
- package/dist/cache/strategies/redis.d.ts.map +0 -1
- /package/dist/{cache → base/cache}/index.d.ts +0 -0
- /package/dist/{cache → base/cache}/strategies/memory.d.ts +0 -0
- /package/dist/{cache → base/cache}/strategies/redis.d.ts +0 -0
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import { FEATURES } from '@plyaz/config';
|
|
1
|
+
import { CACHE_MAX_SIZE_DEFAULT, CACHE_CLEANUP_INTERVAL_DEFAULT, FILE_CHECK_INTERVAL_DEFAULT, FEATURE_FLAG_FILE_PATHS, FEATURE_FLAG_CACHE_TTL_DEFAULT, FEATURES } from '@plyaz/config';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { promisify } from 'util';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
import * as yaml from 'yaml';
|
|
2
7
|
import { Post, Param, Body, Put, Delete, Get, Query, Controller, Injectable, Global, Module, HttpException, HttpStatus, Logger } from '@nestjs/common';
|
|
3
8
|
import React, { createContext, useState, useRef, useCallback, useEffect, useContext, useMemo } from 'react';
|
|
4
9
|
import { jsx } from 'react/jsx-runtime';
|
|
@@ -180,9 +185,9 @@ var ValueUtils = {
|
|
|
180
185
|
* @param defaultValue - Default if path doesn't exist
|
|
181
186
|
* @returns Property value or default
|
|
182
187
|
*/
|
|
183
|
-
getNestedProperty: /* @__PURE__ */ __name((obj,
|
|
188
|
+
getNestedProperty: /* @__PURE__ */ __name((obj, path2, defaultValue) => {
|
|
184
189
|
if (!obj || typeof obj !== "object") return defaultValue;
|
|
185
|
-
const keys =
|
|
190
|
+
const keys = path2.split(".");
|
|
186
191
|
let current = obj;
|
|
187
192
|
for (const key of keys) {
|
|
188
193
|
if (current == null || typeof current !== "object") return defaultValue;
|
|
@@ -750,6 +755,16 @@ var FeatureFlagEngine = class {
|
|
|
750
755
|
this.overrides.delete(key);
|
|
751
756
|
this.log("Override removed:", key);
|
|
752
757
|
}
|
|
758
|
+
/**
|
|
759
|
+
* Updates the default values for feature flags.
|
|
760
|
+
* This is useful when the FEATURES constant is updated at runtime.
|
|
761
|
+
*
|
|
762
|
+
* @param newDefaults - New default values
|
|
763
|
+
*/
|
|
764
|
+
updateDefaults(newDefaults) {
|
|
765
|
+
this.defaults = newDefaults;
|
|
766
|
+
this.log("Updated default feature values");
|
|
767
|
+
}
|
|
753
768
|
/**
|
|
754
769
|
* Clears all manual overrides.
|
|
755
770
|
*/
|
|
@@ -927,7 +942,7 @@ var FeatureFlagEngine = class {
|
|
|
927
942
|
value: rule.value,
|
|
928
943
|
isEnabled: isTruthy(rule.value),
|
|
929
944
|
reason: "rule_match",
|
|
930
|
-
|
|
945
|
+
matchedRuleId: rule.id,
|
|
931
946
|
evaluatedAt: evaluatedAt ?? /* @__PURE__ */ new Date()
|
|
932
947
|
};
|
|
933
948
|
}
|
|
@@ -970,8 +985,6 @@ var FeatureFlagEngine = class {
|
|
|
970
985
|
}
|
|
971
986
|
}
|
|
972
987
|
};
|
|
973
|
-
|
|
974
|
-
// src/cache/strategies/memory.ts
|
|
975
988
|
var MemoryCacheStrategy = class {
|
|
976
989
|
static {
|
|
977
990
|
__name(this, "MemoryCacheStrategy");
|
|
@@ -994,11 +1007,9 @@ var MemoryCacheStrategy = class {
|
|
|
994
1007
|
* @param config - Memory cache configuration
|
|
995
1008
|
*/
|
|
996
1009
|
constructor(config = {}) {
|
|
997
|
-
const DEFAULT_MAX_ENTRIES = 1e3;
|
|
998
|
-
const DEFAULT_CLEANUP_INTERVAL = 6e4;
|
|
999
1010
|
const defaultConfig = {
|
|
1000
|
-
maxEntries:
|
|
1001
|
-
cleanupInterval:
|
|
1011
|
+
maxEntries: CACHE_MAX_SIZE_DEFAULT,
|
|
1012
|
+
cleanupInterval: CACHE_CLEANUP_INTERVAL_DEFAULT
|
|
1002
1013
|
};
|
|
1003
1014
|
this.maxSize = config.maxSize ?? config.maxEntries ?? defaultConfig.maxEntries;
|
|
1004
1015
|
this.cleanupInterval = config.cleanupInterval ?? defaultConfig.cleanupInterval;
|
|
@@ -1151,7 +1162,7 @@ var MemoryCacheStrategy = class {
|
|
|
1151
1162
|
}
|
|
1152
1163
|
};
|
|
1153
1164
|
|
|
1154
|
-
// src/cache/strategies/redis.ts
|
|
1165
|
+
// src/base/cache/strategies/redis.ts
|
|
1155
1166
|
var RedisCacheStrategy = class {
|
|
1156
1167
|
/**
|
|
1157
1168
|
* Creates a new Redis cache strategy.
|
|
@@ -1313,8 +1324,8 @@ var RedisCacheStrategy = class {
|
|
|
1313
1324
|
commandTimeout: this.config.commandTimeout ?? defaultOptions.commandTimeout,
|
|
1314
1325
|
enableOfflineQueue: defaultOptions.enableOfflineQueue
|
|
1315
1326
|
});
|
|
1316
|
-
await new Promise((
|
|
1317
|
-
client.on("ready",
|
|
1327
|
+
await new Promise((resolve2, reject) => {
|
|
1328
|
+
client.on("ready", resolve2);
|
|
1318
1329
|
client.on("error", reject);
|
|
1319
1330
|
});
|
|
1320
1331
|
return client;
|
|
@@ -1331,7 +1342,7 @@ var RedisCacheStrategy = class {
|
|
|
1331
1342
|
}
|
|
1332
1343
|
};
|
|
1333
1344
|
|
|
1334
|
-
// src/cache/index.ts
|
|
1345
|
+
// src/base/cache/index.ts
|
|
1335
1346
|
var CacheManager = class {
|
|
1336
1347
|
/**
|
|
1337
1348
|
* Creates a new cache manager with the specified configuration.
|
|
@@ -1860,31 +1871,53 @@ var MemoryFeatureFlagProvider = class extends FeatureFlagProvider {
|
|
|
1860
1871
|
}
|
|
1861
1872
|
}
|
|
1862
1873
|
/**
|
|
1863
|
-
* Updates a flag
|
|
1874
|
+
* Updates a flag in memory.
|
|
1864
1875
|
*
|
|
1865
|
-
* @param
|
|
1866
|
-
* @param value - The new value
|
|
1867
|
-
* @param updateProps - Optional properties to update
|
|
1876
|
+
* @param flagOrKey - Either a complete flag object or a flag key
|
|
1877
|
+
* @param value - The new value (only used when first param is a key)
|
|
1878
|
+
* @param updateProps - Optional properties to update (only used when first param is a key)
|
|
1868
1879
|
*/
|
|
1869
|
-
updateFlag(
|
|
1870
|
-
|
|
1880
|
+
async updateFlag(flagOrKey, value, updateProps) {
|
|
1881
|
+
let key;
|
|
1882
|
+
let updatedFlag;
|
|
1883
|
+
if (typeof flagOrKey === "string") {
|
|
1884
|
+
key = flagOrKey;
|
|
1885
|
+
const existingFlag = this.flags.find((f) => f.key === key);
|
|
1886
|
+
if (!existingFlag) {
|
|
1887
|
+
this.log(`Flag with key ${key} not found in memory`);
|
|
1888
|
+
return;
|
|
1889
|
+
}
|
|
1890
|
+
if (value === void 0) {
|
|
1891
|
+
this.log(`Value is required when updating flag by key`);
|
|
1892
|
+
return;
|
|
1893
|
+
}
|
|
1894
|
+
updatedFlag = {
|
|
1895
|
+
...existingFlag,
|
|
1896
|
+
value,
|
|
1897
|
+
type: this.inferFlagType(value),
|
|
1898
|
+
updatedAt: /* @__PURE__ */ new Date(),
|
|
1899
|
+
updatedBy: "memory-runtime",
|
|
1900
|
+
...updateProps
|
|
1901
|
+
};
|
|
1902
|
+
} else {
|
|
1903
|
+
const flag = flagOrKey;
|
|
1904
|
+
key = flag.key;
|
|
1905
|
+
updatedFlag = {
|
|
1906
|
+
...flag,
|
|
1907
|
+
updatedAt: /* @__PURE__ */ new Date(),
|
|
1908
|
+
updatedBy: flag.updatedBy || "memory-runtime"
|
|
1909
|
+
};
|
|
1910
|
+
}
|
|
1911
|
+
const flagIndex = this.flags.findIndex((f) => f.key === key);
|
|
1871
1912
|
if (flagIndex === -1) {
|
|
1872
1913
|
this.log(`Flag with key ${key} not found in memory`);
|
|
1873
1914
|
return;
|
|
1874
1915
|
}
|
|
1875
|
-
const updatedFlag = {
|
|
1876
|
-
...this.flags[flagIndex],
|
|
1877
|
-
value,
|
|
1878
|
-
type: this.inferFlagType(value),
|
|
1879
|
-
updatedAt: /* @__PURE__ */ new Date(),
|
|
1880
|
-
updatedBy: "memory-runtime",
|
|
1881
|
-
...updateProps
|
|
1882
|
-
};
|
|
1883
1916
|
this.flags[flagIndex] = updatedFlag;
|
|
1884
1917
|
this.engine.setFlags(this.flags);
|
|
1885
1918
|
void this.cacheManager.clear();
|
|
1886
1919
|
this.notifySubscribers();
|
|
1887
|
-
this.log(`Updated flag: ${key}
|
|
1920
|
+
this.log(`Updated flag: ${key}`);
|
|
1888
1921
|
}
|
|
1889
1922
|
/**
|
|
1890
1923
|
* Adds a new flag to memory at runtime.
|
|
@@ -1994,6 +2027,18 @@ var MemoryFeatureFlagProvider = class extends FeatureFlagProvider {
|
|
|
1994
2027
|
getCurrentRules() {
|
|
1995
2028
|
return [...this.rules];
|
|
1996
2029
|
}
|
|
2030
|
+
/**
|
|
2031
|
+
* Updates the features object and syncs all flags.
|
|
2032
|
+
* This allows updating the FEATURES constant at runtime.
|
|
2033
|
+
*
|
|
2034
|
+
* @param newFeatures - New features object to sync with
|
|
2035
|
+
*/
|
|
2036
|
+
async syncFeatures(newFeatures) {
|
|
2037
|
+
this.log("Syncing with new FEATURES values");
|
|
2038
|
+
this.features = newFeatures;
|
|
2039
|
+
await this.refresh();
|
|
2040
|
+
this.log(`Synced ${Object.keys(newFeatures).length} features`);
|
|
2041
|
+
}
|
|
1997
2042
|
/**
|
|
1998
2043
|
* Resets the memory provider to its initial state.
|
|
1999
2044
|
*/
|
|
@@ -2027,13 +2072,18 @@ var MemoryFeatureFlagProvider = class extends FeatureFlagProvider {
|
|
|
2027
2072
|
}
|
|
2028
2073
|
}
|
|
2029
2074
|
};
|
|
2030
|
-
|
|
2031
|
-
|
|
2075
|
+
var readFile2 = promisify(fs.readFile);
|
|
2076
|
+
var writeFile2 = promisify(fs.writeFile);
|
|
2077
|
+
var access2 = promisify(fs.access);
|
|
2078
|
+
var mkdir2 = promisify(fs.mkdir);
|
|
2032
2079
|
var FileFeatureFlagProvider = class extends FeatureFlagProvider {
|
|
2033
2080
|
static {
|
|
2034
2081
|
__name(this, "FileFeatureFlagProvider");
|
|
2035
2082
|
}
|
|
2036
2083
|
fileWatcher;
|
|
2084
|
+
lastFileContent;
|
|
2085
|
+
fileCheckInterval;
|
|
2086
|
+
rules = [];
|
|
2037
2087
|
/**
|
|
2038
2088
|
* Creates a new file feature flag provider.
|
|
2039
2089
|
*
|
|
@@ -2043,8 +2093,15 @@ var FileFeatureFlagProvider = class extends FeatureFlagProvider {
|
|
|
2043
2093
|
constructor(config, features) {
|
|
2044
2094
|
super(config, features);
|
|
2045
2095
|
this.validateConfig();
|
|
2046
|
-
|
|
2047
|
-
|
|
2096
|
+
}
|
|
2097
|
+
/**
|
|
2098
|
+
* Initializes the provider and sets up file watching if enabled.
|
|
2099
|
+
*/
|
|
2100
|
+
async initialize() {
|
|
2101
|
+
await super.initialize();
|
|
2102
|
+
if (this.config.fileConfig?.shouldWatchForChanges) {
|
|
2103
|
+
this.setupFileWatcher();
|
|
2104
|
+
}
|
|
2048
2105
|
}
|
|
2049
2106
|
/**
|
|
2050
2107
|
* Fetches flags and rules from the configuration file.
|
|
@@ -2053,9 +2110,89 @@ var FileFeatureFlagProvider = class extends FeatureFlagProvider {
|
|
|
2053
2110
|
* @returns Promise resolving to flags and rules from file
|
|
2054
2111
|
*/
|
|
2055
2112
|
async fetchData() {
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2113
|
+
const { filePath, format } = this.config.fileConfig;
|
|
2114
|
+
const resolvedPath = this.resolveFilePath(filePath);
|
|
2115
|
+
try {
|
|
2116
|
+
await access2(resolvedPath, fs.constants.R_OK);
|
|
2117
|
+
const content = await readFile2(resolvedPath, "utf-8");
|
|
2118
|
+
this.lastFileContent = content;
|
|
2119
|
+
const data = await this.parseFileContent(content, format);
|
|
2120
|
+
this.validateFileData(data);
|
|
2121
|
+
this.rules = data.rules || [];
|
|
2122
|
+
return {
|
|
2123
|
+
flags: data.flags || [],
|
|
2124
|
+
rules: data.rules || []
|
|
2125
|
+
};
|
|
2126
|
+
} catch (error) {
|
|
2127
|
+
return this.handleFetchDataError(error, resolvedPath, format);
|
|
2128
|
+
}
|
|
2129
|
+
}
|
|
2130
|
+
/**
|
|
2131
|
+
* Parses file content based on format.
|
|
2132
|
+
*
|
|
2133
|
+
* @private
|
|
2134
|
+
*/
|
|
2135
|
+
async parseFileContent(content, format) {
|
|
2136
|
+
if (format === "json") {
|
|
2137
|
+
return this.parseJSON(content);
|
|
2138
|
+
} else if (format === "yaml") {
|
|
2139
|
+
return await this.parseYAML(content);
|
|
2140
|
+
}
|
|
2141
|
+
throw new Error(`Unsupported file format: ${format}`);
|
|
2142
|
+
}
|
|
2143
|
+
/**
|
|
2144
|
+
* Handles errors for fetchData, including file creation and fallback.
|
|
2145
|
+
*
|
|
2146
|
+
* @private
|
|
2147
|
+
*/
|
|
2148
|
+
async handleFetchDataError(error, resolvedPath, format) {
|
|
2149
|
+
const isFileNotFound = this.isFileNotFoundError(error);
|
|
2150
|
+
if (isFileNotFound && this.config.shouldFallbackToDefaults) {
|
|
2151
|
+
return await this.handleFileNotFound(resolvedPath, format);
|
|
2152
|
+
}
|
|
2153
|
+
this.log(`Error reading file ${resolvedPath}:`, error);
|
|
2154
|
+
if (this.config.shouldFallbackToDefaults) {
|
|
2155
|
+
return this.handleFallbackToDefaults();
|
|
2156
|
+
}
|
|
2157
|
+
throw error;
|
|
2158
|
+
}
|
|
2159
|
+
/**
|
|
2160
|
+
* Type guard for NodeJS.ErrnoException.
|
|
2161
|
+
*/
|
|
2162
|
+
isFileNotFoundError(error) {
|
|
2163
|
+
return error instanceof Error && typeof error.code === "string" && error.code === "ENOENT";
|
|
2164
|
+
}
|
|
2165
|
+
/**
|
|
2166
|
+
* Handles the case when the file is not found and fallback is enabled.
|
|
2167
|
+
*/
|
|
2168
|
+
async handleFileNotFound(resolvedPath, format) {
|
|
2169
|
+
this.log(`File not found at ${resolvedPath}, creating with default values`);
|
|
2170
|
+
try {
|
|
2171
|
+
await this.createDefaultFile(resolvedPath, format);
|
|
2172
|
+
const content = await readFile2(resolvedPath, "utf-8");
|
|
2173
|
+
this.lastFileContent = content;
|
|
2174
|
+
const data = await this.parseFileContent(content, format);
|
|
2175
|
+
return {
|
|
2176
|
+
flags: data.flags || [],
|
|
2177
|
+
rules: data.rules || []
|
|
2178
|
+
};
|
|
2179
|
+
} catch (createError) {
|
|
2180
|
+
this.log("Error creating default file:", createError);
|
|
2181
|
+
return {
|
|
2182
|
+
flags: this.createDefaultFlags(),
|
|
2183
|
+
rules: []
|
|
2184
|
+
};
|
|
2185
|
+
}
|
|
2186
|
+
}
|
|
2187
|
+
/**
|
|
2188
|
+
* Handles fallback to default flags and rules.
|
|
2189
|
+
*/
|
|
2190
|
+
handleFallbackToDefaults() {
|
|
2191
|
+
this.log("Falling back to default values");
|
|
2192
|
+
return {
|
|
2193
|
+
flags: this.createDefaultFlags(),
|
|
2194
|
+
rules: []
|
|
2195
|
+
};
|
|
2059
2196
|
}
|
|
2060
2197
|
/**
|
|
2061
2198
|
* Validates the file provider configuration.
|
|
@@ -2078,6 +2215,165 @@ var FileFeatureFlagProvider = class extends FeatureFlagProvider {
|
|
|
2078
2215
|
throw new Error('File format must be either "json" or "yaml"');
|
|
2079
2216
|
}
|
|
2080
2217
|
}
|
|
2218
|
+
/**
|
|
2219
|
+
* Resolves the file path, supporting relative and absolute paths.
|
|
2220
|
+
*
|
|
2221
|
+
* @private
|
|
2222
|
+
* @param filePath - The file path from configuration
|
|
2223
|
+
* @returns Resolved absolute file path
|
|
2224
|
+
*/
|
|
2225
|
+
resolveFilePath(filePath) {
|
|
2226
|
+
let pathToResolve = filePath;
|
|
2227
|
+
if (!pathToResolve) {
|
|
2228
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
2229
|
+
const __dirname = path.dirname(__filename);
|
|
2230
|
+
pathToResolve = path.join(__dirname, "../../../config/feature-provider.json");
|
|
2231
|
+
}
|
|
2232
|
+
if (path.isAbsolute(pathToResolve)) {
|
|
2233
|
+
return pathToResolve;
|
|
2234
|
+
}
|
|
2235
|
+
return path.resolve(process.cwd(), pathToResolve);
|
|
2236
|
+
}
|
|
2237
|
+
/**
|
|
2238
|
+
* Parses JSON content.
|
|
2239
|
+
*
|
|
2240
|
+
* @private
|
|
2241
|
+
* @param content - File content
|
|
2242
|
+
* @returns Parsed data
|
|
2243
|
+
*/
|
|
2244
|
+
parseJSON(content) {
|
|
2245
|
+
try {
|
|
2246
|
+
return JSON.parse(content);
|
|
2247
|
+
} catch (error) {
|
|
2248
|
+
throw new Error(
|
|
2249
|
+
`Invalid JSON format: ${error instanceof Error ? error.message : String(error)}`
|
|
2250
|
+
);
|
|
2251
|
+
}
|
|
2252
|
+
}
|
|
2253
|
+
/**
|
|
2254
|
+
* Parses YAML content.
|
|
2255
|
+
*
|
|
2256
|
+
* @private
|
|
2257
|
+
* @param content - File content
|
|
2258
|
+
* @returns Parsed data
|
|
2259
|
+
*/
|
|
2260
|
+
async parseYAML(content) {
|
|
2261
|
+
try {
|
|
2262
|
+
const data = yaml.parse(content);
|
|
2263
|
+
return data;
|
|
2264
|
+
} catch (error) {
|
|
2265
|
+
throw new Error(
|
|
2266
|
+
`Invalid YAML format: ${error instanceof Error ? error.message : String(error)}`
|
|
2267
|
+
);
|
|
2268
|
+
}
|
|
2269
|
+
}
|
|
2270
|
+
/**
|
|
2271
|
+
* Validates the structure of file data.
|
|
2272
|
+
*
|
|
2273
|
+
* @private
|
|
2274
|
+
* @param data - Parsed file data
|
|
2275
|
+
*/
|
|
2276
|
+
validateFileData(data) {
|
|
2277
|
+
if (!data || typeof data !== "object") {
|
|
2278
|
+
throw new Error("File must contain a valid object");
|
|
2279
|
+
}
|
|
2280
|
+
const fileData = data;
|
|
2281
|
+
this.checkFlagsArray(fileData.flags);
|
|
2282
|
+
this.checkRulesArray(fileData.rules);
|
|
2283
|
+
}
|
|
2284
|
+
checkFlagsArray(flags) {
|
|
2285
|
+
if (flags && !Array.isArray(flags)) {
|
|
2286
|
+
throw new Error('"flags" must be an array');
|
|
2287
|
+
}
|
|
2288
|
+
if (Array.isArray(flags)) {
|
|
2289
|
+
this.validateFlags(flags);
|
|
2290
|
+
}
|
|
2291
|
+
}
|
|
2292
|
+
checkRulesArray(rules) {
|
|
2293
|
+
if (rules && !Array.isArray(rules)) {
|
|
2294
|
+
throw new Error('"rules" must be an array');
|
|
2295
|
+
}
|
|
2296
|
+
if (Array.isArray(rules)) {
|
|
2297
|
+
this.validateRules(rules);
|
|
2298
|
+
}
|
|
2299
|
+
}
|
|
2300
|
+
validateFlags(flags) {
|
|
2301
|
+
flags.forEach((flag, index) => {
|
|
2302
|
+
if (!flag || typeof flag !== "object") {
|
|
2303
|
+
throw new Error(`Flag at index ${index} must be an object`);
|
|
2304
|
+
}
|
|
2305
|
+
const flagObj = flag;
|
|
2306
|
+
if (!flagObj.key || typeof flagObj.key !== "string") {
|
|
2307
|
+
throw new Error(`Flag at index ${index} must have a "key" property`);
|
|
2308
|
+
}
|
|
2309
|
+
if (flagObj.value === void 0) {
|
|
2310
|
+
throw new Error(`Flag "${flagObj.key}" must have a "value" property`);
|
|
2311
|
+
}
|
|
2312
|
+
});
|
|
2313
|
+
}
|
|
2314
|
+
validateRules(rules) {
|
|
2315
|
+
rules.forEach((rule, index) => {
|
|
2316
|
+
if (!rule || typeof rule !== "object") {
|
|
2317
|
+
throw new Error(`Rule at index ${index} must be an object`);
|
|
2318
|
+
}
|
|
2319
|
+
const ruleObj = rule;
|
|
2320
|
+
if (!ruleObj.id || typeof ruleObj.id !== "string") {
|
|
2321
|
+
throw new Error(`Rule at index ${index} must have an "id" property`);
|
|
2322
|
+
}
|
|
2323
|
+
if (!ruleObj.flagKey || typeof ruleObj.flagKey !== "string") {
|
|
2324
|
+
throw new Error(`Rule "${ruleObj.id}" must have a "flagKey" property`);
|
|
2325
|
+
}
|
|
2326
|
+
if (!Array.isArray(ruleObj.conditions)) {
|
|
2327
|
+
throw new Error(`Rule "${ruleObj.id}" must have a "conditions" array`);
|
|
2328
|
+
}
|
|
2329
|
+
});
|
|
2330
|
+
}
|
|
2331
|
+
/**
|
|
2332
|
+
* Creates a default configuration file with values from features.
|
|
2333
|
+
*
|
|
2334
|
+
* @private
|
|
2335
|
+
* @param filePath - Path where to create the file
|
|
2336
|
+
* @param format - File format (json or yaml)
|
|
2337
|
+
*/
|
|
2338
|
+
async createDefaultFile(filePath, format) {
|
|
2339
|
+
const dir = path.dirname(filePath);
|
|
2340
|
+
await mkdir2(dir, { recursive: true });
|
|
2341
|
+
const defaultData = {
|
|
2342
|
+
flags: this.createDefaultFlags(),
|
|
2343
|
+
rules: []
|
|
2344
|
+
};
|
|
2345
|
+
let content;
|
|
2346
|
+
if (format === "json") {
|
|
2347
|
+
content = JSON.stringify(defaultData, null, 2);
|
|
2348
|
+
} else {
|
|
2349
|
+
content = yaml.stringify(defaultData);
|
|
2350
|
+
}
|
|
2351
|
+
await writeFile2(filePath, content, "utf-8");
|
|
2352
|
+
this.log(`Created default feature flag file at: ${filePath}`);
|
|
2353
|
+
}
|
|
2354
|
+
/**
|
|
2355
|
+
* Creates default flags from the features configuration.
|
|
2356
|
+
*
|
|
2357
|
+
* @private
|
|
2358
|
+
* @returns Array of default flags
|
|
2359
|
+
*/
|
|
2360
|
+
createDefaultFlags() {
|
|
2361
|
+
return Object.entries(this.features).map(([key, value]) => ({
|
|
2362
|
+
key,
|
|
2363
|
+
value,
|
|
2364
|
+
isEnabled: true,
|
|
2365
|
+
name: key,
|
|
2366
|
+
description: `Default flag for ${key}`,
|
|
2367
|
+
type: typeof value === "boolean" ? "boolean" : typeof value === "number" ? "number" : typeof value === "string" ? "string" : "json",
|
|
2368
|
+
environment: "development",
|
|
2369
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
2370
|
+
updatedAt: /* @__PURE__ */ new Date(),
|
|
2371
|
+
createdBy: "system",
|
|
2372
|
+
updatedBy: "system",
|
|
2373
|
+
metadata: {},
|
|
2374
|
+
tags: []
|
|
2375
|
+
}));
|
|
2376
|
+
}
|
|
2081
2377
|
/**
|
|
2082
2378
|
* Sets up file watching for hot reload if enabled.
|
|
2083
2379
|
*
|
|
@@ -2087,8 +2383,32 @@ var FileFeatureFlagProvider = class extends FeatureFlagProvider {
|
|
|
2087
2383
|
if (!this.config.fileConfig?.shouldWatchForChanges) {
|
|
2088
2384
|
return;
|
|
2089
2385
|
}
|
|
2090
|
-
|
|
2091
|
-
|
|
2386
|
+
const { filePath } = this.config.fileConfig;
|
|
2387
|
+
const resolvedPath = this.resolveFilePath(filePath);
|
|
2388
|
+
try {
|
|
2389
|
+
this.fileWatcher = fs.watch(resolvedPath, async (eventType) => {
|
|
2390
|
+
if (eventType === "change") {
|
|
2391
|
+
this.log(`File changed: ${resolvedPath}`);
|
|
2392
|
+
if (this.fileCheckInterval) {
|
|
2393
|
+
clearTimeout(this.fileCheckInterval);
|
|
2394
|
+
}
|
|
2395
|
+
this.fileCheckInterval = setTimeout(async () => {
|
|
2396
|
+
try {
|
|
2397
|
+
const content = await readFile2(resolvedPath, "utf-8");
|
|
2398
|
+
if (content !== this.lastFileContent) {
|
|
2399
|
+
this.log("File content changed, refreshing...");
|
|
2400
|
+
await this.refresh();
|
|
2401
|
+
}
|
|
2402
|
+
} catch (error) {
|
|
2403
|
+
this.log("Error reading changed file:", error);
|
|
2404
|
+
}
|
|
2405
|
+
}, this.config.fileConfig?.fileCheckInterval ?? FILE_CHECK_INTERVAL_DEFAULT);
|
|
2406
|
+
}
|
|
2407
|
+
});
|
|
2408
|
+
this.log(`File watching enabled for: ${resolvedPath}`);
|
|
2409
|
+
} catch (error) {
|
|
2410
|
+
this.log(`Failed to set up file watching: ${error}`);
|
|
2411
|
+
}
|
|
2092
2412
|
}
|
|
2093
2413
|
/**
|
|
2094
2414
|
* Disposes of the file provider and stops file watching.
|
|
@@ -2096,10 +2416,57 @@ var FileFeatureFlagProvider = class extends FeatureFlagProvider {
|
|
|
2096
2416
|
dispose() {
|
|
2097
2417
|
super.dispose();
|
|
2098
2418
|
if (this.fileWatcher) {
|
|
2099
|
-
|
|
2419
|
+
this.fileWatcher.close();
|
|
2100
2420
|
this.fileWatcher = void 0;
|
|
2101
2421
|
this.log("File watching stopped");
|
|
2102
2422
|
}
|
|
2423
|
+
if (this.fileCheckInterval) {
|
|
2424
|
+
clearTimeout(this.fileCheckInterval);
|
|
2425
|
+
this.fileCheckInterval = void 0;
|
|
2426
|
+
}
|
|
2427
|
+
}
|
|
2428
|
+
/**
|
|
2429
|
+
* Refreshes the provider by fetching latest data from the file.
|
|
2430
|
+
* Overrides base class to store rules.
|
|
2431
|
+
*
|
|
2432
|
+
* @returns Promise that resolves when refresh is complete
|
|
2433
|
+
*/
|
|
2434
|
+
async refresh() {
|
|
2435
|
+
await super.refresh();
|
|
2436
|
+
}
|
|
2437
|
+
/**
|
|
2438
|
+
* Updates the features object and writes to file.
|
|
2439
|
+
* This allows updating the FEATURES at runtime and persisting to file.
|
|
2440
|
+
*
|
|
2441
|
+
* @param newFeatures - New features object to sync with
|
|
2442
|
+
*/
|
|
2443
|
+
async syncFeatures(newFeatures) {
|
|
2444
|
+
this.log("Syncing with new FEATURES values and updating file");
|
|
2445
|
+
this.features = newFeatures;
|
|
2446
|
+
const fileData = {
|
|
2447
|
+
flags: this.createDefaultFlags(),
|
|
2448
|
+
rules: this.rules || []
|
|
2449
|
+
};
|
|
2450
|
+
const { filePath, format } = this.config.fileConfig;
|
|
2451
|
+
const resolvedPath = this.resolveFilePath(filePath);
|
|
2452
|
+
try {
|
|
2453
|
+
let content;
|
|
2454
|
+
if (format === "json") {
|
|
2455
|
+
content = JSON.stringify(fileData, null, 2);
|
|
2456
|
+
} else {
|
|
2457
|
+
content = yaml.stringify(fileData);
|
|
2458
|
+
}
|
|
2459
|
+
await writeFile2(resolvedPath, content, "utf-8");
|
|
2460
|
+
this.lastFileContent = content;
|
|
2461
|
+
this.engine.updateDefaults(newFeatures);
|
|
2462
|
+
await this.refresh();
|
|
2463
|
+
this.log(`Synced ${Object.keys(newFeatures).length} features to file: ${resolvedPath}`);
|
|
2464
|
+
} catch (error) {
|
|
2465
|
+
this.log("Error syncing features to file:", error);
|
|
2466
|
+
throw new Error(
|
|
2467
|
+
`Failed to sync features to file: ${error instanceof Error ? error.message : String(error)}`
|
|
2468
|
+
);
|
|
2469
|
+
}
|
|
2103
2470
|
}
|
|
2104
2471
|
/**
|
|
2105
2472
|
* Gets information about the file provider.
|
|
@@ -2107,18 +2474,23 @@ var FileFeatureFlagProvider = class extends FeatureFlagProvider {
|
|
|
2107
2474
|
* @returns File provider information
|
|
2108
2475
|
*/
|
|
2109
2476
|
getFileInfo() {
|
|
2477
|
+
const filePath = this.config.fileConfig?.filePath;
|
|
2478
|
+
const resolvedPath = filePath ? this.resolveFilePath(filePath) : void 0;
|
|
2479
|
+
let lastModified;
|
|
2480
|
+
if (resolvedPath) {
|
|
2481
|
+
try {
|
|
2482
|
+
const stats = fs.statSync(resolvedPath);
|
|
2483
|
+
lastModified = stats.mtime;
|
|
2484
|
+
} catch {
|
|
2485
|
+
}
|
|
2486
|
+
}
|
|
2110
2487
|
return {
|
|
2111
|
-
filePath
|
|
2488
|
+
filePath,
|
|
2489
|
+
resolvedPath,
|
|
2112
2490
|
format: this.config.fileConfig?.format,
|
|
2113
2491
|
isWatchEnabled: Boolean(this.config.fileConfig?.shouldWatchForChanges),
|
|
2114
|
-
isImplemented:
|
|
2115
|
-
|
|
2116
|
-
"File reading and parsing logic",
|
|
2117
|
-
"YAML and JSON format support",
|
|
2118
|
-
"File watching for hot reload",
|
|
2119
|
-
"Standard file path configuration",
|
|
2120
|
-
"File validation and error handling"
|
|
2121
|
-
]
|
|
2492
|
+
isImplemented: true,
|
|
2493
|
+
lastModified
|
|
2122
2494
|
};
|
|
2123
2495
|
}
|
|
2124
2496
|
};
|
|
@@ -2431,8 +2803,6 @@ Examples:
|
|
|
2431
2803
|
};
|
|
2432
2804
|
}
|
|
2433
2805
|
};
|
|
2434
|
-
|
|
2435
|
-
// src/domain/featureFlags/providers/factory.ts
|
|
2436
2806
|
var PROVIDER_REGISTRY = {
|
|
2437
2807
|
memory: MemoryFeatureFlagProvider,
|
|
2438
2808
|
file: FileFeatureFlagProvider,
|
|
@@ -2535,25 +2905,69 @@ var FeatureFlagProviderFactory = class {
|
|
|
2535
2905
|
};
|
|
2536
2906
|
}
|
|
2537
2907
|
/**
|
|
2538
|
-
* Creates a default
|
|
2908
|
+
* Creates a default provider.
|
|
2909
|
+
* Uses file provider if features are not provided, memory provider otherwise.
|
|
2539
2910
|
*
|
|
2540
|
-
* @param features - Record of feature flag keys to their default values
|
|
2911
|
+
* @param features - Record of feature flag keys to their default values (optional)
|
|
2541
2912
|
* @param overrides - Optional configuration overrides
|
|
2542
|
-
* @returns
|
|
2913
|
+
* @returns Provider instance
|
|
2543
2914
|
*/
|
|
2544
2915
|
static createDefault(features, overrides) {
|
|
2916
|
+
const provider = features ? "memory" : "file";
|
|
2545
2917
|
const defaultConfig = {
|
|
2546
|
-
provider
|
|
2918
|
+
provider,
|
|
2547
2919
|
isCacheEnabled: true,
|
|
2548
|
-
cacheTtl:
|
|
2549
|
-
// 5 minutes
|
|
2920
|
+
cacheTtl: FEATURE_FLAG_CACHE_TTL_DEFAULT,
|
|
2550
2921
|
refreshInterval: 0,
|
|
2551
2922
|
// No auto-refresh
|
|
2552
2923
|
isLoggingEnabled: false,
|
|
2553
2924
|
shouldFallbackToDefaults: true,
|
|
2554
2925
|
...overrides
|
|
2555
2926
|
};
|
|
2556
|
-
|
|
2927
|
+
if (defaultConfig.provider === "file" && !defaultConfig.fileConfig) {
|
|
2928
|
+
defaultConfig.fileConfig = {
|
|
2929
|
+
filePath: FEATURE_FLAG_FILE_PATHS.DEFAULT,
|
|
2930
|
+
format: "json",
|
|
2931
|
+
shouldWatchForChanges: false
|
|
2932
|
+
};
|
|
2933
|
+
}
|
|
2934
|
+
return this.create(defaultConfig, features ?? {});
|
|
2935
|
+
}
|
|
2936
|
+
/**
|
|
2937
|
+
* Type guard to check if a provider supports feature syncing.
|
|
2938
|
+
*
|
|
2939
|
+
* @param provider - The provider instance to check
|
|
2940
|
+
* @returns True if provider has syncFeatures method
|
|
2941
|
+
*/
|
|
2942
|
+
static isSyncableProvider(provider) {
|
|
2943
|
+
return "syncFeatures" in provider && typeof provider.syncFeatures === "function";
|
|
2944
|
+
}
|
|
2945
|
+
/**
|
|
2946
|
+
* Updates features on a provider if it supports the syncFeatures method.
|
|
2947
|
+
* This is useful for providers like MemoryProvider that can update their features at runtime.
|
|
2948
|
+
*
|
|
2949
|
+
* @param provider - The provider instance
|
|
2950
|
+
* @param newFeatures - New features to sync
|
|
2951
|
+
* @returns Promise that resolves when sync is complete
|
|
2952
|
+
* @throws Error if provider doesn't support feature syncing
|
|
2953
|
+
*/
|
|
2954
|
+
static async syncFeatures(provider, newFeatures) {
|
|
2955
|
+
if (this.isSyncableProvider(provider)) {
|
|
2956
|
+
await provider.syncFeatures(newFeatures);
|
|
2957
|
+
} else {
|
|
2958
|
+
throw new Error(
|
|
2959
|
+
`Provider type does not support feature syncing. Only providers with syncFeatures method (like MemoryProvider and FileProvider) support this operation.`
|
|
2960
|
+
);
|
|
2961
|
+
}
|
|
2962
|
+
}
|
|
2963
|
+
/**
|
|
2964
|
+
* Checks if a provider supports feature syncing.
|
|
2965
|
+
*
|
|
2966
|
+
* @param provider - The provider instance to check
|
|
2967
|
+
* @returns True if provider supports syncFeatures method
|
|
2968
|
+
*/
|
|
2969
|
+
static supportsFeaturesSync(provider) {
|
|
2970
|
+
return this.isSyncableProvider(provider);
|
|
2557
2971
|
}
|
|
2558
2972
|
/**
|
|
2559
2973
|
* Validates provider configuration before instantiation.
|
|
@@ -2606,8 +3020,11 @@ var FeatureFlagProviderFactory = class {
|
|
|
2606
3020
|
validator();
|
|
2607
3021
|
}
|
|
2608
3022
|
static validateFileConfig(config) {
|
|
2609
|
-
if (
|
|
2610
|
-
|
|
3023
|
+
if (config.fileConfig) {
|
|
3024
|
+
const { format } = config.fileConfig;
|
|
3025
|
+
if (format && !["json", "yaml"].includes(format)) {
|
|
3026
|
+
throw new Error('File format must be either "json" or "yaml"');
|
|
3027
|
+
}
|
|
2611
3028
|
}
|
|
2612
3029
|
}
|
|
2613
3030
|
static validateRedisConfig(config) {
|