@tehw0lf/yaft 0.0.16 → 0.0.17

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 CHANGED
@@ -129,6 +129,19 @@ rules can be applied directly to a feature without going through a provider.
129
129
  A missing feature is off. Unset, `null` or unparseable dates are ignored rather
130
130
  than treated as an error, and never throw.
131
131
 
132
+ ### Boolean shape
133
+
134
+ The boolean providers hold `{ "myToggle": true }` and have no time logic. Only
135
+ the JSON boolean `true` is on. A JSON `false` is kept in the data and reads as
136
+ off. Any value that is not a boolean -- `"true"`, `"false"`, `1`, `null` -- is
137
+ dropped when the data loads, so its key is missing from the data and reads as
138
+ off. `normaliseBooleans` is exported and applies the same rule.
139
+
140
+ **Changed in 0.0.17.** Before, `isEnabled` returned whatever was stored, and a
141
+ caller testing it with `if` turned the string `"false"` **on**, because a
142
+ non-empty string is truthy. A configuration that relied on string or number
143
+ values has to switch to real booleans.
144
+
132
145
  ### Date format
133
146
 
134
147
  Dates must be **RFC 3339 with an offset** (`2026-09-18T15:00:00Z` or
@@ -166,8 +179,8 @@ is tested against that suite rather than only against its own expectations.
166
179
  The version is pinned in `conformance.lock`:
167
180
 
168
181
  ```
169
- version=v1.1.0
170
- sha256=d83ff1c960ad29830c00b57727591da628f4323b777faea245f603565d1c9ae9
182
+ version=v2.0.0
183
+ sha256=a10c1342e9ef346f998c0a819d90ae7f4984b53c933ed555eefab09419e993aa
171
184
  ```
172
185
 
173
186
  `npm test` fetches that release, verifies the checksum and unpacks it before
@@ -37,7 +37,7 @@ class ApiServiceBooleanProvider {
37
37
  // through the core normaliser, so the two providers cannot disagree
38
38
  // about what a response means.
39
39
  if (isKeyedBooleans(response.data)) {
40
- this.data = response.data;
40
+ this.data = (0, mapping_1.normaliseBooleans)(response.data);
41
41
  return;
42
42
  }
43
43
  // Only the value matters here. The boolean shape has no time logic by
@@ -59,24 +59,27 @@ class ApiServiceBooleanProvider {
59
59
  }
60
60
  }
61
61
  isEnabled(key) {
62
- const feature = this.data[key];
63
- if (feature === undefined || feature === null)
64
- return false;
65
- return feature;
62
+ return this.data[key] === true;
66
63
  }
67
64
  }
68
65
  exports.ApiServiceBooleanProvider = ApiServiceBooleanProvider;
69
66
  /**
70
- * True when the payload is already `{ "myToggle": true }`.
67
+ * True when the payload is in the boolean shape, `{ "myToggle": true }`.
71
68
  *
72
69
  * Distinguishing this from a feature-shaped response matters: running a keyed
73
70
  * boolean object through the feature normaliser would look for a `key` field,
74
71
  * find none and discard every entry.
72
+ *
73
+ * One boolean value is enough. A mixed payload such as
74
+ * `{ "a": true, "b": "x" }` is still the boolean shape; normaliseBooleans then
75
+ * drops `b` and keeps `a` (R29), exactly as the local provider does. Requiring
76
+ * every value to be boolean sent it to the feature normaliser instead, which
77
+ * lost `a` as well. Feature responses never carry a boolean -- `value` is a
78
+ * string -- so they are not caught by this.
75
79
  */
