@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.cjs
CHANGED
|
@@ -1,12 +1,39 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
var config = require('@plyaz/config');
|
|
4
|
+
var fs = require('fs');
|
|
5
|
+
var path = require('path');
|
|
6
|
+
var util = require('util');
|
|
7
|
+
var url = require('url');
|
|
8
|
+
var yaml = require('yaml');
|
|
4
9
|
var common = require('@nestjs/common');
|
|
5
10
|
var React = require('react');
|
|
6
11
|
var jsxRuntime = require('react/jsx-runtime');
|
|
7
12
|
|
|
13
|
+
var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
|
|
8
14
|
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
9
15
|
|
|
16
|
+
function _interopNamespace(e) {
|
|
17
|
+
if (e && e.__esModule) return e;
|
|
18
|
+
var n = Object.create(null);
|
|
19
|
+
if (e) {
|
|
20
|
+
Object.keys(e).forEach(function (k) {
|
|
21
|
+
if (k !== 'default') {
|
|
22
|
+
var d = Object.getOwnPropertyDescriptor(e, k);
|
|
23
|
+
Object.defineProperty(n, k, d.get ? d : {
|
|
24
|
+
enumerable: true,
|
|
25
|
+
get: function () { return e[k]; }
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
n.default = e;
|
|
31
|
+
return Object.freeze(n);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
var fs__namespace = /*#__PURE__*/_interopNamespace(fs);
|
|
35
|
+
var path__namespace = /*#__PURE__*/_interopNamespace(path);
|
|
36
|
+
var yaml__namespace = /*#__PURE__*/_interopNamespace(yaml);
|
|
10
37
|
var React__default = /*#__PURE__*/_interopDefault(React);
|
|
11
38
|
|
|
12
39
|
// @plyaz package - Built with tsup
|
|
@@ -186,9 +213,9 @@ var ValueUtils = {
|
|
|
186
213
|
* @param defaultValue - Default if path doesn't exist
|
|
187
214
|
* @returns Property value or default
|
|
188
215
|
*/
|
|
189
|
-
getNestedProperty: /* @__PURE__ */ __name((obj,
|
|
216
|
+
getNestedProperty: /* @__PURE__ */ __name((obj, path2, defaultValue) => {
|
|
190
217
|
if (!obj || typeof obj !== "object") return defaultValue;
|
|
191
|
-
const keys =
|
|
218
|
+
const keys = path2.split(".");
|
|
192
219
|
let current = obj;
|
|
193
220
|
for (const key of keys) {
|
|
194
221
|
if (current == null || typeof current !== "object") return defaultValue;
|
|
@@ -756,6 +783,16 @@ var FeatureFlagEngine = class {
|
|
|
756
783
|
this.overrides.delete(key);
|
|
757
784
|
this.log("Override removed:", key);
|
|
758
785
|
}
|
|
786
|
+
/**
|
|
787
|
+
* Updates the default values for feature flags.
|
|
788
|
+
* This is useful when the FEATURES constant is updated at runtime.
|
|
789
|
+
*
|
|
790
|
+
* @param newDefaults - New default values
|
|
791
|
+
*/
|
|
792
|
+
updateDefaults(newDefaults) {
|
|
793
|
+
this.defaults = newDefaults;
|
|
794
|
+
this.log("Updated default feature values");
|
|
795
|
+
}
|
|
759
796
|
/**
|
|
760
797
|
* Clears all manual overrides.
|
|
761
798
|
*/
|
|
@@ -933,7 +970,7 @@ var FeatureFlagEngine = class {
|
|
|
933
970
|
value: rule.value,
|
|
934
971
|
isEnabled: isTruthy(rule.value),
|
|
935
972
|
reason: "rule_match",
|
|
936
|
-
|
|
973
|
+
matchedRuleId: rule.id,
|
|
937
974
|
evaluatedAt: evaluatedAt ?? /* @__PURE__ */ new Date()
|
|
938
975
|
};
|
|
939
976
|
}
|
|
@@ -976,8 +1013,6 @@ var FeatureFlagEngine = class {
|
|
|
976
1013
|
}
|
|
977
1014
|
}
|
|
978
1015
|
};
|
|
979
|
-
|
|
980
|
-
// src/cache/strategies/memory.ts
|
|
981
1016
|
var MemoryCacheStrategy = class {
|
|
982
1017
|
static {
|
|
983
1018
|
__name(this, "MemoryCacheStrategy");
|
|
@@ -999,16 +1034,14 @@ var MemoryCacheStrategy = class {
|
|
|
999
1034
|
*
|
|
1000
1035
|
* @param config - Memory cache configuration
|
|
1001
1036
|
*/
|
|
1002
|
-
constructor(config = {}) {
|
|
1003
|
-
const DEFAULT_MAX_ENTRIES = 1e3;
|
|
1004
|
-
const DEFAULT_CLEANUP_INTERVAL = 6e4;
|
|
1037
|
+
constructor(config$1 = {}) {
|
|
1005
1038
|
const defaultConfig = {
|
|
1006
|
-
maxEntries:
|
|
1007
|
-
cleanupInterval:
|
|
1039
|
+
maxEntries: config.CACHE_MAX_SIZE_DEFAULT,
|
|
1040
|
+
cleanupInterval: config.CACHE_CLEANUP_INTERVAL_DEFAULT
|
|
1008
1041
|
};
|
|
1009
|
-
this.maxSize = config.maxSize ?? config.maxEntries ?? defaultConfig.maxEntries;
|
|
1010
|
-
this.cleanupInterval = config.cleanupInterval ?? defaultConfig.cleanupInterval;
|
|
1011
|
-
this.onEvict = config.onEvict;
|
|
1042
|
+
this.maxSize = config$1.maxSize ?? config$1.maxEntries ?? defaultConfig.maxEntries;
|
|
1043
|
+
this.cleanupInterval = config$1.cleanupInterval ?? defaultConfig.cleanupInterval;
|
|
1044
|
+
this.onEvict = config$1.onEvict;
|
|
1012
1045
|
this.startCleanup();
|
|
1013
1046
|
}
|
|
1014
1047
|
/**
|
|
@@ -1157,7 +1190,7 @@ var MemoryCacheStrategy = class {
|
|
|
1157
1190
|
}
|
|
1158
1191
|
};
|
|
1159
1192
|
|
|
1160
|
-
// src/cache/strategies/redis.ts
|
|
1193
|
+
// src/base/cache/strategies/redis.ts
|
|
1161
1194
|
var RedisCacheStrategy = class {
|
|
1162
1195
|
/**
|
|
1163
1196
|
* Creates a new Redis cache strategy.
|
|
@@ -1319,8 +1352,8 @@ var RedisCacheStrategy = class {
|
|
|
1319
1352
|
commandTimeout: this.config.commandTimeout ?? defaultOptions.commandTimeout,
|
|
1320
1353
|
enableOfflineQueue: defaultOptions.enableOfflineQueue
|
|
1321
1354
|
});
|
|
1322
|
-
await new Promise((
|
|
1323
|
-
client.on("ready",
|
|
1355
|
+
await new Promise((resolve2, reject) => {
|
|
1356
|
+
client.on("ready", resolve2);
|
|
1324
1357
|
client.on("error", reject);
|
|
1325
1358
|
});
|
|
1326
1359
|
return client;
|
|
@@ -1337,7 +1370,7 @@ var RedisCacheStrategy = class {
|
|
|
1337
1370
|
}
|
|
1338
1371
|
};
|
|
1339
1372
|
|
|
1340
|
-
// src/cache/index.ts
|
|
1373
|
+
// src/base/cache/index.ts
|
|
1341
1374
|
var CacheManager = class {
|
|
1342
1375
|
/**
|
|
1343
1376
|
* Creates a new cache manager with the specified configuration.
|
|
@@ -1866,31 +1899,53 @@ var MemoryFeatureFlagProvider = class extends FeatureFlagProvider {
|
|
|
1866
1899
|
}
|
|
1867
1900
|
}
|
|
1868
1901
|
/**
|
|
1869
|
-
* Updates a flag
|
|
1902
|
+
* Updates a flag in memory.
|
|
1870
1903
|
*
|
|
1871
|
-
* @param
|
|
1872
|
-
* @param value - The new value
|
|
1873
|
-
* @param updateProps - Optional properties to update
|
|
1904
|
+
* @param flagOrKey - Either a complete flag object or a flag key
|
|
1905
|
+
* @param value - The new value (only used when first param is a key)
|
|
1906
|
+
* @param updateProps - Optional properties to update (only used when first param is a key)
|
|
1874
1907
|
*/
|
|
1875
|
-
updateFlag(
|
|
1876
|
-
|
|
1908
|
+
async updateFlag(flagOrKey, value, updateProps) {
|
|
1909
|
+
let key;
|
|
1910
|
+
let updatedFlag;
|
|
1911
|
+
if (typeof flagOrKey === "string") {
|
|
1912
|
+
key = flagOrKey;
|
|
1913
|
+
const existingFlag = this.flags.find((f) => f.key === key);
|
|
1914
|
+
if (!existingFlag) {
|
|
1915
|
+
this.log(`Flag with key ${key} not found in memory`);
|
|
1916
|
+
return;
|
|
1917
|
+
}
|
|
1918
|
+
if (value === void 0) {
|
|
1919
|
+
this.log(`Value is required when updating flag by key`);
|
|
1920
|
+
return;
|
|
1921
|
+
}
|
|
1922
|
+
updatedFlag = {
|
|
1923
|
+
...existingFlag,
|
|
1924
|
+
value,
|
|
1925
|
+
type: this.inferFlagType(value),
|
|
1926
|
+
updatedAt: /* @__PURE__ */ new Date(),
|
|
1927
|
+
updatedBy: "memory-runtime",
|
|
1928
|
+
...updateProps
|
|
1929
|
+
};
|
|
1930
|
+
} else {
|
|
1931
|
+
const flag = flagOrKey;
|
|
1932
|
+
key = flag.key;
|
|
1933
|
+
updatedFlag = {
|
|
1934
|
+
...flag,
|
|
1935
|
+
updatedAt: /* @__PURE__ */ new Date(),
|
|
1936
|
+
updatedBy: flag.updatedBy || "memory-runtime"
|
|
1937
|
+
};
|
|
1938
|
+
}
|
|
1939
|
+
const flagIndex = this.flags.findIndex((f) => f.key === key);
|
|
1877
1940
|
if (flagIndex === -1) {
|
|
1878
1941
|
this.log(`Flag with key ${key} not found in memory`);
|
|
1879
1942
|
return;
|
|
1880
1943
|
}
|
|
1881
|
-
const updatedFlag = {
|
|
1882
|
-
...this.flags[flagIndex],
|
|
1883
|
-
value,
|
|
1884
|
-
type: this.inferFlagType(value),
|
|
1885
|
-
updatedAt: /* @__PURE__ */ new Date(),
|
|
1886
|
-
updatedBy: "memory-runtime",
|
|
1887
|
-
...updateProps
|
|
1888
|
-
};
|
|
1889
1944
|
this.flags[flagIndex] = updatedFlag;
|
|
1890
1945
|
this.engine.setFlags(this.flags);
|
|
1891
1946
|
void this.cacheManager.clear();
|
|
1892
1947
|
this.notifySubscribers();
|
|
1893
|
-
this.log(`Updated flag: ${key}
|
|
1948
|
+
this.log(`Updated flag: ${key}`);
|
|
1894
1949
|
}
|
|
1895
1950
|
/**
|
|
1896
1951
|
* Adds a new flag to memory at runtime.
|
|
@@ -2000,6 +2055,18 @@ var MemoryFeatureFlagProvider = class extends FeatureFlagProvider {
|
|
|
2000
2055
|
getCurrentRules() {
|
|
2001
2056
|
return [...this.rules];
|
|
2002
2057
|
}
|
|
2058
|
+
/**
|
|
2059
|
+
* Updates the features object and syncs all flags.
|
|
2060
|
+
* This allows updating the FEATURES constant at runtime.
|
|
2061
|
+
*
|
|
2062
|
+
* @param newFeatures - New features object to sync with
|
|
2063
|
+
*/
|
|
2064
|
+
async syncFeatures(newFeatures) {
|
|
2065
|
+
this.log("Syncing with new FEATURES values");
|
|
2066
|
+
this.features = newFeatures;
|
|
2067
|
+
await this.refresh();
|
|
2068
|
+
this.log(`Synced ${Object.keys(newFeatures).length} features`);
|
|
2069
|
+
}
|
|
2003
2070
|
/**
|
|
2004
2071
|
* Resets the memory provider to its initial state.
|
|
2005
2072
|
*/
|
|
@@ -2033,13 +2100,18 @@ var MemoryFeatureFlagProvider = class extends FeatureFlagProvider {
|
|
|
2033
2100
|
}
|
|
2034
2101
|
}
|
|
2035
2102
|
};
|
|
2036
|
-
|
|
2037
|
-
|
|
2103
|
+
var readFile2 = util.promisify(fs__namespace.readFile);
|
|
2104
|
+
var writeFile2 = util.promisify(fs__namespace.writeFile);
|
|
2105
|
+
var access2 = util.promisify(fs__namespace.access);
|
|
2106
|
+
var mkdir2 = util.promisify(fs__namespace.mkdir);
|
|
2038
2107
|
var FileFeatureFlagProvider = class extends FeatureFlagProvider {
|
|
2039
2108
|
static {
|
|
2040
2109
|
__name(this, "FileFeatureFlagProvider");
|
|
2041
2110
|
}
|
|
2042
2111
|
fileWatcher;
|
|
2112
|
+
lastFileContent;
|
|
2113
|
+
fileCheckInterval;
|
|
2114
|
+
rules = [];
|
|
2043
2115
|
/**
|
|
2044
2116
|
* Creates a new file feature flag provider.
|
|
2045
2117
|
*
|
|
@@ -2049,8 +2121,15 @@ var FileFeatureFlagProvider = class extends FeatureFlagProvider {
|
|
|
2049
2121
|
constructor(config, features) {
|
|
2050
2122
|
super(config, features);
|
|
2051
2123
|
this.validateConfig();
|
|
2052
|
-
|
|
2053
|
-
|
|
2124
|
+
}
|
|
2125
|
+
/**
|
|
2126
|
+
* Initializes the provider and sets up file watching if enabled.
|
|
2127
|
+
*/
|
|
2128
|
+
async initialize() {
|
|
2129
|
+
await super.initialize();
|
|
2130
|
+
if (this.config.fileConfig?.shouldWatchForChanges) {
|
|
2131
|
+
this.setupFileWatcher();
|
|
2132
|
+
}
|
|
2054
2133
|
}
|
|
2055
2134
|
/**
|
|
2056
2135
|
* Fetches flags and rules from the configuration file.
|
|
@@ -2059,9 +2138,89 @@ var FileFeatureFlagProvider = class extends FeatureFlagProvider {
|
|
|
2059
2138
|
* @returns Promise resolving to flags and rules from file
|
|
2060
2139
|
*/
|
|
2061
2140
|
async fetchData() {
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2141
|
+
const { filePath, format } = this.config.fileConfig;
|
|
2142
|
+
const resolvedPath = this.resolveFilePath(filePath);
|
|
2143
|
+
try {
|
|
2144
|
+
await access2(resolvedPath, fs__namespace.constants.R_OK);
|
|
2145
|
+
const content = await readFile2(resolvedPath, "utf-8");
|
|
2146
|
+
this.lastFileContent = content;
|
|
2147
|
+
const data = await this.parseFileContent(content, format);
|
|
2148
|
+
this.validateFileData(data);
|
|
2149
|
+
this.rules = data.rules || [];
|
|
2150
|
+
return {
|
|
2151
|
+
flags: data.flags || [],
|
|
2152
|
+
rules: data.rules || []
|
|
2153
|
+
};
|
|
2154
|
+
} catch (error) {
|
|
2155
|
+
return this.handleFetchDataError(error, resolvedPath, format);
|
|
2156
|
+
}
|
|
2157
|
+
}
|
|
2158
|
+
/**
|
|
2159
|
+
* Parses file content based on format.
|
|
2160
|
+
*
|
|
2161
|
+
* @private
|
|
2162
|
+
*/
|
|
2163
|
+
async parseFileContent(content, format) {
|
|
2164
|
+
if (format === "json") {
|
|
2165
|
+
return this.parseJSON(content);
|
|
2166
|
+
} else if (format === "yaml") {
|
|
2167
|
+
return await this.parseYAML(content);
|
|
2168
|
+
}
|
|
2169
|
+
throw new Error(`Unsupported file format: ${format}`);
|
|
2170
|
+
}
|
|
2171
|
+
/**
|
|
2172
|
+
* Handles errors for fetchData, including file creation and fallback.
|
|
2173
|
+
*
|
|
2174
|
+
* @private
|
|
2175
|
+
*/
|
|
2176
|
+
async handleFetchDataError(error, resolvedPath, format) {
|
|
2177
|
+
const isFileNotFound = this.isFileNotFoundError(error);
|
|
2178
|
+
if (isFileNotFound && this.config.shouldFallbackToDefaults) {
|
|
2179
|
+
return await this.handleFileNotFound(resolvedPath, format);
|
|
2180
|
+
}
|
|
2181
|
+
this.log(`Error reading file ${resolvedPath}:`, error);
|
|
2182
|
+
if (this.config.shouldFallbackToDefaults) {
|
|
2183
|
+
return this.handleFallbackToDefaults();
|
|
2184
|
+
}
|
|
2185
|
+
throw error;
|
|
2186
|
+
}
|
|
2187
|
+
/**
|
|
2188
|
+
* Type guard for NodeJS.ErrnoException.
|
|
2189
|
+
*/
|
|
2190
|
+
isFileNotFoundError(error) {
|
|
2191
|
+
return error instanceof Error && typeof error.code === "string" && error.code === "ENOENT";
|
|
2192
|
+
}
|
|
2193
|
+
/**
|
|
2194
|
+
* Handles the case when the file is not found and fallback is enabled.
|
|
2195
|
+
*/
|
|
2196
|
+
async handleFileNotFound(resolvedPath, format) {
|
|
2197
|
+
this.log(`File not found at ${resolvedPath}, creating with default values`);
|
|
2198
|
+
try {
|
|
2199
|
+
await this.createDefaultFile(resolvedPath, format);
|
|
2200
|
+
const content = await readFile2(resolvedPath, "utf-8");
|
|
2201
|
+
this.lastFileContent = content;
|
|
2202
|
+
const data = await this.parseFileContent(content, format);
|
|
2203
|
+
return {
|
|
2204
|
+
flags: data.flags || [],
|
|
2205
|
+
rules: data.rules || []
|
|
2206
|
+
};
|
|
2207
|
+
} catch (createError) {
|
|
2208
|
+
this.log("Error creating default file:", createError);
|
|
2209
|
+
return {
|
|
2210
|
+
flags: this.createDefaultFlags(),
|
|
2211
|
+
rules: []
|
|
2212
|
+
};
|
|
2213
|
+
}
|
|
2214
|
+
}
|
|
2215
|
+
/**
|
|
2216
|
+
* Handles fallback to default flags and rules.
|
|
2217
|
+
*/
|
|
2218
|
+
handleFallbackToDefaults() {
|
|
2219
|
+
this.log("Falling back to default values");
|
|
2220
|
+
return {
|
|
2221
|
+
flags: this.createDefaultFlags(),
|
|
2222
|
+
rules: []
|
|
2223
|
+
};
|
|
2065
2224
|
}
|
|
2066
2225
|
/**
|
|
2067
2226
|
* Validates the file provider configuration.
|
|
@@ -2084,6 +2243,165 @@ var FileFeatureFlagProvider = class extends FeatureFlagProvider {
|
|
|
2084
2243
|
throw new Error('File format must be either "json" or "yaml"');
|
|
2085
2244
|
}
|
|
2086
2245
|
}
|
|
2246
|
+
/**
|
|
2247
|
+
* Resolves the file path, supporting relative and absolute paths.
|
|
2248
|
+
*
|
|
2249
|
+
* @private
|
|
2250
|
+
* @param filePath - The file path from configuration
|
|
2251
|
+
* @returns Resolved absolute file path
|
|
2252
|
+
*/
|
|
2253
|
+
resolveFilePath(filePath) {
|
|
2254
|
+
let pathToResolve = filePath;
|
|
2255
|
+
if (!pathToResolve) {
|
|
2256
|
+
const __filename = url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)));
|
|
2257
|
+
const __dirname = path__namespace.dirname(__filename);
|
|
2258
|
+
pathToResolve = path__namespace.join(__dirname, "../../../config/feature-provider.json");
|
|
2259
|
+
}
|
|
2260
|
+
if (path__namespace.isAbsolute(pathToResolve)) {
|
|
2261
|
+
return pathToResolve;
|
|
2262
|
+
}
|
|
2263
|
+
return path__namespace.resolve(process.cwd(), pathToResolve);
|
|
2264
|
+
}
|
|
2265
|
+
/**
|
|
2266
|
+
* Parses JSON content.
|
|
2267
|
+
*
|
|
2268
|
+
* @private
|
|
2269
|
+
* @param content - File content
|
|
2270
|
+
* @returns Parsed data
|
|
2271
|
+
*/
|
|
2272
|
+
parseJSON(content) {
|
|
2273
|
+
try {
|
|
2274
|
+
return JSON.parse(content);
|
|
2275
|
+
} catch (error) {
|
|
2276
|
+
throw new Error(
|
|
2277
|
+
`Invalid JSON format: ${error instanceof Error ? error.message : String(error)}`
|
|
2278
|
+
);
|
|
2279
|
+
}
|
|
2280
|
+
}
|
|
2281
|
+
/**
|
|
2282
|
+
* Parses YAML content.
|
|
2283
|
+
*
|
|
2284
|
+
* @private
|
|
2285
|
+
* @param content - File content
|
|
2286
|
+
* @returns Parsed data
|
|
2287
|
+
*/
|
|
2288
|
+
async parseYAML(content) {
|
|
2289
|
+
try {
|
|
2290
|
+
const data = yaml__namespace.parse(content);
|
|
2291
|
+
return data;
|
|
2292
|
+
} catch (error) {
|
|
2293
|
+
throw new Error(
|
|
2294
|
+
`Invalid YAML format: ${error instanceof Error ? error.message : String(error)}`
|
|
2295
|
+
);
|
|
2296
|
+
}
|
|
2297
|
+
}
|
|
2298
|
+
/**
|
|
2299
|
+
* Validates the structure of file data.
|
|
2300
|
+
*
|
|
2301
|
+
* @private
|
|
2302
|
+
* @param data - Parsed file data
|
|
2303
|
+
*/
|
|
2304
|
+
validateFileData(data) {
|
|
2305
|
+
if (!data || typeof data !== "object") {
|
|
2306
|
+
throw new Error("File must contain a valid object");
|
|
2307
|
+
}
|
|
2308
|
+
const fileData = data;
|
|
2309
|
+
this.checkFlagsArray(fileData.flags);
|
|
2310
|
+
this.checkRulesArray(fileData.rules);
|
|
2311
|
+
}
|
|
2312
|
+
checkFlagsArray(flags) {
|
|
2313
|
+
if (flags && !Array.isArray(flags)) {
|
|
2314
|
+
throw new Error('"flags" must be an array');
|
|
2315
|
+
}
|
|
2316
|
+
if (Array.isArray(flags)) {
|
|
2317
|
+
this.validateFlags(flags);
|
|
2318
|
+
}
|
|
2319
|
+
}
|
|
2320
|
+
checkRulesArray(rules) {
|
|
2321
|
+
if (rules && !Array.isArray(rules)) {
|
|
2322
|
+
throw new Error('"rules" must be an array');
|
|
2323
|
+
}
|
|
2324
|
+
if (Array.isArray(rules)) {
|
|
2325
|
+
this.validateRules(rules);
|
|
2326
|
+
}
|
|
2327
|
+
}
|
|
2328
|
+
validateFlags(flags) {
|
|
2329
|
+
flags.forEach((flag, index) => {
|
|
2330
|
+
if (!flag || typeof flag !== "object") {
|
|
2331
|
+
throw new Error(`Flag at index ${index} must be an object`);
|
|
2332
|
+
}
|
|
2333
|
+
const flagObj = flag;
|
|
2334
|
+
if (!flagObj.key || typeof flagObj.key !== "string") {
|
|
2335
|
+
throw new Error(`Flag at index ${index} must have a "key" property`);
|
|
2336
|
+
}
|
|
2337
|
+
if (flagObj.value === void 0) {
|
|
2338
|
+
throw new Error(`Flag "${flagObj.key}" must have a "value" property`);
|
|
2339
|
+
}
|
|
2340
|
+
});
|
|
2341
|
+
}
|
|
2342
|
+
validateRules(rules) {
|
|
2343
|
+
rules.forEach((rule, index) => {
|
|
2344
|
+
if (!rule || typeof rule !== "object") {
|
|
2345
|
+
throw new Error(`Rule at index ${index} must be an object`);
|
|
2346
|
+
}
|
|
2347
|
+
const ruleObj = rule;
|
|
2348
|
+
if (!ruleObj.id || typeof ruleObj.id !== "string") {
|
|
2349
|
+
throw new Error(`Rule at index ${index} must have an "id" property`);
|
|
2350
|
+
}
|
|
2351
|
+
if (!ruleObj.flagKey || typeof ruleObj.flagKey !== "string") {
|
|
2352
|
+
throw new Error(`Rule "${ruleObj.id}" must have a "flagKey" property`);
|
|
2353
|
+
}
|
|
2354
|
+
if (!Array.isArray(ruleObj.conditions)) {
|
|
2355
|
+
throw new Error(`Rule "${ruleObj.id}" must have a "conditions" array`);
|
|
2356
|
+
}
|
|
2357
|
+
});
|
|
2358
|
+
}
|
|
2359
|
+
/**
|
|
2360
|
+
* Creates a default configuration file with values from features.
|
|
2361
|
+
*
|
|
2362
|
+
* @private
|
|
2363
|
+
* @param filePath - Path where to create the file
|
|
2364
|
+
* @param format - File format (json or yaml)
|
|
2365
|
+
*/
|
|
2366
|
+
async createDefaultFile(filePath, format) {
|
|
2367
|
+
const dir = path__namespace.dirname(filePath);
|
|
2368
|
+
await mkdir2(dir, { recursive: true });
|
|
2369
|
+
const defaultData = {
|
|
2370
|
+
flags: this.createDefaultFlags(),
|
|
2371
|
+
rules: []
|
|
2372
|
+
};
|
|
2373
|
+
let content;
|
|
2374
|
+
if (format === "json") {
|
|
2375
|
+
content = JSON.stringify(defaultData, null, 2);
|
|
2376
|
+
} else {
|
|
2377
|
+
content = yaml__namespace.stringify(defaultData);
|
|
2378
|
+
}
|
|
2379
|
+
await writeFile2(filePath, content, "utf-8");
|
|
2380
|
+
this.log(`Created default feature flag file at: ${filePath}`);
|
|
2381
|
+
}
|
|
2382
|
+
/**
|
|
2383
|
+
* Creates default flags from the features configuration.
|
|
2384
|
+
*
|
|
2385
|
+
* @private
|
|
2386
|
+
* @returns Array of default flags
|
|
2387
|
+
*/
|
|
2388
|
+
createDefaultFlags() {
|
|
2389
|
+
return Object.entries(this.features).map(([key, value]) => ({
|
|
2390
|
+
key,
|
|
2391
|
+
value,
|
|
2392
|
+
isEnabled: true,
|
|
2393
|
+
name: key,
|
|
2394
|
+
description: `Default flag for ${key}`,
|
|
2395
|
+
type: typeof value === "boolean" ? "boolean" : typeof value === "number" ? "number" : typeof value === "string" ? "string" : "json",
|
|
2396
|
+
environment: "development",
|
|
2397
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
2398
|
+
updatedAt: /* @__PURE__ */ new Date(),
|
|
2399
|
+
createdBy: "system",
|
|
2400
|
+
updatedBy: "system",
|
|
2401
|
+
metadata: {},
|
|
2402
|
+
tags: []
|
|
2403
|
+
}));
|
|
2404
|
+
}
|
|
2087
2405
|
/**
|
|
2088
2406
|
* Sets up file watching for hot reload if enabled.
|
|
2089
2407
|
*
|
|
@@ -2093,8 +2411,32 @@ var FileFeatureFlagProvider = class extends FeatureFlagProvider {
|
|
|
2093
2411
|
if (!this.config.fileConfig?.shouldWatchForChanges) {
|
|
2094
2412
|
return;
|
|
2095
2413
|
}
|
|
2096
|
-
|
|
2097
|
-
|
|
2414
|
+
const { filePath } = this.config.fileConfig;
|
|
2415
|
+
const resolvedPath = this.resolveFilePath(filePath);
|
|
2416
|
+
try {
|
|
2417
|
+
this.fileWatcher = fs__namespace.watch(resolvedPath, async (eventType) => {
|
|
2418
|
+
if (eventType === "change") {
|
|
2419
|
+
this.log(`File changed: ${resolvedPath}`);
|
|
2420
|
+
if (this.fileCheckInterval) {
|
|
2421
|
+
clearTimeout(this.fileCheckInterval);
|
|
2422
|
+
}
|
|
2423
|
+
this.fileCheckInterval = setTimeout(async () => {
|
|
2424
|
+
try {
|
|
2425
|
+
const content = await readFile2(resolvedPath, "utf-8");
|
|
2426
|
+
if (content !== this.lastFileContent) {
|
|
2427
|
+
this.log("File content changed, refreshing...");
|
|
2428
|
+
await this.refresh();
|
|
2429
|
+
}
|
|
2430
|
+
} catch (error) {
|
|
2431
|
+
this.log("Error reading changed file:", error);
|
|
2432
|
+
}
|
|
2433
|
+
}, this.config.fileConfig?.fileCheckInterval ?? config.FILE_CHECK_INTERVAL_DEFAULT);
|
|
2434
|
+
}
|
|
2435
|
+
});
|
|
2436
|
+
this.log(`File watching enabled for: ${resolvedPath}`);
|
|
2437
|
+
} catch (error) {
|
|
2438
|
+
this.log(`Failed to set up file watching: ${error}`);
|
|
2439
|
+
}
|
|
2098
2440
|
}
|
|
2099
2441
|
/**
|
|
2100
2442
|
* Disposes of the file provider and stops file watching.
|
|
@@ -2102,10 +2444,57 @@ var FileFeatureFlagProvider = class extends FeatureFlagProvider {
|
|
|
2102
2444
|
dispose() {
|
|
2103
2445
|
super.dispose();
|
|
2104
2446
|
if (this.fileWatcher) {
|
|
2105
|
-
|
|
2447
|
+
this.fileWatcher.close();
|
|
2106
2448
|
this.fileWatcher = void 0;
|
|
2107
2449
|
this.log("File watching stopped");
|
|
2108
2450
|
}
|
|
2451
|
+
if (this.fileCheckInterval) {
|
|
2452
|
+
clearTimeout(this.fileCheckInterval);
|
|
2453
|
+
this.fileCheckInterval = void 0;
|
|
2454
|
+
}
|
|
2455
|
+
}
|
|
2456
|
+
/**
|
|
2457
|
+
* Refreshes the provider by fetching latest data from the file.
|
|
2458
|
+
* Overrides base class to store rules.
|
|
2459
|
+
*
|
|
2460
|
+
* @returns Promise that resolves when refresh is complete
|
|
2461
|
+
*/
|
|
2462
|
+
async refresh() {
|
|
2463
|
+
await super.refresh();
|
|
2464
|
+
}
|
|
2465
|
+
/**
|
|
2466
|
+
* Updates the features object and writes to file.
|
|
2467
|
+
* This allows updating the FEATURES at runtime and persisting to file.
|
|
2468
|
+
*
|
|
2469
|
+
* @param newFeatures - New features object to sync with
|
|
2470
|
+
*/
|
|
2471
|
+
async syncFeatures(newFeatures) {
|
|
2472
|
+
this.log("Syncing with new FEATURES values and updating file");
|
|
2473
|
+
this.features = newFeatures;
|
|
2474
|
+
const fileData = {
|
|
2475
|
+
flags: this.createDefaultFlags(),
|
|
2476
|
+
rules: this.rules || []
|
|
2477
|
+
};
|
|
2478
|
+
const { filePath, format } = this.config.fileConfig;
|
|
2479
|
+
const resolvedPath = this.resolveFilePath(filePath);
|
|
2480
|
+
try {
|
|
2481
|
+
let content;
|
|
2482
|
+
if (format === "json") {
|
|
2483
|
+
content = JSON.stringify(fileData, null, 2);
|
|
2484
|
+
} else {
|
|
2485
|
+
content = yaml__namespace.stringify(fileData);
|
|
2486
|
+
}
|
|
2487
|
+
await writeFile2(resolvedPath, content, "utf-8");
|
|
2488
|
+
this.lastFileContent = content;
|
|
2489
|
+
this.engine.updateDefaults(newFeatures);
|
|
2490
|
+
await this.refresh();
|
|
2491
|
+
this.log(`Synced ${Object.keys(newFeatures).length} features to file: ${resolvedPath}`);
|
|
2492
|
+
} catch (error) {
|
|
2493
|
+
this.log("Error syncing features to file:", error);
|
|
2494
|
+
throw new Error(
|
|
2495
|
+
`Failed to sync features to file: ${error instanceof Error ? error.message : String(error)}`
|
|
2496
|
+
);
|
|
2497
|
+
}
|
|
2109
2498
|
}
|
|
2110
2499
|
/**
|
|
2111
2500
|
* Gets information about the file provider.
|
|
@@ -2113,18 +2502,23 @@ var FileFeatureFlagProvider = class extends FeatureFlagProvider {
|
|
|
2113
2502
|
* @returns File provider information
|
|
2114
2503
|
*/
|
|
2115
2504
|
getFileInfo() {
|
|
2505
|
+
const filePath = this.config.fileConfig?.filePath;
|
|
2506
|
+
const resolvedPath = filePath ? this.resolveFilePath(filePath) : void 0;
|
|
2507
|
+
let lastModified;
|
|
2508
|
+
if (resolvedPath) {
|
|
2509
|
+
try {
|
|
2510
|
+
const stats = fs__namespace.statSync(resolvedPath);
|
|
2511
|
+
lastModified = stats.mtime;
|
|
2512
|
+
} catch {
|
|
2513
|
+
}
|
|
2514
|
+
}
|
|
2116
2515
|
return {
|
|
2117
|
-
filePath
|
|
2516
|
+
filePath,
|
|
2517
|
+
resolvedPath,
|
|
2118
2518
|
format: this.config.fileConfig?.format,
|
|
2119
2519
|
isWatchEnabled: Boolean(this.config.fileConfig?.shouldWatchForChanges),
|
|
2120
|
-
isImplemented:
|
|
2121
|
-
|
|
2122
|
-
"File reading and parsing logic",
|
|
2123
|
-
"YAML and JSON format support",
|
|
2124
|
-
"File watching for hot reload",
|
|
2125
|
-
"Standard file path configuration",
|
|
2126
|
-
"File validation and error handling"
|
|
2127
|
-
]
|
|
2520
|
+
isImplemented: true,
|
|
2521
|
+
lastModified
|
|
2128
2522
|
};
|
|
2129
2523
|
}
|
|
2130
2524
|
};
|
|
@@ -2437,8 +2831,6 @@ Examples:
|
|
|
2437
2831
|
};
|
|
2438
2832
|
}
|
|
2439
2833
|
};
|
|
2440
|
-
|
|
2441
|
-
// src/domain/featureFlags/providers/factory.ts
|
|
2442
2834
|
var PROVIDER_REGISTRY = {
|
|
2443
2835
|
memory: MemoryFeatureFlagProvider,
|
|
2444
2836
|
file: FileFeatureFlagProvider,
|
|
@@ -2541,25 +2933,69 @@ var FeatureFlagProviderFactory = class {
|
|
|
2541
2933
|
};
|
|
2542
2934
|
}
|
|
2543
2935
|
/**
|
|
2544
|
-
* Creates a default
|
|
2936
|
+
* Creates a default provider.
|
|
2937
|
+
* Uses file provider if features are not provided, memory provider otherwise.
|
|
2545
2938
|
*
|
|
2546
|
-
* @param features - Record of feature flag keys to their default values
|
|
2939
|
+
* @param features - Record of feature flag keys to their default values (optional)
|
|
2547
2940
|
* @param overrides - Optional configuration overrides
|
|
2548
|
-
* @returns
|
|
2941
|
+
* @returns Provider instance
|
|
2549
2942
|
*/
|
|
2550
2943
|
static createDefault(features, overrides) {
|
|
2944
|
+
const provider = features ? "memory" : "file";
|
|
2551
2945
|
const defaultConfig = {
|
|
2552
|
-
provider
|
|
2946
|
+
provider,
|
|
2553
2947
|
isCacheEnabled: true,
|
|
2554
|
-
cacheTtl:
|
|
2555
|
-
// 5 minutes
|
|
2948
|
+
cacheTtl: config.FEATURE_FLAG_CACHE_TTL_DEFAULT,
|
|
2556
2949
|
refreshInterval: 0,
|
|
2557
2950
|
// No auto-refresh
|
|
2558
2951
|
isLoggingEnabled: false,
|
|
2559
2952
|
shouldFallbackToDefaults: true,
|
|
2560
2953
|
...overrides
|
|
2561
2954
|
};
|
|
2562
|
-
|
|
2955
|
+
if (defaultConfig.provider === "file" && !defaultConfig.fileConfig) {
|
|
2956
|
+
defaultConfig.fileConfig = {
|
|
2957
|
+
filePath: config.FEATURE_FLAG_FILE_PATHS.DEFAULT,
|
|
2958
|
+
format: "json",
|
|
2959
|
+
shouldWatchForChanges: false
|
|
2960
|
+
};
|
|
2961
|
+
}
|
|
2962
|
+
return this.create(defaultConfig, features ?? {});
|
|
2963
|
+
}
|
|
2964
|
+
/**
|
|
2965
|
+
* Type guard to check if a provider supports feature syncing.
|
|
2966
|
+
*
|
|
2967
|
+
* @param provider - The provider instance to check
|
|
2968
|
+
* @returns True if provider has syncFeatures method
|
|
2969
|
+
*/
|
|
2970
|
+
static isSyncableProvider(provider) {
|
|
2971
|
+
return "syncFeatures" in provider && typeof provider.syncFeatures === "function";
|
|
2972
|
+
}
|
|
2973
|
+
/**
|
|
2974
|
+
* Updates features on a provider if it supports the syncFeatures method.
|
|
2975
|
+
* This is useful for providers like MemoryProvider that can update their features at runtime.
|
|
2976
|
+
*
|
|
2977
|
+
* @param provider - The provider instance
|
|
2978
|
+
* @param newFeatures - New features to sync
|
|
2979
|
+
* @returns Promise that resolves when sync is complete
|
|
2980
|
+
* @throws Error if provider doesn't support feature syncing
|
|
2981
|
+
*/
|
|
2982
|
+
static async syncFeatures(provider, newFeatures) {
|
|
2983
|
+
if (this.isSyncableProvider(provider)) {
|
|
2984
|
+
await provider.syncFeatures(newFeatures);
|
|
2985
|
+
} else {
|
|
2986
|
+
throw new Error(
|
|
2987
|
+
`Provider type does not support feature syncing. Only providers with syncFeatures method (like MemoryProvider and FileProvider) support this operation.`
|
|
2988
|
+
);
|
|
2989
|
+
}
|
|
2990
|
+
}
|
|
2991
|
+
/**
|
|
2992
|
+
* Checks if a provider supports feature syncing.
|
|
2993
|
+
*
|
|
2994
|
+
* @param provider - The provider instance to check
|
|
2995
|
+
* @returns True if provider supports syncFeatures method
|
|
2996
|
+
*/
|
|
2997
|
+
static supportsFeaturesSync(provider) {
|
|
2998
|
+
return this.isSyncableProvider(provider);
|
|
2563
2999
|
}
|
|
2564
3000
|
/**
|
|
2565
3001
|
* Validates provider configuration before instantiation.
|
|
@@ -2612,8 +3048,11 @@ var FeatureFlagProviderFactory = class {
|
|
|
2612
3048
|
validator();
|
|
2613
3049
|
}
|
|
2614
3050
|
static validateFileConfig(config) {
|
|
2615
|
-
if (
|
|
2616
|
-
|
|
3051
|
+
if (config.fileConfig) {
|
|
3052
|
+
const { format } = config.fileConfig;
|
|
3053
|
+
if (format && !["json", "yaml"].includes(format)) {
|
|
3054
|
+
throw new Error('File format must be either "json" or "yaml"');
|
|
3055
|
+
}
|
|
2617
3056
|
}
|
|
2618
3057
|
}
|
|
2619
3058
|
static validateRedisConfig(config) {
|