Skip to content

Commit 2c1527e

Browse files
authored
Merge pull request #13 from ReforgeHQ/mfaga-better-typing
feat: introduce type safety
2 parents 6f787c6 + eb63946 commit 2c1527e

20 files changed

+367
-190
lines changed

.eslintrc.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ module.exports = {
2525
"@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }],
2626
"@typescript-eslint/no-empty-function": "off",
2727
"@typescript-eslint/no-explicit-any": "off",
28+
"no-shadow": "off", // disable base rule for TypeScript
29+
"@typescript-eslint/no-shadow": ["error"],
2830
"import/prefer-default-export": "off",
2931
"import/extensions": [
3032
"error",

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Changelog
22

3+
## 0.0.0-pre.6 - 2025-09-30
4+
5+
- Support extendable typescript types w/typegen from cli
6+
37
## 0.0.0-pre.5 - 2025-09-24
48

59
- Use reforge.com endpoints

index.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
1-
import { reforge, Reforge, ReforgeBootstrap } from "./src/reforge";
1+
import { reforge, Reforge, ReforgeInitParams, ReforgeBootstrap } from "./src/reforge";
22
import { Config } from "./src/config";
33
import Context from "./src/context";
44
// eslint-disable-next-line @typescript-eslint/no-var-requires
55
const { version } = require("./package.json");
66

7-
export { reforge, Reforge, Config, Context, version };
7+
export { reforge, Reforge, ReforgeInitParams, Config, Context, version };
88

99
export { ReforgeBootstrap };
1010

11-
export type { Duration } from "./src/configValue";
12-
export type { default as ConfigValue } from "./src/configValue";
13-
export type { default as ContextValue } from "./src/contextValue";
11+
export type { ConfigValue } from "./src/config";
12+
export type {
13+
Duration,
14+
ContextValue,
15+
Contexts,
16+
TypedFrontEndConfigurationRaw,
17+
FrontEndConfigurationRaw,
18+
} from "./src/types";
1419
export type { CollectContextModeType } from "./src/loader";

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"packageManager": "yarn@4.9.2",
33
"name": "@reforge-com/javascript",
4-
"version": "0.0.0-pre.5",
4+
"version": "0.0.0-pre.6",
55
"description": "Feature Flags & Dynamic Configuration as a Service",
66
"main": "dist/index.cjs",
77
"module": "dist/index.mjs",

src/config.ts

Lines changed: 138 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,136 @@
1-
import ConfigKey from "./configKey";
2-
import ConfigValue from "./configValue";
1+
import { ReforgeLogLevel } from "./logger";
2+
import { TypedFrontEndConfigurationRaw, ConfigEvaluationMetadata } from "./types";
33

4-
export type RawConfigWithoutTypes = { [key: string]: any };
5-
6-
export type ConfigEvaluationMetadata = {
7-
configRowIndex: number;
8-
conditionalValueIndex: number;
9-
type: string;
10-
configId: string;
11-
};
4+
export type RawConfigWithoutTypes = Record<string, any>;
125

136
type APIKeyMetadata = {
147
id: string | number;
158
};
169

10+
// TODO: Why is this definition different from the one in ./types.ts?
1711
type Duration = {
1812
definition: string;
1913
millis: number;
2014
};
2115

22-
type Value = {
23-
[key: string]: number | string | string[] | boolean | Duration;
24-
};
16+
export interface IntRange {
17+
/** if empty treat as Number.MIN_VALUE. Inclusive */
18+
start?: bigint | undefined;
19+
/** if empty treat as Number.MAX_VALUE. Exclusive */
20+
end?: bigint | undefined;
21+
}
22+
23+
export enum ProvidedSource {
24+
EnvVar = "ENV_VAR",
25+
}
26+
export interface Provided {
27+
source?: ProvidedSource | undefined;
28+
/** eg MY_ENV_VAR */
29+
lookup?: string | undefined;
30+
}
31+
32+
export enum SchemaType {
33+
UNKNOWN = 0,
34+
ZOD = 1,
35+
JSON_SCHEMA = 2,
36+
}
37+
38+
export interface Schema {
39+
schema: string;
40+
schemaType: SchemaType;
41+
}
42+
43+
export interface WeightedValue {
44+
/** out of 1000 */
45+
weight: number;
46+
// eslint-disable-next-line no-use-before-define
47+
value: ConfigValue | undefined;
48+
}
49+
50+
export enum LimitResponse_LimitPolicyNames {
51+
SecondlyRolling = 1,
52+
MinutelyRolling = 3,
53+
HourlyRolling = 5,
54+
DailyRolling = 7,
55+
MonthlyRolling = 8,
56+
Infinite = 9,
57+
YearlyRolling = 10,
58+
}
59+
60+
export enum LimitDefinition_SafetyLevel {
61+
L4_BEST_EFFORT = 4,
62+
L5_BOMBPROOF = 5,
63+
}
64+
65+
export interface LimitDefinition {
66+
policyName: LimitResponse_LimitPolicyNames;
67+
limit: number;
68+
burst: number;
69+
accountId: number;
70+
lastModified: number;
71+
returnable: boolean;
72+
/** [default = L4_BEST_EFFORT]; // Overridable by request */
73+
safetyLevel: LimitDefinition_SafetyLevel;
74+
}
75+
export interface WeightedValues {
76+
weightedValues: WeightedValue[];
77+
hashByPropertyName?: string | undefined;
78+
}
79+
80+
export type ConfigValue =
81+
| {
82+
int: number | undefined;
83+
}
84+
| {
85+
string: string | undefined;
86+
}
87+
| {
88+
bytes: Buffer | undefined;
89+
}
90+
| {
91+
double: number | undefined;
92+
}
93+
| {
94+
bool: boolean | undefined;
95+
}
96+
| {
97+
weightedValues?: WeightedValues | undefined;
98+
}
99+
| {
100+
limitDefinition?: LimitDefinition | undefined;
101+
}
102+
| {
103+
logLevel: ReforgeLogLevel | undefined;
104+
}
105+
| {
106+
stringList: string[] | undefined;
107+
}
108+
| {
109+
intRange: IntRange | undefined;
110+
}
111+
| {
112+
provided: Provided | undefined;
113+
}
114+
| {
115+
duration: Duration | undefined;
116+
}
117+
| {
118+
json: string | undefined;
119+
}
120+
| {
121+
schema: Schema | undefined;
122+
}
123+
| {
124+
/** don't log or telemetry this value */
125+
confidential: boolean | undefined;
126+
}
127+
| {
128+
/** key name to decrypt with */
129+
decryptWith: string | undefined;
130+
};
25131

26132
type Evaluation = {
27-
value: Value;
133+
value: ConfigValue;
28134
configEvaluationMetadata: {
29135
configRowIndex: string | number;
30136
conditionalValueIndex: string | number;
@@ -47,7 +153,11 @@ const parseRawMetadata = (metadata: any) => ({
47153
configId: metadata.id,
48154
});
49155

50-
const valueFor = (value: Value, type: string, key: string): ConfigValue => {
156+
const valueFor = <K extends keyof TypedFrontEndConfigurationRaw>(
157+
value: ConfigValue,
158+
type: keyof ConfigValue,
159+
key: K
160+
): TypedFrontEndConfigurationRaw[K] => {
51161
const rawValue = value[type];
52162

53163
switch (type) {
@@ -77,7 +187,7 @@ export const parseEvaluationPayload = (payload: EvaluationPayload) => {
77187
Object.keys(payload.evaluations).forEach((key) => {
78188
const evaluation = payload.evaluations[key];
79189

80-
const type = Object.keys(evaluation.value)[0];
190+
const type = Object.keys(evaluation.value)[0] as keyof ConfigValue;
81191

82192
// eslint-disable-next-line no-use-before-define
83193
configs[key] = new Config(
@@ -98,30 +208,32 @@ const parseRawConfigWithoutTypes = (payload: RawConfigWithoutTypes) => {
98208
// eslint-disable-next-line no-use-before-define
99209
const configs = {} as { [key: string]: Config };
100210
Object.keys(payload).forEach((key) => {
101-
const type = typeof payload[key];
211+
const type = typeof payload[key] as keyof ConfigValue;
102212
// eslint-disable-next-line no-use-before-define
103213
configs[key] = new Config(key, valueFor({ [type]: payload[key] }, type, key), type);
104214
});
105215

106216
return configs;
107217
};
108218

109-
export class Config {
110-
key: ConfigKey;
219+
export class Config<
220+
K extends keyof TypedFrontEndConfigurationRaw = keyof TypedFrontEndConfigurationRaw,
221+
> {
222+
key: K;
111223

112-
value: ConfigValue;
224+
value: TypedFrontEndConfigurationRaw[K];
113225

114-
rawValue: Value | undefined;
226+
rawValue: ConfigValue | undefined;
115227

116-
type: string;
228+
type: keyof ConfigValue;
117229

118230
configEvaluationMetadata: ConfigEvaluationMetadata | undefined;
119231

120232
constructor(
121-
key: ConfigKey,
122-
value: ConfigValue,
123-
type: string,
124-
rawValue?: Value,
233+
key: K,
234+
value: TypedFrontEndConfigurationRaw[K],
235+
type: keyof ConfigValue,
236+
rawValue?: ConfigValue,
125237
metadata?: ConfigEvaluationMetadata
126238
) {
127239
this.key = key;

src/configKey.ts

Lines changed: 0 additions & 3 deletions
This file was deleted.

src/configValue.ts

Lines changed: 0 additions & 8 deletions
This file was deleted.

src/context.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
1-
import Key from "./key";
2-
import ContextValue from "./contextValue";
1+
import { Contexts, ContextValue } from "./types";
32
import base64Encode from "./base64Encode";
43

5-
export type Contexts = { [key: Key]: Record<string, ContextValue> };
6-
74
const isEqual = (a: Contexts, b: Contexts) => {
85
const aKeys = Object.keys(a);
96
const bKeys = Object.keys(b);

src/contextValue.ts

Lines changed: 0 additions & 3 deletions
This file was deleted.

src/evaluationSummaryAggregator.test.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
1-
import {
2-
massageSelectedValue,
3-
massageConfigForTelemetry,
4-
ConfigEvaluationCounter,
5-
} from "./evaluationSummaryAggregator";
6-
import { Config, parseEvaluationPayload, EvaluationPayload } from "./config";
1+
import { massageSelectedValue, massageConfigForTelemetry } from "./evaluationSummaryAggregator";
2+
import { Config, parseEvaluationPayload, EvaluationPayload, ConfigValue } from "./config";
3+
import { ConfigEvaluationCounter } from "./types";
74

85
describe("massageSelectedValue", () => {
96
[
@@ -21,7 +18,11 @@ describe("massageSelectedValue", () => {
2118
[{ stringList: ["a", "b", "c"] }, { values: ["a", "b", "c"] }],
2219
].forEach(([value, expected]) => {
2320
it(`should massage ${JSON.stringify(value)} to ${JSON.stringify(expected)}`, () => {
24-
const config = new Config("key", Object.values(value)[0], Object.keys(value)[0]);
21+
const config = new Config(
22+
"key",
23+
Object.values(value)[0],
24+
Object.keys(value)[0] as keyof ConfigValue
25+
);
2526
const massagedValue = massageSelectedValue(config);
2627

2728
expect(massagedValue).toEqual(expected);

0 commit comments

Comments
 (0)