@tehw0lf/yaft 0.0.1 → 0.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/.github/workflows/build.yml +25 -0
- package/LICENSE +21 -0
- package/README.md +103 -0
- package/logo.svg +27 -0
- package/package.json +7 -5
- package/src/FeatureToggle.ts +80 -0
- package/src/examples/ApiServiceBooleanProvider.ts +44 -0
- package/src/examples/ApiServiceFeatureProvider.ts +49 -0
- package/src/examples/LocalStorageBooleanProvider.ts +25 -0
- package/src/examples/LocalStorageFeatureProvider.ts +30 -0
- package/src/index.ts +6 -0
- package/tsconfig.json +10 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
name: build and publish pipeline
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- main
|
|
7
|
+
pull_request:
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
build:
|
|
11
|
+
name: external workflow
|
|
12
|
+
uses: tehw0lf/workflows/.github/workflows/build-test-publish.yml@main
|
|
13
|
+
permissions:
|
|
14
|
+
actions: write
|
|
15
|
+
contents: write
|
|
16
|
+
packages: write
|
|
17
|
+
with:
|
|
18
|
+
tool: "npm"
|
|
19
|
+
build_branch: "run build"
|
|
20
|
+
build_main: "run build"
|
|
21
|
+
event_name: ${{ github.event_name }}
|
|
22
|
+
artifact_path: "dist"
|
|
23
|
+
library_path: "dist"
|
|
24
|
+
secrets:
|
|
25
|
+
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Robert Weyres
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# YaFT for TypeScript
|
|
2
|
+
|
|
3
|
+
<div align="center">
|
|
4
|
+
<img src="./logo.svg" alt="YaFT Logo" width="140">
|
|
5
|
+
</div>
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
This provides a client for YaFT which aims to bring simple feature toggles for Methods and Classes to Typescript.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
`npm install --save @tehw0lf/yaft`
|
|
16
|
+
|
|
17
|
+
## Initialization
|
|
18
|
+
|
|
19
|
+
To be able to use YaFT, implement and set a `FeatureProvider` on the abstract Base Class, or copy and adapt one of the example providers.
|
|
20
|
+
|
|
21
|
+
In this example a local json file with the `Feature` data type is used, but in theory any data type can be used since the Provider implements the isEnabled function which can be overridden as necessary.
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import { FeatureToggleBase } from "@tehw0lf/yaft";
|
|
25
|
+
import { LocalStorageFeatureProvider } from "./provider";
|
|
26
|
+
|
|
27
|
+
FeatureToggleBase.featureProvider = new LocalStorageFeatureProvider(
|
|
28
|
+
"./test-feature.json"
|
|
29
|
+
);
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
This repo contains examples for two provider types: API Providers for `boolean` and `Feature` and LocalStorage Providers for `boolean` and `Feature`. The APi providers are designed to work with the default Go implementation of YaFT, but can be implemented through their interface:
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
export interface FeatureProvider<T> {
|
|
36
|
+
apiUrl?: string;
|
|
37
|
+
baseUUID?: string;
|
|
38
|
+
data: Record<string, T>;
|
|
39
|
+
initConfig(configPathOrUrl: string): void;
|
|
40
|
+
isEnabled(key: string): boolean;
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Usage
|
|
45
|
+
|
|
46
|
+
To manage anything with a feature toggle, decorate it with the feature's key:
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
import { FeatureToggle } from "@tehw0lf/yaft";
|
|
50
|
+
|
|
51
|
+
@FeatureToggle("myKey")
|
|
52
|
+
class MyClass {}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
import { FeatureToggle } from "@tehw0lf/yaft";
|
|
57
|
+
|
|
58
|
+
@FeatureToggle("myKey")
|
|
59
|
+
myMethod() {}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
If the feature is enabled, the original class/method will be used.
|
|
63
|
+
|
|
64
|
+
If the feature is disabled, a fallback can be provided:
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
import { FeatureToggle } from "@tehw0lf/yaft";
|
|
68
|
+
|
|
69
|
+
@FeatureToggle("myKey", MyFallbackClass)
|
|
70
|
+
class MyClass {}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
import { FeatureToggle } from "@tehw0lf/yaft";
|
|
75
|
+
|
|
76
|
+
@FeatureToggle("myKey", myFallbackMethod)
|
|
77
|
+
myMethod() {}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Otherwise, or if the feature does not exist, the class/method gets replaced with an empty object/an empty function.
|
|
81
|
+
|
|
82
|
+
## Collection Hash
|
|
83
|
+
|
|
84
|
+
The default YaFT API provides a collection hash to efficiently check whether the locally cached features are still up to date. See API provider examples for details.
|
|
85
|
+
This can be scheduled to automatically update feature data in the background.
|
|
86
|
+
|
|
87
|
+
## Feature Data Type
|
|
88
|
+
|
|
89
|
+
The default data type for YaFT is `Feature`. A feature has a key string, a value string representing a boolean and optional activeAt and disabledAt date strings.
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
export type Feature = {
|
|
93
|
+
key: string;
|
|
94
|
+
value: string;
|
|
95
|
+
activeAt: string;
|
|
96
|
+
disabledAt: string;
|
|
97
|
+
};
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
# Licenses
|
|
101
|
+
|
|
102
|
+
- Code: MIT License
|
|
103
|
+
- Logo/Branding: All rights reserved
|
package/logo.svg
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<svg viewBox="0 0 60 60" stroke="" fill="grey" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<title>YaFT Logo</title>
|
|
3
|
+
<desc>Yet Another Feature Toggle - Copyright 2025 tehw0lf</desc>
|
|
4
|
+
<metadata>
|
|
5
|
+
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
|
|
6
|
+
<rdf:Description>
|
|
7
|
+
<dc:creator>tehw0lf</dc:creator>
|
|
8
|
+
<dc:date>2025</dc:date>
|
|
9
|
+
<dc:rights>Copyright 2025 tehw0lf. All rights reserved.</dc:rights>
|
|
10
|
+
</rdf:Description>
|
|
11
|
+
</rdf:RDF>
|
|
12
|
+
</metadata>
|
|
13
|
+
<!-- Y -->
|
|
14
|
+
<path d="M5,5 L30,28" stroke="grey" stroke-width="4" fill="none" />
|
|
15
|
+
<path d="M55,5 L30,28" stroke="grey" stroke-width="4" fill="none" />
|
|
16
|
+
<!-- A -->
|
|
17
|
+
<rect x="19" y="17.5" width="22" height="3" />
|
|
18
|
+
<!-- F -->
|
|
19
|
+
<rect x="27.5" y="40" width="15" height="5" />
|
|
20
|
+
<!-- T -->
|
|
21
|
+
<rect x="27.5" y="25" width="5" height="35" />
|
|
22
|
+
<rect x="10" y="25" width="40" height="5" />
|
|
23
|
+
<!-- cross -->
|
|
24
|
+
<path d="M5,20 L20,5" stroke="red" stroke-width="4" fill="none" />
|
|
25
|
+
<!-- check -->
|
|
26
|
+
<path d="M40.8,20.8, L30.8,10.8" stroke="green" stroke-width="4" fill="none" />
|
|
27
|
+
</svg>
|
package/package.json
CHANGED
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tehw0lf/yaft",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3",
|
|
4
4
|
"description": "YaFT - Feature Toggles using Go&PostgreSQL or any source!",
|
|
5
|
-
"files": [
|
|
6
|
-
"dist"
|
|
7
|
-
],
|
|
8
5
|
"type": "commonjs",
|
|
9
6
|
"scripts": {
|
|
10
|
-
"build": "tsc && npm pkg delete devDependencies && cp package.json dist/
|
|
7
|
+
"build": "tsc && npm pkg delete devDependencies && cp package.json README.md logo.svg dist/"
|
|
11
8
|
},
|
|
12
9
|
"repository": {
|
|
13
10
|
"type": "git",
|
|
@@ -27,5 +24,10 @@
|
|
|
27
24
|
"homepage": "https://github.com/tehw0lf/yaft-ts#readme",
|
|
28
25
|
"dependencies": {
|
|
29
26
|
"reflect-metadata": "^0.2.2"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@types/node": "^22.14.1",
|
|
30
|
+
"axios": "^1.8.4",
|
|
31
|
+
"typescript": "^4.2.3"
|
|
30
32
|
}
|
|
31
33
|
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import "reflect-metadata";
|
|
2
|
+
|
|
3
|
+
class EmptyClass {
|
|
4
|
+
constructor() {}
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export type Feature = {
|
|
8
|
+
key: string;
|
|
9
|
+
value: string;
|
|
10
|
+
activeAt: string;
|
|
11
|
+
disabledAt: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export interface FeatureProvider<T> {
|
|
15
|
+
apiUrl?: string;
|
|
16
|
+
baseUUID?: string;
|
|
17
|
+
data: Record<string, T>;
|
|
18
|
+
getCollectionHash?(configPathOrUrl: string): void;
|
|
19
|
+
getConfig(configPathOrUrl: string): void;
|
|
20
|
+
isEnabled(key: string): boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function FeatureToggle(key: string, fallback?: any) {
|
|
24
|
+
if (!FeatureToggleBase.featureProvider) {
|
|
25
|
+
throw new Error("FeatureToggleProvider not set");
|
|
26
|
+
}
|
|
27
|
+
const isEnabled = FeatureToggleBase.featureProvider.isEnabled(key);
|
|
28
|
+
return (
|
|
29
|
+
target: any,
|
|
30
|
+
propertyKey?: string,
|
|
31
|
+
descriptor?: PropertyDescriptor
|
|
32
|
+
) => {
|
|
33
|
+
if (propertyKey && descriptor) {
|
|
34
|
+
// Method
|
|
35
|
+
const originalMethod = descriptor.value;
|
|
36
|
+
|
|
37
|
+
descriptor.value = function (...args: any[]) {
|
|
38
|
+
if (isEnabled) {
|
|
39
|
+
return originalMethod.apply(this, args);
|
|
40
|
+
} else {
|
|
41
|
+
return fallback !== undefined
|
|
42
|
+
? fallback.apply(this, args)
|
|
43
|
+
: (() => {}).apply(this, args);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
return descriptor;
|
|
47
|
+
} else {
|
|
48
|
+
// Class
|
|
49
|
+
const originalConstructor = target;
|
|
50
|
+
let newConstructor: any;
|
|
51
|
+
|
|
52
|
+
if (isEnabled) {
|
|
53
|
+
newConstructor = originalConstructor;
|
|
54
|
+
} else {
|
|
55
|
+
newConstructor = fallback !== undefined ? fallback : EmptyClass;
|
|
56
|
+
if (fallback) newConstructor.__proto__ = fallback.__proto__;
|
|
57
|
+
|
|
58
|
+
if (newConstructor === EmptyClass) {
|
|
59
|
+
Object.getOwnPropertyNames(originalConstructor.prototype).forEach(
|
|
60
|
+
(name: string) => {
|
|
61
|
+
if (name === "constructor") return;
|
|
62
|
+
if (typeof originalConstructor.prototype[name] === "function")
|
|
63
|
+
newConstructor.prototype[name] = () => {};
|
|
64
|
+
if (
|
|
65
|
+
originalConstructor.prototype[name][Symbol.toStringTag] ===
|
|
66
|
+
"AsyncFunction"
|
|
67
|
+
)
|
|
68
|
+
newConstructor.prototype[name] = async () => {};
|
|
69
|
+
}
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return newConstructor;
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export abstract class FeatureToggleBase {
|
|
79
|
+
static featureProvider: FeatureProvider<any>;
|
|
80
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import axios from "axios";
|
|
2
|
+
|
|
3
|
+
import { FeatureProvider } from "../FeatureToggle";
|
|
4
|
+
|
|
5
|
+
export class ApiServiceBooleanProvider implements FeatureProvider<boolean> {
|
|
6
|
+
apiUrl: string;
|
|
7
|
+
baseUUID: string;
|
|
8
|
+
data: Record<string, boolean>;
|
|
9
|
+
collectionHash = "";
|
|
10
|
+
|
|
11
|
+
constructor(apiUrl: string, baseUUID: string) {
|
|
12
|
+
this.apiUrl = apiUrl;
|
|
13
|
+
this.baseUUID = baseUUID;
|
|
14
|
+
this.getCollectionHash(`${this.apiUrl}/collectionHash/${this.baseUUID}`);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async getCollectionHash(configPathOrUrl: string): Promise<void> {
|
|
18
|
+
try {
|
|
19
|
+
const response = await axios.get(configPathOrUrl);
|
|
20
|
+
const newHash = response.data.value;
|
|
21
|
+
if (this.collectionHash !== newHash) {
|
|
22
|
+
this.collectionHash = newHash;
|
|
23
|
+
await this.getConfig(`${this.apiUrl}/features/${this.baseUUID}`);
|
|
24
|
+
}
|
|
25
|
+
} catch (error) {
|
|
26
|
+
console.error("Failed to fetch feature toggle from API:", error);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async getConfig(configPathOrUrl: string): Promise<void> {
|
|
31
|
+
try {
|
|
32
|
+
const response = await axios.get(configPathOrUrl);
|
|
33
|
+
this.data = response.data.value;
|
|
34
|
+
} catch (error) {
|
|
35
|
+
console.error("Failed to fetch feature toggle from API:", error);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
isEnabled(key: string): boolean {
|
|
40
|
+
const feature = this.data[key];
|
|
41
|
+
if (feature === undefined || feature === null) return false;
|
|
42
|
+
return feature;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import axios from "axios";
|
|
2
|
+
|
|
3
|
+
import { Feature, FeatureProvider } from "../FeatureToggle";
|
|
4
|
+
|
|
5
|
+
export class ApiServiceFeatureProvider implements FeatureProvider<Feature> {
|
|
6
|
+
apiUrl: string;
|
|
7
|
+
baseUUID: string;
|
|
8
|
+
data: Record<string, Feature> = {};
|
|
9
|
+
collectionHash = "";
|
|
10
|
+
|
|
11
|
+
constructor(apiUrl: string, baseUUID: string) {
|
|
12
|
+
this.apiUrl = apiUrl;
|
|
13
|
+
this.baseUUID = baseUUID;
|
|
14
|
+
this.getCollectionHash(`${this.apiUrl}/collectionHash/${this.baseUUID}`);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async getCollectionHash(configPathOrUrl: string): Promise<void> {
|
|
18
|
+
try {
|
|
19
|
+
const response = await axios.get(configPathOrUrl);
|
|
20
|
+
const newHash = response.data.value;
|
|
21
|
+
if (this.collectionHash !== newHash) {
|
|
22
|
+
this.collectionHash = newHash;
|
|
23
|
+
await this.getConfig(`${this.apiUrl}/features/${this.baseUUID}`);
|
|
24
|
+
}
|
|
25
|
+
} catch (error) {
|
|
26
|
+
console.error("Failed to fetch feature toggle from API:", error);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async getConfig(configPathOrUrl: string): Promise<void> {
|
|
31
|
+
try {
|
|
32
|
+
const response = await axios.get(configPathOrUrl);
|
|
33
|
+
this.data = response.data.value;
|
|
34
|
+
} catch (error) {
|
|
35
|
+
console.error("Failed to fetch feature toggle from API:", error);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
isEnabled(key: string): boolean {
|
|
40
|
+
const feature = this.data[key];
|
|
41
|
+
|
|
42
|
+
if (feature === undefined || feature === null) return false;
|
|
43
|
+
|
|
44
|
+
if (Date.now() >= Date.parse(feature.disabledAt)) return false;
|
|
45
|
+
if (Date.now() >= Date.parse(feature.activeAt)) return true;
|
|
46
|
+
|
|
47
|
+
return JSON.parse(feature.value);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { FeatureProvider } from "../FeatureToggle";
|
|
2
|
+
|
|
3
|
+
export class LocalStorageBooleanProvider implements FeatureProvider<boolean> {
|
|
4
|
+
data: Record<string, boolean> = {};
|
|
5
|
+
|
|
6
|
+
constructor(configPath: string) {
|
|
7
|
+
this.getConfig(configPath);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
getConfig(configPathOrUrl: string): void {
|
|
11
|
+
try {
|
|
12
|
+
const configData = require(configPathOrUrl);
|
|
13
|
+
this.data = configData;
|
|
14
|
+
} catch (error) {
|
|
15
|
+
console.error("Failed to load configuration from local file:", error);
|
|
16
|
+
this.data = {};
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
isEnabled(key: string): boolean {
|
|
21
|
+
const feature = this.data[key];
|
|
22
|
+
if (feature === undefined || feature === null) return false;
|
|
23
|
+
return feature;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Feature, FeatureProvider } from "../FeatureToggle";
|
|
2
|
+
|
|
3
|
+
export class LocalStorageFeatureProvider implements FeatureProvider<Feature> {
|
|
4
|
+
data: Record<string, Feature> = {};
|
|
5
|
+
|
|
6
|
+
constructor(configPath: string) {
|
|
7
|
+
this.getConfig(configPath);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
getConfig(configPathOrUrl: string): void {
|
|
11
|
+
try {
|
|
12
|
+
const configData = require(configPathOrUrl);
|
|
13
|
+
this.data = configData;
|
|
14
|
+
} catch (error) {
|
|
15
|
+
console.error("Failed to load configuration from local file:", error);
|
|
16
|
+
this.data = {};
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
isEnabled(key: string): boolean {
|
|
21
|
+
const feature = this.data[key] as Feature;
|
|
22
|
+
|
|
23
|
+
if (feature === undefined || feature === null) return false;
|
|
24
|
+
|
|
25
|
+
if (Date.now() >= Date.parse(feature.disabledAt)) return false;
|
|
26
|
+
if (Date.now() >= Date.parse(feature.activeAt)) return true;
|
|
27
|
+
|
|
28
|
+
return JSON.parse(feature.value);
|
|
29
|
+
}
|
|
30
|
+
}
|
package/src/index.ts
ADDED