Skip to content

Commit e68f99d

Browse files
committed
feat: made feature flag decorator generics strongly typed
1 parent df4054a commit e68f99d

File tree

2 files changed

+85
-37
lines changed

2 files changed

+85
-37
lines changed

valhalla/jawn/src/controllers/public/heliconeSqlController.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ export interface ClickHouseTableColumn {
4646
ttl_expression?: string;
4747
}
4848

49+
// TODO DRY these interfaces
4950
export interface ExecuteSqlRequest {
5051
sql: string;
5152
}

valhalla/jawn/src/decorators/featureFlag.ts

Lines changed: 84 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,91 @@
11
import "reflect-metadata";
22
import { checkFeatureFlag } from "../lib/utils/featureFlags";
3-
import { isError } from "../packages/common/result";
3+
import { isError, Result, err } from "../packages/common/result";
44
import { Controller } from "tsoa";
55
import { JawnAuthenticatedRequest } from "../types/request";
66

77
const FEATURE_FLAG_METADATA_KEY = Symbol("featureFlags");
88

9+
/**
10+
* Base constraint for error types - can be string enums, string literals, or any serializable type
11+
*/
12+
export type ErrorType = string | number | symbol;
13+
914
/**
1015
* Generic error formatter type that can work with any error system
1116
*/
12-
export type ErrorFormatter<T = any> = (flag: string) => {
17+
export type ErrorFormatter<TError extends ErrorType = string> = (flag: string) => {
1318
message: string;
1419
statusCode: number;
15-
error?: T;
20+
error?: TError;
1621
};
1722

1823
/**
1924
* Options for feature flag decorator with generic error type
2025
*/
21-
export interface FeatureFlagOptions<T = any> {
22-
errorFormatter?: ErrorFormatter<T>;
26+
export interface FeatureFlagOptions<TError extends ErrorType = string> {
27+
errorFormatter?: ErrorFormatter<TError>;
2328
}
2429

2530
/**
2631
* Metadata stored for feature flags
2732
*/
28-
export interface FeatureFlagMetadata<T = any> {
33+
export interface FeatureFlagMetadata<TError extends ErrorType = string> {
2934
flags: string[];
30-
options?: FeatureFlagOptions<T>;
35+
options?: FeatureFlagOptions<TError>;
3136
}
3237

33-
export function RequireFeatureFlag<T = any>(
38+
/**
39+
* Type for controller methods that can be decorated
40+
* These methods take JawnAuthenticatedRequest and return Result
41+
*/
42+
type ControllerMethod = (
43+
this: Controller,
44+
...args: any[] // Could include @Body, @Request, @Query params etc.
45+
) => Promise<Result<any, string>>;
46+
47+
/**
48+
* Generic decorator to require feature flag(s) for a controller method.
49+
* Can be used with any error system by providing a custom error formatter.
50+
*
51+
* @param flag - The feature flag name to check
52+
* @param options - Optional configuration for error handling
53+
*
54+
* @example
55+
* // Using default error format
56+
* @RequireFeatureFlag("my-feature")
57+
*
58+
* @example
59+
* // Using with HQL error system (strongly typed)
60+
* @RequireFeatureFlag<HqlErrorCode>(HQL_FEATURE_FLAG, {
61+
* errorFormatter: (flag) => ({
62+
* message: `[${HqlErrorCode.FEATURE_NOT_ENABLED}] Feature not enabled`,
63+
* statusCode: 403,
64+
* error: HqlErrorCode.FEATURE_NOT_ENABLED
65+
* })
66+
* })
67+
*
68+
* @example
69+
* // Using with string literal types
70+
* type MyError = "FEATURE_DISABLED" | "NOT_AVAILABLE";
71+
* @RequireFeatureFlag<MyError>("my-feature", {
72+
* errorFormatter: (flag) => ({
73+
* message: `Feature ${flag} is disabled`,
74+
* statusCode: 403,
75+
* error: "FEATURE_DISABLED"
76+
* })
77+
* })
78+
*/
79+
export function RequireFeatureFlag<TError extends ErrorType = string>(
3480
flag: string,
35-
options?: FeatureFlagOptions<T>
81+
options?: FeatureFlagOptions<TError>
3682
): MethodDecorator {
3783
return function (
38-
target: any,
84+
target: object,
3985
propertyKey: string | symbol,
4086
descriptor: PropertyDescriptor
41-
) {
42-
const existingMetadata: FeatureFlagMetadata<T> = Reflect.getMetadata(
87+
): PropertyDescriptor {
88+
const existingMetadata: FeatureFlagMetadata<TError> = Reflect.getMetadata(
4389
FEATURE_FLAG_METADATA_KEY,
4490
target,
4591
propertyKey
@@ -57,25 +103,26 @@ export function RequireFeatureFlag<T = any>(
57103
propertyKey
58104
);
59105

60-
const originalMethod = descriptor.value;
106+
const originalMethod = descriptor.value as ControllerMethod;
61107

62108
descriptor.value = async function (
63109
this: Controller,
64110
...args: any[]
65-
) {
111+
): Promise<Result<any, string>> {
112+
// Find the JawnAuthenticatedRequest in the arguments
66113
const request = args.find(
67-
(arg) => arg && typeof arg === "object" && "authParams" in arg
68-
) as JawnAuthenticatedRequest | undefined;
114+
(arg): arg is JawnAuthenticatedRequest =>
115+
arg !== null &&
116+
typeof arg === "object" &&
117+
"authParams" in arg
118+
);
69119

70120
if (!request?.authParams?.organizationId) {
71121
this.setStatus(401);
72-
return {
73-
error: "Authentication required",
74-
data: null,
75-
};
122+
return err("Authentication required");
76123
}
77124

78-
const metadata: FeatureFlagMetadata<T> = Reflect.getMetadata(
125+
const metadata: FeatureFlagMetadata<TError> = Reflect.getMetadata(
79126
FEATURE_FLAG_METADATA_KEY,
80127
target,
81128
propertyKey
@@ -97,10 +144,7 @@ export function RequireFeatureFlag<T = any>(
97144
};
98145

99146
this.setStatus(errorInfo.statusCode);
100-
return {
101-
error: errorInfo.message,
102-
data: null,
103-
};
147+
return err(errorInfo.message);
104148
}
105149
}
106150

@@ -123,17 +167,17 @@ export function RequireFeatureFlag<T = any>(
123167
* errorFormatter: (flag) => ({ ... })
124168
* })
125169
*/
126-
export function RequireFeatureFlags<T = any>(
170+
export function RequireFeatureFlags<TError extends ErrorType = string>(
127171
flags: string[],
128-
options?: FeatureFlagOptions<T>
172+
options?: FeatureFlagOptions<TError>
129173
): MethodDecorator {
130174
return function (
131-
target: any,
175+
target: object,
132176
propertyKey: string | symbol,
133177
descriptor: PropertyDescriptor
134-
) {
178+
): PropertyDescriptor {
135179
flags.forEach((flag) => {
136-
RequireFeatureFlag<T>(flag, options)(target, propertyKey, descriptor);
180+
RequireFeatureFlag<TError>(flag, options)(target, propertyKey, descriptor);
137181
});
138182
return descriptor;
139183
};
@@ -149,11 +193,11 @@ export function RequireFeatureFlags<T = any>(
149193
* 403
150194
* );
151195
*/
152-
export function createErrorFormatter<T>(
153-
errorSelector: (flag: string) => T,
154-
messageFormatter: (error: T, flag: string) => string,
196+
export function createErrorFormatter<TError extends ErrorType>(
197+
errorSelector: (flag: string) => TError,
198+
messageFormatter: (error: TError, flag: string) => string,
155199
statusCode: number = 403
156-
): ErrorFormatter<T> {
200+
): ErrorFormatter<TError> {
157201
return (flag: string) => {
158202
const error = errorSelector(flag);
159203
return {
@@ -164,9 +208,12 @@ export function createErrorFormatter<T>(
164208
};
165209
}
166210

167-
export function getFeatureFlagMetadata<T = any>(
168-
target: any,
211+
/**
212+
* Retrieves feature flag metadata from a decorated method
213+
*/
214+
export function getFeatureFlagMetadata<TError extends ErrorType = string>(
215+
target: object,
169216
propertyKey: string | symbol
170-
): FeatureFlagMetadata<T> | undefined {
217+
): FeatureFlagMetadata<TError> | undefined {
171218
return Reflect.getMetadata(FEATURE_FLAG_METADATA_KEY, target, propertyKey);
172219
}

0 commit comments

Comments
 (0)