@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.
- package/.github/workflows/build.yml +26 -0
- package/CLAUDE.md +138 -0
- package/LICENSE +21 -0
- package/README.md +103 -0
- package/jest.config.js +13 -0
- package/logo.svg +27 -0
- package/package.json +12 -2
- 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/src/test/decorator-behavior.spec.ts +359 -0
- package/src/test/error-handling.spec.ts +391 -0
- package/src/test/feature-data.spec.ts +102 -0
- package/src/test/integration.spec.ts +472 -0
- package/src/test/providers.spec.ts +218 -0
- package/src/test/test-boolean.json +1 -0
- package/src/test/test-feature.json +20 -0
- package/src/test/test-setup.ts +2 -0
- package/src/test/yaft.demo.spec.ts +53 -0
- package/tsconfig.json +10 -0
- package/tsconfig.spec.json +10 -0
- package/FeatureToggle.d.ts +0 -19
- package/FeatureToggle.js +0 -60
- package/index.d.ts +0 -1
- package/index.js +0 -6
|
@@ -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.
|
|
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
|
+
}
|