Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
153 changes: 153 additions & 0 deletions packages/common/src/env.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import faker from "faker";
import { getEnv, isString, isInteger } from "./env";
describe("the module for picking up env variables", () => {
it("is a function does not return null", () => {
const env = getEnv({});
expect(env).not.toBeNull();
});

it("is a function that does not return undefined", () => {
const env = getEnv({});
expect(env).not.toBeUndefined();
});

it("throws if one of its input is not in the env", () => {
expect(() => {
getEnv({
NOT_IN_ENV: isString,
});
}).toThrow();
});

it("throws with appropriate error if key is not in the environment", () => {
expect(() => {
getEnv({
NOT_IN_ENV: isString,
});
}).toThrowError(`NOT_IN_ENV was expected to be in environment`);
});

const withEnvironment =
<T>(environment: T, action: (environment: T) => void) =>
() => {
const previousEnv = process.env;
process.env = environment as any as NodeJS.ProcessEnv;

action(environment);
process.env = previousEnv;
};

describe("withEnvironment helper", () => {
it(
"does change process.env",
withEnvironment(
{
TEST: "value",
},
(environment) => {
expect(process.env.TEST).toEqual("value");
}
)
);

it(
"does return test key in callback",
withEnvironment(
{
KEY_NAME: "TEST_VALUE",
},
(environment) => {
expect(environment.KEY_NAME).toEqual("TEST_VALUE");
}
)
);

it(
"returns environment exactly equal to process.env",
withEnvironment(
{
KEY: "VALUE",
},
(environment) => {
expect(environment).toEqual(process.env);
}
)
);

it(
"does not have other keys in env than the ones specified",
withEnvironment({ KEY: "VALUE" }, (environment) => {
expect(process.env).toEqual({ KEY: "VALUE" });
})
);
it(
"does not have any other keys in object than the ones specified",
withEnvironment(
{
KEY: "VALUE",
},
(environment) => {
expect(environment).toEqual({ KEY: "VALUE" });
}
)
);

it(
"Does return an object with the KEY from input",
withEnvironment({ KEY: "VALUE" }, () => {
const env = getEnv({ KEY: isString });
expect(env.KEY).toBeDefined();
expect(env.KEY).toEqual("VALUE");
})
);

it(
"Accepts key with type of Validator",
withEnvironment({ API_KEY: "some_key" }, () => {
const env = getEnv({ API_KEY: isString });
expect(env.API_KEY).toBeDefined();
})
);

it(
"Does not accept an argument that's not a validator",
withEnvironment({ PORT: faker.datatype.number() }, () => {
expect(() => {
//@ts-expect-error
getEnv({ PORT: "NOT_A_VALIDATOR" });
}).toThrow(); //will throw because "NOT_A_VALIDATOR" is not a function
})
);

it("Does return a value with the expeted type", () =>
withEnvironment({ PORT: faker.datatype.number() }, () => {
const acceptingNumber = (input: number) => {};
const env = getEnv({ PORT: isInteger });

//NOTE: if this does not compile, consider test failing
acceptingNumber(env.PORT);
}));

describe("isInteger validator", () => {
it(
"Fails if is not number",
withEnvironment({ PORT: faker.lorem.word(10) }, (environment) => {
expect(() => {
getEnv({ PORT: isInteger });
}).toThrowError(
`PORT did not match its validator. Found: ${environment.PORT}`
);
})
);

it(
"Does not fail if it is an integer",
withEnvironment({ PORT: faker.datatype.number() }, () => {
expect(() => {
getEnv({ PORT: isInteger });
}).not.toThrow();
})
);
});
});
});
43 changes: 43 additions & 0 deletions packages/common/src/env.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
type PossibleTypes = string | number;
type Validator = <T extends PossibleTypes>(
value: any
) => boolean /* value is T */;
//const a: Validator = (p: any): p is string => true;
//a("thing");

