@tehw0lf/yaft 0.0.2 → 0.0.4

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.
@@ -0,0 +1,26 @@
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
+ test: "run test"
20
+ build_branch: "run build"
21
+ build_main: "run build"
22
+ event_name: ${{ github.event_name }}
23
+ artifact_path: "dist"
24
+ library_path: "dist"
25
+ secrets:
26
+ NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
package/CLAUDE.md ADDED
@@ -0,0 +1,138 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## Project Overview
6
+
7
+ YaFT (Yet another Feature Toggle) is a TypeScript library that provides decorator-based feature toggles for classes and methods. It integrates with backend services (Go/PostgreSQL by default) or local data sources to enable/disable features at runtime.
8
+
9
+ ## Development Commands
10
+
11
+ ### Building
12
+ ```bash
13
+ npm run build # Compile TypeScript and prepare package for publishing
14
+ ```
15
+
16
+ ### Testing
17
+ ```bash
18
+ npm test # Run Jest tests with ts-jest transform
19
+ npx jest # Alternative test command
20
+ npx jest --watch # Run tests in watch mode
21
+ npx jest --coverage # Run tests with coverage report
22
+ ```
23
+
24
+ ### Running Single Tests
25
+ ```bash
26
+ npx jest yaft.spec.ts # Run specific test file
27
+ npx jest -t "test name" # Run specific test by name pattern
28
+ ```
29
+
30
+ ## Architecture
31
+
32
+ ### Core Components
33
+
34
+ **FeatureToggle Decorator (`src/FeatureToggle.ts`)**
35
+ - Main decorator function that can be applied to classes or methods
36
+ - Evaluates feature state at decoration time using the configured provider
37
+ - Returns fallback implementations when features are disabled
38
+
39
+ **FeatureToggleBase Class**
40
+ - Static container for the global `featureProvider` instance
41
+ - Must be configured before using any `@FeatureToggle` decorators
42
+
43
+ **FeatureProvider Interface**
44
+ - Defines contract for data sources: `isEnabled(key: string): boolean`
45
+ - Optional API integration with `apiUrl`, `baseUUID`, and collection hash support
46
+ - Flexible data structure support via generic type parameter
47
+
48
+ ### Provider Implementations
49
+
50
+ **API Providers** (`src/examples/ApiService*Provider.ts`)
51
+ - Integrate with YaFT Go backend via HTTP API
52
+ - Support collection hash for efficient cache invalidation
53
+ - Handle both boolean and Feature data types
54
+ - Include error handling for network failures
55
+
56
+ **Local Storage Providers** (`src/examples/LocalStorage*Provider.ts`)
57
+ - Load configuration from local JSON files using `require()`
58
+ - Support both boolean and Feature data types
59
+ - Fallback to empty configuration on file load errors
60
+
61
+ ### Feature Data Model
62
+
63
+ ```typescript
64
+ type Feature = {
65
+ key: string; // Unique feature identifier
66
+ value: string; // Boolean value as string
67
+ activeAt: string; // ISO date when feature becomes active
68
+ disabledAt: string; // ISO date when feature gets disabled
69
+ }
70
+ ```
71
+
72
+ Features support time-based activation/deactivation logic evaluated at runtime.
73
+
74
+ ### Decorator Behavior
75
+
76
+ **Method Decoration:**
77
+ - Replaces method implementation based on feature state
78
+ - Supports fallback method when feature is disabled
79
+ - Returns empty function if no fallback provided
80
+
81
+ **Class Decoration:**
82
+ - Replaces entire class constructor and prototype
83
+ - Supports fallback class when feature is disabled
84
+ - Creates EmptyClass with stub methods if no fallback provided
85
+ - Preserves async method signatures in EmptyClass stubs
86
+
87
+ ## Usage Patterns
88
+
89
+ ### Initialization
90
+ ```typescript
91
+ import { FeatureToggleBase } from "@tehw0lf/yaft";
92
+ import { LocalStorageFeatureProvider } from "./examples/LocalStorageFeatureProvider";
93
+
94
+ FeatureToggleBase.featureProvider = new LocalStorageFeatureProvider("./config.json");
95
+ ```
96
+
97
+ ### Method Toggle
98
+ ```typescript
99
+ @FeatureToggle("myFeature", fallbackMethod)
100
+ myMethod() {
101
+ // Implementation when feature is enabled
102
+ }
103
+ ```
104
+
105
+ ### Class Toggle
106
+ ```typescript
107
+ @FeatureToggle("myFeature", FallbackClass)
108
+ class MyClass {
109
+ // Implementation when feature is enabled
110
+ }
111
+ ```
112
+
113
+ ## Testing Strategy
114
+
115
+ Tests use LocalStorageFeatureProvider with test configuration files (`src/test/test-*.json`). The test suite demonstrates:
116
+ - Class decoration with fallback classes
117
+ - Method decoration with and without fallbacks
118
+ - Async method handling
119
+ - Provider configuration and feature evaluation
120
+
121
+ ## Build Configuration
122
+
123
+ - **TypeScript**: CommonJS output targeting ES2022
124
+ - **Decorators**: Experimental decorators enabled for `@FeatureToggle` syntax
125
+ - **Output**: Compiled to `dist/yaft/` with declaration files
126
+ - **Package**: Strips devDependencies during build process
127
+
128
+ ## Dependencies
129
+
130
+ - **Runtime**: `reflect-metadata` for decorator metadata
131
+ - **Development**: TypeScript, Jest, ts-jest for testing
132
+ - **Optional**: `axios` for API provider implementations
133
+
134
+ ## Pre-commit Validation
135
+
136
+ ```bash
137
+ npm test && npm run build
138
+ ```
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/jest.config.js ADDED
@@ -0,0 +1,13 @@
1
+ /** @type {import('ts-jest').JestConfigWithTsJest} **/
2
+ module.exports = {
3
+ displayName: 'yaft',
4
+ setupFilesAfterEnv: [
5
+ '<rootDir>/src/test/test-setup.ts'
6
+ ],
7
+ globals: {},
8
+ coverageDirectory: 'coverage/yaft',
9
+ testEnvironment: "node",
10
+ transform: {
11
+ "^.+\.tsx?$": ["ts-jest",{}],
12
+ },
13
+ };
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,10 +1,11 @@
1
1
  {
2
2
  "name": "@tehw0lf/yaft",
3
- "version": "0.0.2",
3
+ "version": "0.0.4",
4
4
  "description": "YaFT - Feature Toggles using Go&PostgreSQL or any source!",
5
5
  "type": "commonjs",
6
6
  "scripts": {
7
- "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/",
8
+ "test": "jest"
8
9
  },
9
10
  "repository": {
10
11
  "type": "git",
@@ -24,5 +25,14 @@
24
25
  "homepage": "https://github.com/tehw0lf/yaft-ts#readme",
25
26
  "dependencies": {
26
27
  "reflect-metadata": "^0.2.2"
28
+ },
29
+ "devDependencies": {
30
+ "@types/jest": "^29.5.14",
31
+ "@types/node": "^22.14.1",
32
+ "axios": "^1.8.4",
33
+ "jest": "^29.7.0",
34
+ "ts-jest": "^29.3.2",
35
+ "ts-node": "^10.9.2",
36
+ "typescript": "^4.2.3"
27
37
  }
28
38
  }
@@ -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
@@ -0,0 +1,6 @@
1
+ export {
2
+ Feature,
3
+ FeatureToggle,
4
+ FeatureToggleBase,
5
+ FeatureProvider,
6
+ } from "./FeatureToggle";