rps-flagforge 1.0.0 → 1.0.2
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/dist/bin/mergeSchema.d.ts +2 -0
- package/dist/bin/mergeSchema.js +2 -0
- package/dist/bin/mergeSchema.js.map +1 -0
- package/dist/index.d.mts +106 -0
- package/dist/index.d.ts +106 -0
- package/dist/index.js +228 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +194 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +2 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { PrismaClient } from '@custom-prisma/client';
|
|
2
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
3
|
+
import React, { ReactNode } from 'react';
|
|
4
|
+
|
|
5
|
+
type Operator = "equals" | "not_equals" | "gt" | "lt" | "contains" | "in";
|
|
6
|
+
interface Condition {
|
|
7
|
+
field: string;
|
|
8
|
+
operator: Operator;
|
|
9
|
+
value: any;
|
|
10
|
+
}
|
|
11
|
+
type RuleType = "USER_ID" | "USER_ATTRIBUTE" | "PERMISSION" | "GROUP" | "PERCENTAGE" | "CUSTOM";
|
|
12
|
+
interface FlagRule {
|
|
13
|
+
id: string;
|
|
14
|
+
type: RuleType;
|
|
15
|
+
priority: number;
|
|
16
|
+
conditions?: Condition | Condition[];
|
|
17
|
+
percentage?: number;
|
|
18
|
+
}
|
|
19
|
+
interface FeatureFlag {
|
|
20
|
+
key: string;
|
|
21
|
+
enabled: boolean;
|
|
22
|
+
rules: FlagRule[];
|
|
23
|
+
}
|
|
24
|
+
interface FlagContext {
|
|
25
|
+
userId?: string;
|
|
26
|
+
attributes?: Record<string, any>;
|
|
27
|
+
permissions?: string[];
|
|
28
|
+
groups?: string[];
|
|
29
|
+
}
|
|
30
|
+
interface FlagAdapter {
|
|
31
|
+
getFlag(key: string): Promise<FeatureFlag | null>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
declare class FlagEngine {
|
|
35
|
+
private adapter;
|
|
36
|
+
constructor(adapter: FlagAdapter);
|
|
37
|
+
isEnabled(key: string, context: FlagContext): Promise<boolean>;
|
|
38
|
+
private evaluateRule;
|
|
39
|
+
private percentageRollout;
|
|
40
|
+
private hash;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
declare class PrismaFlagAdapter implements FlagAdapter {
|
|
44
|
+
private prisma;
|
|
45
|
+
constructor(prisma: PrismaClient);
|
|
46
|
+
getFlag(key: string): Promise<FeatureFlag | null>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface MemoryAdapterOptions {
|
|
50
|
+
initialFlags?: FeatureFlag[];
|
|
51
|
+
}
|
|
52
|
+
declare class MemoryFlagAdapter implements FlagAdapter {
|
|
53
|
+
private flags;
|
|
54
|
+
constructor(options?: MemoryAdapterOptions);
|
|
55
|
+
getFlag(key: string): Promise<FeatureFlag | null>;
|
|
56
|
+
/**
|
|
57
|
+
* Add or update a flag
|
|
58
|
+
*/
|
|
59
|
+
setFlag(flag: FeatureFlag): void;
|
|
60
|
+
/**
|
|
61
|
+
* Remove a flag
|
|
62
|
+
*/
|
|
63
|
+
removeFlag(key: string): void;
|
|
64
|
+
/**
|
|
65
|
+
* Get all flags (useful for debugging)
|
|
66
|
+
*/
|
|
67
|
+
getAllFlags(): FeatureFlag[];
|
|
68
|
+
/**
|
|
69
|
+
* Clear all flags
|
|
70
|
+
*/
|
|
71
|
+
clear(): void;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
interface FlagProviderValue {
|
|
75
|
+
engine: FlagEngine;
|
|
76
|
+
context: FlagContext;
|
|
77
|
+
}
|
|
78
|
+
declare const FlagContextReact: React.Context<FlagProviderValue | null>;
|
|
79
|
+
interface FlagProviderProps {
|
|
80
|
+
engine: FlagEngine;
|
|
81
|
+
context: FlagContext;
|
|
82
|
+
children: ReactNode;
|
|
83
|
+
}
|
|
84
|
+
declare function FlagProvider({ engine, context, children }: FlagProviderProps): react_jsx_runtime.JSX.Element;
|
|
85
|
+
|
|
86
|
+
declare function useFlag(key: string): boolean;
|
|
87
|
+
|
|
88
|
+
interface WithFlagOptions {
|
|
89
|
+
fallback?: React.ReactNode;
|
|
90
|
+
}
|
|
91
|
+
declare function withFlag(flagKey: string, options?: WithFlagOptions): <P extends object>(WrappedComponent: React.ComponentType<P>) => React.FC<P>;
|
|
92
|
+
|
|
93
|
+
interface RequireFlagOptions {
|
|
94
|
+
key: string;
|
|
95
|
+
engine: FlagEngine;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Universal middleware/check function for any Node.js system
|
|
99
|
+
* @param options.key - Feature flag key
|
|
100
|
+
* @param options.engine - FlagEngine instance
|
|
101
|
+
*/
|
|
102
|
+
declare function requireFlag({ key, engine }: RequireFlagOptions): (context: FlagContext & {
|
|
103
|
+
user?: any;
|
|
104
|
+
}, next: () => void | Promise<void>, handleDenied?: () => void) => Promise<void>;
|
|
105
|
+
|
|
106
|
+
export { type Condition, type FeatureFlag, type FlagAdapter, type FlagContext, FlagContextReact, FlagEngine, FlagProvider, type FlagProviderValue, type FlagRule, type MemoryAdapterOptions, MemoryFlagAdapter, type Operator, PrismaFlagAdapter, type RequireFlagOptions, type RuleType, requireFlag, useFlag, withFlag };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { PrismaClient } from '@custom-prisma/client';
|
|
2
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
3
|
+
import React, { ReactNode } from 'react';
|
|
4
|
+
|
|
5
|
+
type Operator = "equals" | "not_equals" | "gt" | "lt" | "contains" | "in";
|
|
6
|
+
interface Condition {
|
|
7
|
+
field: string;
|
|
8
|
+
operator: Operator;
|
|
9
|
+
value: any;
|
|
10
|
+
}
|
|
11
|
+
type RuleType = "USER_ID" | "USER_ATTRIBUTE" | "PERMISSION" | "GROUP" | "PERCENTAGE" | "CUSTOM";
|
|
12
|
+
interface FlagRule {
|
|
13
|
+
id: string;
|
|
14
|
+
type: RuleType;
|
|
15
|
+
priority: number;
|
|
16
|
+
conditions?: Condition | Condition[];
|
|
17
|
+
percentage?: number;
|
|
18
|
+
}
|
|
19
|
+
interface FeatureFlag {
|
|
20
|
+
key: string;
|
|
21
|
+
enabled: boolean;
|
|
22
|
+
rules: FlagRule[];
|
|
23
|
+
}
|
|
24
|
+
interface FlagContext {
|
|
25
|
+
userId?: string;
|
|
26
|
+
attributes?: Record<string, any>;
|
|
27
|
+
permissions?: string[];
|
|
28
|
+
groups?: string[];
|
|
29
|
+
}
|
|
30
|
+
interface FlagAdapter {
|
|
31
|
+
getFlag(key: string): Promise<FeatureFlag | null>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
declare class FlagEngine {
|
|
35
|
+
private adapter;
|
|
36
|
+
constructor(adapter: FlagAdapter);
|
|
37
|
+
isEnabled(key: string, context: FlagContext): Promise<boolean>;
|
|
38
|
+
private evaluateRule;
|
|
39
|
+
private percentageRollout;
|
|
40
|
+
private hash;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
declare class PrismaFlagAdapter implements FlagAdapter {
|
|
44
|
+
private prisma;
|
|
45
|
+
constructor(prisma: PrismaClient);
|
|
46
|
+
getFlag(key: string): Promise<FeatureFlag | null>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface MemoryAdapterOptions {
|
|
50
|
+
initialFlags?: FeatureFlag[];
|
|
51
|
+
}
|
|
52
|
+
declare class MemoryFlagAdapter implements FlagAdapter {
|
|
53
|
+
private flags;
|
|
54
|
+
constructor(options?: MemoryAdapterOptions);
|
|
55
|
+
getFlag(key: string): Promise<FeatureFlag | null>;
|
|
56
|
+
/**
|
|
57
|
+
* Add or update a flag
|
|
58
|
+
*/
|
|
59
|
+
setFlag(flag: FeatureFlag): void;
|
|
60
|
+
/**
|
|
61
|
+
* Remove a flag
|
|
62
|
+
*/
|
|
63
|
+
removeFlag(key: string): void;
|
|
64
|
+
/**
|
|
65
|
+
* Get all flags (useful for debugging)
|
|
66
|
+
*/
|
|
67
|
+
getAllFlags(): FeatureFlag[];
|
|
68
|
+
/**
|
|
69
|
+
* Clear all flags
|
|
70
|
+
*/
|
|
71
|
+
clear(): void;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
interface FlagProviderValue {
|
|
75
|
+
engine: FlagEngine;
|
|
76
|
+
context: FlagContext;
|
|
77
|
+
}
|
|
78
|
+
declare const FlagContextReact: React.Context<FlagProviderValue | null>;
|
|
79
|
+
interface FlagProviderProps {
|
|
80
|
+
engine: FlagEngine;
|
|
81
|
+
context: FlagContext;
|
|
82
|
+
children: ReactNode;
|
|
83
|
+
}
|
|
84
|
+
declare function FlagProvider({ engine, context, children }: FlagProviderProps): react_jsx_runtime.JSX.Element;
|
|
85
|
+
|
|
86
|
+
declare function useFlag(key: string): boolean;
|
|
87
|
+
|
|
88
|
+
interface WithFlagOptions {
|
|
89
|
+
fallback?: React.ReactNode;
|
|
90
|
+
}
|
|
91
|
+
declare function withFlag(flagKey: string, options?: WithFlagOptions): <P extends object>(WrappedComponent: React.ComponentType<P>) => React.FC<P>;
|
|
92
|
+
|
|
93
|
+
interface RequireFlagOptions {
|
|
94
|
+
key: string;
|
|
95
|
+
engine: FlagEngine;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Universal middleware/check function for any Node.js system
|
|
99
|
+
* @param options.key - Feature flag key
|
|
100
|
+
* @param options.engine - FlagEngine instance
|
|
101
|
+
*/
|
|
102
|
+
declare function requireFlag({ key, engine }: RequireFlagOptions): (context: FlagContext & {
|
|
103
|
+
user?: any;
|
|
104
|
+
}, next: () => void | Promise<void>, handleDenied?: () => void) => Promise<void>;
|
|
105
|
+
|
|
106
|
+
export { type Condition, type FeatureFlag, type FlagAdapter, type FlagContext, FlagContextReact, FlagEngine, FlagProvider, type FlagProviderValue, type FlagRule, type MemoryAdapterOptions, MemoryFlagAdapter, type Operator, PrismaFlagAdapter, type RequireFlagOptions, type RuleType, requireFlag, useFlag, withFlag };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
FlagContextReact: () => FlagContextReact,
|
|
24
|
+
FlagEngine: () => FlagEngine,
|
|
25
|
+
FlagProvider: () => FlagProvider,
|
|
26
|
+
MemoryFlagAdapter: () => MemoryFlagAdapter,
|
|
27
|
+
PrismaFlagAdapter: () => PrismaFlagAdapter,
|
|
28
|
+
requireFlag: () => requireFlag,
|
|
29
|
+
useFlag: () => useFlag,
|
|
30
|
+
withFlag: () => withFlag
|
|
31
|
+
});
|
|
32
|
+
module.exports = __toCommonJS(index_exports);
|
|
33
|
+
|
|
34
|
+
// src/core/engine.ts
|
|
35
|
+
var FlagEngine = class {
|
|
36
|
+
constructor(adapter) {
|
|
37
|
+
this.adapter = adapter;
|
|
38
|
+
}
|
|
39
|
+
async isEnabled(key, context) {
|
|
40
|
+
const flag = await this.adapter.getFlag(key);
|
|
41
|
+
if (!flag || !flag.enabled) return false;
|
|
42
|
+
const sortedRules = flag.rules.sort((a, b) => b.priority - a.priority);
|
|
43
|
+
for (const rule of sortedRules) {
|
|
44
|
+
const result = await this.evaluateRule(rule, context);
|
|
45
|
+
if (result !== null) return result;
|
|
46
|
+
}
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
async evaluateRule(rule, context) {
|
|
50
|
+
switch (rule.type) {
|
|
51
|
+
case "USER_ID":
|
|
52
|
+
return rule.conditions.includes(context.userId);
|
|
53
|
+
case "PERMISSION":
|
|
54
|
+
return context.permissions?.includes(rule.conditions.permission) ?? false;
|
|
55
|
+
case "USER_ATTRIBUTE":
|
|
56
|
+
const { field, operator, value } = rule.conditions;
|
|
57
|
+
const userValue = context.attributes?.[field];
|
|
58
|
+
switch (operator) {
|
|
59
|
+
case "equals":
|
|
60
|
+
return userValue === value;
|
|
61
|
+
case "gt":
|
|
62
|
+
return userValue > value;
|
|
63
|
+
case "lt":
|
|
64
|
+
return userValue < value;
|
|
65
|
+
case "contains":
|
|
66
|
+
return userValue?.includes(value);
|
|
67
|
+
}
|
|
68
|
+
return false;
|
|
69
|
+
case "GROUP_MEMBERSHIP":
|
|
70
|
+
return context.groups?.includes(rule.conditions.groupId) ?? false;
|
|
71
|
+
case "PERCENTAGE":
|
|
72
|
+
return this.percentageRollout(context.userId, rule.percentage);
|
|
73
|
+
default:
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
percentageRollout(userId, percentage) {
|
|
78
|
+
const hash = this.hash(userId);
|
|
79
|
+
return hash % 100 < percentage;
|
|
80
|
+
}
|
|
81
|
+
hash(str) {
|
|
82
|
+
let hash = 0;
|
|
83
|
+
for (let i = 0; i < str.length; i++) {
|
|
84
|
+
hash = (hash << 5) - hash + str.charCodeAt(i);
|
|
85
|
+
hash |= 0;
|
|
86
|
+
}
|
|
87
|
+
return Math.abs(hash);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// src/adapters/prisma.ts
|
|
92
|
+
var PrismaFlagAdapter = class {
|
|
93
|
+
constructor(prisma) {
|
|
94
|
+
this.prisma = prisma;
|
|
95
|
+
}
|
|
96
|
+
async getFlag(key) {
|
|
97
|
+
const flag = await this.prisma.featureFlag.findUnique({
|
|
98
|
+
where: { key },
|
|
99
|
+
include: { rules: true }
|
|
100
|
+
// make sure 'rules' relation exists
|
|
101
|
+
});
|
|
102
|
+
if (!flag) return null;
|
|
103
|
+
return {
|
|
104
|
+
key: flag.key,
|
|
105
|
+
enabled: flag.enabled,
|
|
106
|
+
rules: flag.rules
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
// src/adapters/memory.ts
|
|
112
|
+
var MemoryFlagAdapter = class {
|
|
113
|
+
constructor(options) {
|
|
114
|
+
this.flags = /* @__PURE__ */ new Map();
|
|
115
|
+
if (options?.initialFlags) {
|
|
116
|
+
for (const flag of options.initialFlags) {
|
|
117
|
+
this.flags.set(flag.key, flag);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
async getFlag(key) {
|
|
122
|
+
return this.flags.get(key) ?? null;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Add or update a flag
|
|
126
|
+
*/
|
|
127
|
+
setFlag(flag) {
|
|
128
|
+
this.flags.set(flag.key, flag);
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Remove a flag
|
|
132
|
+
*/
|
|
133
|
+
removeFlag(key) {
|
|
134
|
+
this.flags.delete(key);
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Get all flags (useful for debugging)
|
|
138
|
+
*/
|
|
139
|
+
getAllFlags() {
|
|
140
|
+
return Array.from(this.flags.values());
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Clear all flags
|
|
144
|
+
*/
|
|
145
|
+
clear() {
|
|
146
|
+
this.flags.clear();
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
// src/react/FlagProvider.tsx
|
|
151
|
+
var import_react = require("react");
|
|
152
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
153
|
+
var FlagContextReact = (0, import_react.createContext)(null);
|
|
154
|
+
function FlagProvider({
|
|
155
|
+
engine,
|
|
156
|
+
context,
|
|
157
|
+
children
|
|
158
|
+
}) {
|
|
159
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FlagContextReact.Provider, { value: { engine, context }, children });
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// src/react/useFlag.ts
|
|
163
|
+
var import_react2 = require("react");
|
|
164
|
+
function useFlag(key) {
|
|
165
|
+
const ctx = (0, import_react2.useContext)(FlagContextReact);
|
|
166
|
+
if (!ctx) {
|
|
167
|
+
throw new Error("useFlag must be used inside FlagProvider");
|
|
168
|
+
}
|
|
169
|
+
const { engine, context } = ctx;
|
|
170
|
+
const [enabled, setEnabled] = (0, import_react2.useState)(false);
|
|
171
|
+
(0, import_react2.useEffect)(() => {
|
|
172
|
+
let cancelled = false;
|
|
173
|
+
engine.isEnabled(key, context).then((result) => {
|
|
174
|
+
if (!cancelled) {
|
|
175
|
+
setEnabled(result);
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
return () => {
|
|
179
|
+
cancelled = true;
|
|
180
|
+
};
|
|
181
|
+
}, [engine, key, context]);
|
|
182
|
+
return enabled;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// src/react/withFlag.tsx
|
|
186
|
+
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
187
|
+
function withFlag(flagKey, options) {
|
|
188
|
+
return function(WrappedComponent) {
|
|
189
|
+
const ComponentWithFlag = (props) => {
|
|
190
|
+
const enabled = useFlag(flagKey);
|
|
191
|
+
if (!enabled) {
|
|
192
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_jsx_runtime2.Fragment, { children: options?.fallback ?? null });
|
|
193
|
+
}
|
|
194
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(WrappedComponent, { ...props });
|
|
195
|
+
};
|
|
196
|
+
ComponentWithFlag.displayName = `withFlag(${WrappedComponent.displayName || WrappedComponent.name || "Component"})`;
|
|
197
|
+
return ComponentWithFlag;
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// src/node/middleware.ts
|
|
202
|
+
function requireFlag({ key, engine }) {
|
|
203
|
+
return async (context, next, handleDenied) => {
|
|
204
|
+
const allowed = await engine.isEnabled(key, {
|
|
205
|
+
userId: context.user?.id,
|
|
206
|
+
attributes: context.user,
|
|
207
|
+
permissions: context.user?.permissions,
|
|
208
|
+
groups: context.user?.groups ?? context.groups
|
|
209
|
+
});
|
|
210
|
+
if (!allowed) {
|
|
211
|
+
if (handleDenied) return handleDenied();
|
|
212
|
+
throw new Error(`Feature "${key}" is disabled for this user`);
|
|
213
|
+
}
|
|
214
|
+
return next();
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
218
|
+
0 && (module.exports = {
|
|
219
|
+
FlagContextReact,
|
|
220
|
+
FlagEngine,
|
|
221
|
+
FlagProvider,
|
|
222
|
+
MemoryFlagAdapter,
|
|
223
|
+
PrismaFlagAdapter,
|
|
224
|
+
requireFlag,
|
|
225
|
+
useFlag,
|
|
226
|
+
withFlag
|
|
227
|
+
});
|
|
228
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/core/engine.ts","../src/adapters/prisma.ts","../src/adapters/memory.ts","../src/react/FlagProvider.tsx","../src/react/useFlag.ts","../src/react/withFlag.tsx","../src/node/middleware.ts"],"sourcesContent":["// Types\r\nexport * from \"./core/types\" // exports both types & values (if any)\r\nexport type { FlagAdapter, FeatureFlag, FlagContext, FlagRule, RuleType, Condition, Operator } from \"./core/types\"\r\n\r\n// Engine\r\nexport { FlagEngine } from \"./core/engine\"\r\n\r\n// Adapters\r\nexport { PrismaFlagAdapter } from \"./adapters/prisma\"\r\nexport { MemoryFlagAdapter, MemoryAdapterOptions } from \"./adapters/memory\"\r\n\r\n// React\r\nexport { FlagProvider, FlagProviderValue, FlagContextReact } from \"./react/FlagProvider\"\r\nexport { useFlag } from \"./react/useFlag\"\r\nexport { withFlag } from \"./react/withFlag\"\r\n\r\n// Node\r\nexport { requireFlag, RequireFlagOptions } from \"./node/middleware\"\r\n","import { FlagAdapter, FlagContext } from \"./types\"\r\n\r\nexport class FlagEngine {\r\n constructor(private adapter: FlagAdapter) {}\r\n\r\n async isEnabled(key: string, context: FlagContext): Promise<boolean> {\r\n const flag = await this.adapter.getFlag(key)\r\n if (!flag || !flag.enabled) return false\r\n\r\n const sortedRules = flag.rules.sort((a: any, b: any) => b.priority - a.priority)\r\n\r\n for (const rule of sortedRules) {\r\n const result = await this.evaluateRule(rule, context)\r\n if (result !== null) return result\r\n }\r\n\r\n return false\r\n }\r\n\r\n private async evaluateRule(rule: any, context: FlagContext): Promise<boolean | null> {\r\n switch (rule.type) {\r\n case \"USER_ID\":\r\n return rule.conditions.includes(context.userId)\r\n\r\n case \"PERMISSION\":\r\n return context.permissions?.includes(rule.conditions.permission) ?? false\r\n\r\n case \"USER_ATTRIBUTE\":\r\n const { field, operator, value } = rule.conditions\r\n const userValue = context.attributes?.[field]\r\n\r\n switch (operator) {\r\n case \"equals\":\r\n return userValue === value\r\n case \"gt\":\r\n return userValue > value\r\n case \"lt\":\r\n return userValue < value\r\n case \"contains\":\r\n return userValue?.includes(value)\r\n }\r\n return false\r\n\r\n case \"GROUP_MEMBERSHIP\":\r\n return context.groups?.includes(rule.conditions.groupId) ?? false\r\n\r\n case \"PERCENTAGE\":\r\n return this.percentageRollout(context.userId!, rule.percentage)\r\n\r\n default:\r\n return null\r\n }\r\n }\r\n\r\n private percentageRollout(userId: string, percentage: number) {\r\n const hash = this.hash(userId)\r\n return (hash % 100) < percentage\r\n }\r\n\r\n private hash(str: string) {\r\n let hash = 0\r\n for (let i = 0; i < str.length; i++) {\r\n hash = (hash << 5) - hash + str.charCodeAt(i)\r\n hash |= 0\r\n }\r\n return Math.abs(hash)\r\n }\r\n}\r\n","import { PrismaClient } from \"@custom-prisma/client\" // ← match schema generator\r\nimport { FlagAdapter, FeatureFlag } from \"../core/types\"\r\n\r\nexport class PrismaFlagAdapter implements FlagAdapter {\r\n constructor(private prisma: PrismaClient) {}\r\n\r\n async getFlag(key: string): Promise<FeatureFlag | null> {\r\n const flag = await this.prisma.featureFlag.findUnique({\r\n where: { key },\r\n include: { rules: true } // make sure 'rules' relation exists\r\n })\r\n\r\n if (!flag) return null\r\n\r\n return {\r\n key: flag.key,\r\n enabled: flag.enabled,\r\n rules: flag.rules as any\r\n }\r\n }\r\n}\r\n","import type {\r\n FlagAdapter,\r\n FeatureFlag\r\n} from \"../core/types\"\r\n\r\nexport interface MemoryAdapterOptions {\r\n initialFlags?: FeatureFlag[]\r\n}\r\n\r\nexport class MemoryFlagAdapter implements FlagAdapter {\r\n private flags = new Map<string, FeatureFlag>()\r\n\r\n constructor(options?: MemoryAdapterOptions) {\r\n if (options?.initialFlags) {\r\n for (const flag of options.initialFlags) {\r\n this.flags.set(flag.key, flag)\r\n }\r\n }\r\n }\r\n\r\n async getFlag(key: string): Promise<FeatureFlag | null> {\r\n return this.flags.get(key) ?? null\r\n }\r\n\r\n /**\r\n * Add or update a flag\r\n */\r\n setFlag(flag: FeatureFlag): void {\r\n this.flags.set(flag.key, flag)\r\n }\r\n\r\n /**\r\n * Remove a flag\r\n */\r\n removeFlag(key: string): void {\r\n this.flags.delete(key)\r\n }\r\n\r\n /**\r\n * Get all flags (useful for debugging)\r\n */\r\n getAllFlags(): FeatureFlag[] {\r\n return Array.from(this.flags.values())\r\n }\r\n\r\n /**\r\n * Clear all flags\r\n */\r\n clear(): void {\r\n this.flags.clear()\r\n }\r\n}\r\n","import React, { createContext, ReactNode } from \"react\"\r\nimport type { FlagEngine } from \"../core/engine\"\r\nimport type { FlagContext } from \"../core/types\"\r\n\r\nexport interface FlagProviderValue {\r\n engine: FlagEngine\r\n context: FlagContext\r\n}\r\n\r\nexport const FlagContextReact =\r\n createContext<FlagProviderValue | null>(null)\r\n\r\ninterface FlagProviderProps {\r\n engine: FlagEngine\r\n context: FlagContext\r\n children: ReactNode\r\n}\r\n\r\nexport function FlagProvider({\r\n engine,\r\n context,\r\n children\r\n}: FlagProviderProps) {\r\n return (\r\n <FlagContextReact.Provider value={{ engine, context }}>\r\n {children}\r\n </FlagContextReact.Provider>\r\n )\r\n}\r\n","import { useContext, useEffect, useState } from \"react\"\r\nimport { FlagContextReact } from \"./FlagProvider\"\r\n\r\nexport function useFlag(key: string): boolean {\r\n const ctx = useContext(FlagContextReact)\r\n\r\n if (!ctx) {\r\n throw new Error(\"useFlag must be used inside FlagProvider\")\r\n }\r\n\r\n const { engine, context } = ctx\r\n const [enabled, setEnabled] = useState(false)\r\n\r\n useEffect(() => {\r\n let cancelled = false\r\n\r\n engine.isEnabled(key, context).then(result => {\r\n if (!cancelled) {\r\n setEnabled(result)\r\n }\r\n })\r\n\r\n return () => {\r\n cancelled = true\r\n }\r\n }, [engine, key, context])\r\n\r\n return enabled\r\n}\r\n","import React from \"react\"\r\nimport { useFlag } from \"./useFlag\"\r\n\r\ninterface WithFlagOptions {\r\n fallback?: React.ReactNode\r\n}\r\n\r\nexport function withFlag(\r\n flagKey: string,\r\n options?: WithFlagOptions\r\n) {\r\n return function <P extends object>(\r\n WrappedComponent: React.ComponentType<P>\r\n ) {\r\n const ComponentWithFlag: React.FC<P> = (props) => {\r\n const enabled = useFlag(flagKey)\r\n\r\n if (!enabled) {\r\n return <>{options?.fallback ?? null}</>\r\n }\r\n\r\n return <WrappedComponent {...props} />\r\n }\r\n\r\n ComponentWithFlag.displayName = `withFlag(${\r\n WrappedComponent.displayName ||\r\n WrappedComponent.name ||\r\n \"Component\"\r\n })`\r\n\r\n return ComponentWithFlag\r\n }\r\n}\r\n","import { FlagEngine } from \"../core/engine\"\r\nimport type { FlagContext } from \"../core/types\"\r\n\r\nexport interface RequireFlagOptions {\r\n key: string\r\n engine: FlagEngine\r\n}\r\n\r\n/**\r\n * Universal middleware/check function for any Node.js system\r\n * @param options.key - Feature flag key\r\n * @param options.engine - FlagEngine instance\r\n */\r\nexport function requireFlag({ key, engine }: RequireFlagOptions) {\r\n /**\r\n * context: object containing user info or any relevant attributes\r\n * next: function to call if feature is enabled\r\n * handleDenied: optional function to handle denial (default throws)\r\n */\r\n return async (\r\n context: FlagContext & { user?: any },\r\n next: () => void | Promise<void>,\r\n handleDenied?: () => void\r\n ) => {\r\n const allowed = await engine.isEnabled(key, {\r\n userId: context.user?.id,\r\n attributes: context.user,\r\n permissions: context.user?.permissions,\r\n groups: context.user?.groups ?? context.groups\r\n })\r\n\r\n if (!allowed) {\r\n if (handleDenied) return handleDenied()\r\n throw new Error(`Feature \"${key}\" is disabled for this user`)\r\n }\r\n\r\n return next()\r\n }\r\n}\r\n"],"mappings":"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","names":["import_react","import_jsx_runtime"]}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
// src/core/engine.ts
|
|
2
|
+
var FlagEngine = class {
|
|
3
|
+
constructor(adapter) {
|
|
4
|
+
this.adapter = adapter;
|
|
5
|
+
}
|
|
6
|
+
async isEnabled(key, context) {
|
|
7
|
+
const flag = await this.adapter.getFlag(key);
|
|
8
|
+
if (!flag || !flag.enabled) return false;
|
|
9
|
+
const sortedRules = flag.rules.sort((a, b) => b.priority - a.priority);
|
|
10
|
+
for (const rule of sortedRules) {
|
|
11
|
+
const result = await this.evaluateRule(rule, context);
|
|
12
|
+
if (result !== null) return result;
|
|
13
|
+
}
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
async evaluateRule(rule, context) {
|
|
17
|
+
switch (rule.type) {
|
|
18
|
+
case "USER_ID":
|
|
19
|
+
return rule.conditions.includes(context.userId);
|
|
20
|
+
case "PERMISSION":
|
|
21
|
+
return context.permissions?.includes(rule.conditions.permission) ?? false;
|
|
22
|
+
case "USER_ATTRIBUTE":
|
|
23
|
+
const { field, operator, value } = rule.conditions;
|
|
24
|
+
const userValue = context.attributes?.[field];
|
|
25
|
+
switch (operator) {
|
|
26
|
+
case "equals":
|
|
27
|
+
return userValue === value;
|
|
28
|
+
case "gt":
|
|
29
|
+
return userValue > value;
|
|
30
|
+
case "lt":
|
|
31
|
+
return userValue < value;
|
|
32
|
+
case "contains":
|
|
33
|
+
return userValue?.includes(value);
|
|
34
|
+
}
|
|
35
|
+
return false;
|
|
36
|
+
case "GROUP_MEMBERSHIP":
|
|
37
|
+
return context.groups?.includes(rule.conditions.groupId) ?? false;
|
|
38
|
+
case "PERCENTAGE":
|
|
39
|
+
return this.percentageRollout(context.userId, rule.percentage);
|
|
40
|
+
default:
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
percentageRollout(userId, percentage) {
|
|
45
|
+
const hash = this.hash(userId);
|
|
46
|
+
return hash % 100 < percentage;
|
|
47
|
+
}
|
|
48
|
+
hash(str) {
|
|
49
|
+
let hash = 0;
|
|
50
|
+
for (let i = 0; i < str.length; i++) {
|
|
51
|
+
hash = (hash << 5) - hash + str.charCodeAt(i);
|
|
52
|
+
hash |= 0;
|
|
53
|
+
}
|
|
54
|
+
return Math.abs(hash);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// src/adapters/prisma.ts
|
|
59
|
+
var PrismaFlagAdapter = class {
|
|
60
|
+
constructor(prisma) {
|
|
61
|
+
this.prisma = prisma;
|
|
62
|
+
}
|
|
63
|
+
async getFlag(key) {
|
|
64
|
+
const flag = await this.prisma.featureFlag.findUnique({
|
|
65
|
+
where: { key },
|
|
66
|
+
include: { rules: true }
|
|
67
|
+
// make sure 'rules' relation exists
|
|
68
|
+
});
|
|
69
|
+
if (!flag) return null;
|
|
70
|
+
return {
|
|
71
|
+
key: flag.key,
|
|
72
|
+
enabled: flag.enabled,
|
|
73
|
+
rules: flag.rules
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// src/adapters/memory.ts
|
|
79
|
+
var MemoryFlagAdapter = class {
|
|
80
|
+
constructor(options) {
|
|
81
|
+
this.flags = /* @__PURE__ */ new Map();
|
|
82
|
+
if (options?.initialFlags) {
|
|
83
|
+
for (const flag of options.initialFlags) {
|
|
84
|
+
this.flags.set(flag.key, flag);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
async getFlag(key) {
|
|
89
|
+
return this.flags.get(key) ?? null;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Add or update a flag
|
|
93
|
+
*/
|
|
94
|
+
setFlag(flag) {
|
|
95
|
+
this.flags.set(flag.key, flag);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Remove a flag
|
|
99
|
+
*/
|
|
100
|
+
removeFlag(key) {
|
|
101
|
+
this.flags.delete(key);
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Get all flags (useful for debugging)
|
|
105
|
+
*/
|
|
106
|
+
getAllFlags() {
|
|
107
|
+
return Array.from(this.flags.values());
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Clear all flags
|
|
111
|
+
*/
|
|
112
|
+
clear() {
|
|
113
|
+
this.flags.clear();
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
// src/react/FlagProvider.tsx
|
|
118
|
+
import { createContext } from "react";
|
|
119
|
+
import { jsx } from "react/jsx-runtime";
|
|
120
|
+
var FlagContextReact = createContext(null);
|
|
121
|
+
function FlagProvider({
|
|
122
|
+
engine,
|
|
123
|
+
context,
|
|
124
|
+
children
|
|
125
|
+
}) {
|
|
126
|
+
return /* @__PURE__ */ jsx(FlagContextReact.Provider, { value: { engine, context }, children });
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// src/react/useFlag.ts
|
|
130
|
+
import { useContext, useEffect, useState } from "react";
|
|
131
|
+
function useFlag(key) {
|
|
132
|
+
const ctx = useContext(FlagContextReact);
|
|
133
|
+
if (!ctx) {
|
|
134
|
+
throw new Error("useFlag must be used inside FlagProvider");
|
|
135
|
+
}
|
|
136
|
+
const { engine, context } = ctx;
|
|
137
|
+
const [enabled, setEnabled] = useState(false);
|
|
138
|
+
useEffect(() => {
|
|
139
|
+
let cancelled = false;
|
|
140
|
+
engine.isEnabled(key, context).then((result) => {
|
|
141
|
+
if (!cancelled) {
|
|
142
|
+
setEnabled(result);
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
return () => {
|
|
146
|
+
cancelled = true;
|
|
147
|
+
};
|
|
148
|
+
}, [engine, key, context]);
|
|
149
|
+
return enabled;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// src/react/withFlag.tsx
|
|
153
|
+
import { Fragment, jsx as jsx2 } from "react/jsx-runtime";
|
|
154
|
+
function withFlag(flagKey, options) {
|
|
155
|
+
return function(WrappedComponent) {
|
|
156
|
+
const ComponentWithFlag = (props) => {
|
|
157
|
+
const enabled = useFlag(flagKey);
|
|
158
|
+
if (!enabled) {
|
|
159
|
+
return /* @__PURE__ */ jsx2(Fragment, { children: options?.fallback ?? null });
|
|
160
|
+
}
|
|
161
|
+
return /* @__PURE__ */ jsx2(WrappedComponent, { ...props });
|
|
162
|
+
};
|
|
163
|
+
ComponentWithFlag.displayName = `withFlag(${WrappedComponent.displayName || WrappedComponent.name || "Component"})`;
|
|
164
|
+
return ComponentWithFlag;
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// src/node/middleware.ts
|
|
169
|
+
function requireFlag({ key, engine }) {
|
|
170
|
+
return async (context, next, handleDenied) => {
|
|
171
|
+
const allowed = await engine.isEnabled(key, {
|
|
172
|
+
userId: context.user?.id,
|
|
173
|
+
attributes: context.user,
|
|
174
|
+
permissions: context.user?.permissions,
|
|
175
|
+
groups: context.user?.groups ?? context.groups
|
|
176
|
+
});
|
|
177
|
+
if (!allowed) {
|
|
178
|
+
if (handleDenied) return handleDenied();
|
|
179
|
+
throw new Error(`Feature "${key}" is disabled for this user`);
|
|
180
|
+
}
|
|
181
|
+
return next();
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
export {
|
|
185
|
+
FlagContextReact,
|
|
186
|
+
FlagEngine,
|
|
187
|
+
FlagProvider,
|
|
188
|
+
MemoryFlagAdapter,
|
|
189
|
+
PrismaFlagAdapter,
|
|
190
|
+
requireFlag,
|
|
191
|
+
useFlag,
|
|
192
|
+
withFlag
|
|
193
|
+
};
|
|
194
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/engine.ts","../src/adapters/prisma.ts","../src/adapters/memory.ts","../src/react/FlagProvider.tsx","../src/react/useFlag.ts","../src/react/withFlag.tsx","../src/node/middleware.ts"],"sourcesContent":["import { FlagAdapter, FlagContext } from \"./types\"\r\n\r\nexport class FlagEngine {\r\n constructor(private adapter: FlagAdapter) {}\r\n\r\n async isEnabled(key: string, context: FlagContext): Promise<boolean> {\r\n const flag = await this.adapter.getFlag(key)\r\n if (!flag || !flag.enabled) return false\r\n\r\n const sortedRules = flag.rules.sort((a: any, b: any) => b.priority - a.priority)\r\n\r\n for (const rule of sortedRules) {\r\n const result = await this.evaluateRule(rule, context)\r\n if (result !== null) return result\r\n }\r\n\r\n return false\r\n }\r\n\r\n private async evaluateRule(rule: any, context: FlagContext): Promise<boolean | null> {\r\n switch (rule.type) {\r\n case \"USER_ID\":\r\n return rule.conditions.includes(context.userId)\r\n\r\n case \"PERMISSION\":\r\n return context.permissions?.includes(rule.conditions.permission) ?? false\r\n\r\n case \"USER_ATTRIBUTE\":\r\n const { field, operator, value } = rule.conditions\r\n const userValue = context.attributes?.[field]\r\n\r\n switch (operator) {\r\n case \"equals\":\r\n return userValue === value\r\n case \"gt\":\r\n return userValue > value\r\n case \"lt\":\r\n return userValue < value\r\n case \"contains\":\r\n return userValue?.includes(value)\r\n }\r\n return false\r\n\r\n case \"GROUP_MEMBERSHIP\":\r\n return context.groups?.includes(rule.conditions.groupId) ?? false\r\n\r\n case \"PERCENTAGE\":\r\n return this.percentageRollout(context.userId!, rule.percentage)\r\n\r\n default:\r\n return null\r\n }\r\n }\r\n\r\n private percentageRollout(userId: string, percentage: number) {\r\n const hash = this.hash(userId)\r\n return (hash % 100) < percentage\r\n }\r\n\r\n private hash(str: string) {\r\n let hash = 0\r\n for (let i = 0; i < str.length; i++) {\r\n hash = (hash << 5) - hash + str.charCodeAt(i)\r\n hash |= 0\r\n }\r\n return Math.abs(hash)\r\n }\r\n}\r\n","import { PrismaClient } from \"@custom-prisma/client\" // ← match schema generator\r\nimport { FlagAdapter, FeatureFlag } from \"../core/types\"\r\n\r\nexport class PrismaFlagAdapter implements FlagAdapter {\r\n constructor(private prisma: PrismaClient) {}\r\n\r\n async getFlag(key: string): Promise<FeatureFlag | null> {\r\n const flag = await this.prisma.featureFlag.findUnique({\r\n where: { key },\r\n include: { rules: true } // make sure 'rules' relation exists\r\n })\r\n\r\n if (!flag) return null\r\n\r\n return {\r\n key: flag.key,\r\n enabled: flag.enabled,\r\n rules: flag.rules as any\r\n }\r\n }\r\n}\r\n","import type {\r\n FlagAdapter,\r\n FeatureFlag\r\n} from \"../core/types\"\r\n\r\nexport interface MemoryAdapterOptions {\r\n initialFlags?: FeatureFlag[]\r\n}\r\n\r\nexport class MemoryFlagAdapter implements FlagAdapter {\r\n private flags = new Map<string, FeatureFlag>()\r\n\r\n constructor(options?: MemoryAdapterOptions) {\r\n if (options?.initialFlags) {\r\n for (const flag of options.initialFlags) {\r\n this.flags.set(flag.key, flag)\r\n }\r\n }\r\n }\r\n\r\n async getFlag(key: string): Promise<FeatureFlag | null> {\r\n return this.flags.get(key) ?? null\r\n }\r\n\r\n /**\r\n * Add or update a flag\r\n */\r\n setFlag(flag: FeatureFlag): void {\r\n this.flags.set(flag.key, flag)\r\n }\r\n\r\n /**\r\n * Remove a flag\r\n */\r\n removeFlag(key: string): void {\r\n this.flags.delete(key)\r\n }\r\n\r\n /**\r\n * Get all flags (useful for debugging)\r\n */\r\n getAllFlags(): FeatureFlag[] {\r\n return Array.from(this.flags.values())\r\n }\r\n\r\n /**\r\n * Clear all flags\r\n */\r\n clear(): void {\r\n this.flags.clear()\r\n }\r\n}\r\n","import React, { createContext, ReactNode } from \"react\"\r\nimport type { FlagEngine } from \"../core/engine\"\r\nimport type { FlagContext } from \"../core/types\"\r\n\r\nexport interface FlagProviderValue {\r\n engine: FlagEngine\r\n context: FlagContext\r\n}\r\n\r\nexport const FlagContextReact =\r\n createContext<FlagProviderValue | null>(null)\r\n\r\ninterface FlagProviderProps {\r\n engine: FlagEngine\r\n context: FlagContext\r\n children: ReactNode\r\n}\r\n\r\nexport function FlagProvider({\r\n engine,\r\n context,\r\n children\r\n}: FlagProviderProps) {\r\n return (\r\n <FlagContextReact.Provider value={{ engine, context }}>\r\n {children}\r\n </FlagContextReact.Provider>\r\n )\r\n}\r\n","import { useContext, useEffect, useState } from \"react\"\r\nimport { FlagContextReact } from \"./FlagProvider\"\r\n\r\nexport function useFlag(key: string): boolean {\r\n const ctx = useContext(FlagContextReact)\r\n\r\n if (!ctx) {\r\n throw new Error(\"useFlag must be used inside FlagProvider\")\r\n }\r\n\r\n const { engine, context } = ctx\r\n const [enabled, setEnabled] = useState(false)\r\n\r\n useEffect(() => {\r\n let cancelled = false\r\n\r\n engine.isEnabled(key, context).then(result => {\r\n if (!cancelled) {\r\n setEnabled(result)\r\n }\r\n })\r\n\r\n return () => {\r\n cancelled = true\r\n }\r\n }, [engine, key, context])\r\n\r\n return enabled\r\n}\r\n","import React from \"react\"\r\nimport { useFlag } from \"./useFlag\"\r\n\r\ninterface WithFlagOptions {\r\n fallback?: React.ReactNode\r\n}\r\n\r\nexport function withFlag(\r\n flagKey: string,\r\n options?: WithFlagOptions\r\n) {\r\n return function <P extends object>(\r\n WrappedComponent: React.ComponentType<P>\r\n ) {\r\n const ComponentWithFlag: React.FC<P> = (props) => {\r\n const enabled = useFlag(flagKey)\r\n\r\n if (!enabled) {\r\n return <>{options?.fallback ?? null}</>\r\n }\r\n\r\n return <WrappedComponent {...props} />\r\n }\r\n\r\n ComponentWithFlag.displayName = `withFlag(${\r\n WrappedComponent.displayName ||\r\n WrappedComponent.name ||\r\n \"Component\"\r\n })`\r\n\r\n return ComponentWithFlag\r\n }\r\n}\r\n","import { FlagEngine } from \"../core/engine\"\r\nimport type { FlagContext } from \"../core/types\"\r\n\r\nexport interface RequireFlagOptions {\r\n key: string\r\n engine: FlagEngine\r\n}\r\n\r\n/**\r\n * Universal middleware/check function for any Node.js system\r\n * @param options.key - Feature flag key\r\n * @param options.engine - FlagEngine instance\r\n */\r\nexport function requireFlag({ key, engine }: RequireFlagOptions) {\r\n /**\r\n * context: object containing user info or any relevant attributes\r\n * next: function to call if feature is enabled\r\n * handleDenied: optional function to handle denial (default throws)\r\n */\r\n return async (\r\n context: FlagContext & { user?: any },\r\n next: () => void | Promise<void>,\r\n handleDenied?: () => void\r\n ) => {\r\n const allowed = await engine.isEnabled(key, {\r\n userId: context.user?.id,\r\n attributes: context.user,\r\n permissions: context.user?.permissions,\r\n groups: context.user?.groups ?? context.groups\r\n })\r\n\r\n if (!allowed) {\r\n if (handleDenied) return handleDenied()\r\n throw new Error(`Feature \"${key}\" is disabled for this user`)\r\n }\r\n\r\n return next()\r\n }\r\n}\r\n"],"mappings":"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","names":["jsx"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rps-flagforge",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "A Multi-Core Feature Flag Package with support for React & NodeJS",
|
|
5
5
|
"main": "dist/index.cjs.js",
|
|
6
6
|
"module": "dist/index.esm.js",
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
"devDependencies": {
|
|
29
29
|
"@types/express": "^5.0.6",
|
|
30
30
|
"@types/node": "^25.2.3",
|
|
31
|
+
"@types/prompts": "^2.4.9",
|
|
31
32
|
"@types/react": "^19.2.14",
|
|
32
33
|
"prisma": "^6.14.0",
|
|
33
34
|
"tsup": "^8.5.1",
|