export const getEnv = <G extends string, T extends { [key in G]: Validator }>(
input: T
) =>
Object.keys(input)
.map((key: G) => {
const actualValue = process.env[key];
if (!actualValue) throw `${key} was expected to be in environment`;

const validator = input[key];
if (!validator(actualValue))
throw `${key} did not match its validator. Found: ${actualValue}`;

return {
[key as G]: actualValue,
};
})
.reduce((a, b) => ({
...a,
...b,
}));

export const isString = (input: any): boolean /* input is string */ =>
typeof input === "string" && isNaN(parseInt(input));

const env = getEnv({
KEY: isString,
});

export const isInteger = (input: any): boolean /* input is number */ =>
!isNaN(parseInt(input));

const isFloat = (input: any) => parseFloat(input) !== NaN;
const isBetween = (min: number, max: number) => (input: string) => {
const number = parseFloat(input);
return number !== NaN || (number < max && number > min);
};
126 changes: 126 additions & 0 deletions packages/common/src/renamed_env_before_merge.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { logger } from "./logger/logger";

/**
* IF this works, create new module
*/
/* type Specification =
| "string"
| "number"
| string[]
| [min: number, max: number];
const _env = <T extends string>(input: { [key in T]: Specification }) => {};

type SecondSpecification =
| "string"
| "number"
| `between ${number} and ${number}`;

const _around = <T extends Specification>(
input: { [key in SecondSpecification]: string }
) => {};

const envs = ["test", "development", "production"] as const;
type Env = typeof envs[number];

type MyEnvironment = "PORT" | "API_KEY" | "AGE";
const env = _env<MyEnvironment>({
PORT: "number",
AGE: [13, 99],
API_KEY: "string",
});
*/
/* type Validator = (key: string) => boolean;
const _validator = <T extends string>(input: { [key in T]: Validator }) => {
Object.keys(input).map((key) => {
const validator = input[key] as Validator;
const value = process.env[key];
if (!validator(value))
throw `${key} in env did not pass validator. (value: ${value})`;
});
};
*/
/* const guarantee = <T>(value: any, boolean: boolean): value is T => boolean;

const validators = {
string: <T extends string>(value: T): T => {
guarantee(value, typeof value === "string");
return value;
},
number: <T extends string>(value: T): number => {
if (!guarantee<number>(value, parseInt(value) !== NaN)) throw "";
return value;
},
range: <T extends number>(from: number, to: number) => (
value: any
): number => {
if (
!guarantee<T>(
value,
validators.number(value) && value >= from && value < to
)
)
throw "";
return value;
},
};

_validator({
PORT: validators.range(8000, 9000),
SERIAL: validators.number,
API_KEY: validators.string,
}); */

type Validators = {
number: () => Validator<number>;
string: () => Validator<string>;
range: (from: number, to: number) => Validator<number>;
oneof: <T extends string>(values: T) => Validator<T>;
};
const validators: Validators = {
number: () => (value: string) => {
//TODO: validate
return () => -1;
},
string: () => (value: string) => {
//TODO: validate
return () => value;
},
range: (from: number, to: number) => (value: string) => {
//TODO: value
return () => -1;
},
oneof: <T extends string>(values: string) => () => {
//TODO: validate
return () => null as T;
},
};

type Validator<G> = (input: string) => () => G;
const _env = <T extends string>(
input: { [key in T]: Validator<any> }
): {
[key in T]: any;
} => {
return Object.keys(input)
.map((key: T) => ({ key, get: input[key](key) }))
.map((a) => ({
[a.key]: a.get,
}))
.reduce((a, b) => ({
...a,
...b,
})) as {
[key in T]: any;
};
};

const env = _env({
PORT: validators.range(100, 200),
API_KEY: validators.range(1, 2),
//API_KEY: validators.string(),
});

//FIXME: values are any..
const getPort = validators.range(1, 2)(process.env.PORT);
const port = getPort();
console.log(port);