76
80
  function isKeyedBooleans(data) {
77
81
  if (data === null || typeof data !== 'object' || Array.isArray(data)) {
78
82
  return false;
79
83
  }
80
- const values = Object.values(data);
81
- return values.length > 0 && values.every((v) => typeof v === 'boolean');
84
+ return Object.values(data).some((v) => typeof v === 'boolean');
82
85
  }
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.LocalStorageBooleanProvider = void 0;
4
+ const mapping_1 = require("../mapping");
4
5
  class LocalStorageBooleanProvider {
5
6
  data = {};
6
7
  constructor(configPath) {
@@ -8,8 +9,7 @@ class LocalStorageBooleanProvider {
8
9
  }
9
10
  getConfig(configPathOrUrl) {
10
11
  try {
11
- const configData = require(configPathOrUrl);
12
- this.data = configData;
12
+ this.data = (0, mapping_1.normaliseBooleans)(require(configPathOrUrl));
13
13
  }
14
14
  catch (error) {
15
15
  console.error("Failed to load configuration from local file:", error);
@@ -17,10 +17,7 @@ class LocalStorageBooleanProvider {
17
17
  }
18
18
  }
19
19
  isEnabled(key) {
20
- const feature = this.data[key];
21
- if (feature === undefined || feature === null)
22
- return false;
23
- return feature;
20
+ return this.data[key] === true;
24
21
  }
25
22
  }
26
23
  exports.LocalStorageBooleanProvider = LocalStorageBooleanProvider;
package/index.d.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  export { Feature, FeatureToggle, FeatureToggleBase, FeatureProvider, } from "./FeatureToggle";
2
2
  export { Clock, evaluate, parseTimestamp, systemClock } from "./evaluate";
3
- export { normaliseCollection, normaliseFeature } from "./mapping";
3
+ export { normaliseBooleans, normaliseCollection, normaliseFeature } from "./mapping";
package/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.normaliseFeature = exports.normaliseCollection = exports.systemClock = exports.parseTimestamp = exports.evaluate = exports.FeatureToggleBase = exports.FeatureToggle = void 0;
3
+ exports.normaliseFeature = exports.normaliseCollection = exports.normaliseBooleans = exports.systemClock = exports.parseTimestamp = exports.evaluate = exports.FeatureToggleBase = exports.FeatureToggle = void 0;
4
4
  var FeatureToggle_1 = require("./FeatureToggle");
5
5
  Object.defineProperty(exports, "FeatureToggle", { enumerable: true, get: function () { return FeatureToggle_1.FeatureToggle; } });
6
6
  Object.defineProperty(exports, "FeatureToggleBase", { enumerable: true, get: function () { return FeatureToggle_1.FeatureToggleBase; } });
@@ -9,5 +9,6 @@ Object.defineProperty(exports, "evaluate", { enumerable: true, get: function ()
9
9
  Object.defineProperty(exports, "parseTimestamp", { enumerable: true, get: function () { return evaluate_1.parseTimestamp; } });
10
10
  Object.defineProperty(exports, "systemClock", { enumerable: true, get: function () { return evaluate_1.systemClock; } });
11
11
  var mapping_1 = require("./mapping");
12
+ Object.defineProperty(exports, "normaliseBooleans", { enumerable: true, get: function () { return mapping_1.normaliseBooleans; } });
12
13
  Object.defineProperty(exports, "normaliseCollection", { enumerable: true, get: function () { return mapping_1.normaliseCollection; } });
13
14
  Object.defineProperty(exports, "normaliseFeature", { enumerable: true, get: function () { return mapping_1.normaliseFeature; } });
package/mapping.d.ts CHANGED
@@ -24,4 +24,12 @@ export declare function normaliseFeature(raw: RawFeature): Feature;
24
24
  * where `isEnabled("")` could reach it.
25
25
  */
26
26
  export declare function normaliseCollection(response: unknown): Record<string, Feature>;
27
+ /**
28
+ * Normalises a boolean-shape payload, `{ "myToggle": true }`.
29
+ *
30
+ * Only real booleans are kept (R29). Anything else is dropped, so its key
31
+ * reads as missing and therefore off. Keeping it and testing its truthiness
32
+ * would turn `"false"` on, since a non-empty string is truthy.
33
+ */
34
+ export declare function normaliseBooleans(response: unknown): Record<string, boolean>;
27
35
  export {};
package/mapping.js CHANGED
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.normaliseFeature = normaliseFeature;
4
4
  exports.normaliseCollection = normaliseCollection;
5
+ exports.normaliseBooleans = normaliseBooleans;
5
6
  /**
6
7
  * Reads a field by presence, not by truthiness.
7
8
  *
@@ -77,3 +78,21 @@ function normaliseCollection(response) {
77
78
  }
78
79
  return data;
79
80
  }
81
+ /**
82
+ * Normalises a boolean-shape payload, `{ "myToggle": true }`.
83
+ *
84
+ * Only real booleans are kept (R29). Anything else is dropped, so its key
85
+ * reads as missing and therefore off. Keeping it and testing its truthiness
86
+ * would turn `"false"` on, since a non-empty string is truthy.
87
+ */
88
+ function normaliseBooleans(response) {
89
+ if (response === null || typeof response !== 'object' || Array.isArray(response)) {
90
+ return {};
91
+ }
92
+ const data = {};
93
+ for (const [key, value] of Object.entries(response)) {
94
+ if (typeof value === 'boolean')
95
+ data[key] = value;
96
+ }
97
+ return data;
98
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tehw0lf/yaft",
3
- "version": "0.0.16",
3
+ "version": "0.0.17",
4
4
  "description": "YaFT - Feature Toggles using Go&PostgreSQL or any source!",
5
5
  "type": "commonjs",
6
6
  "main": "index.js",