Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement new ENV package #195

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ Click on one of the links to access the documentation of the package:
| result | [@openally/result](./src/result) |
| auto-url | [@openally/auto-url](./src/auto-url) |
| config | [@openally/config](./src/config) |
| env | [@openally/env](./src/env) |

These packages are available in the Node Package Repository and can be easily installed with [npm](https://docs.npmjs.com/getting-started/what-is-npm) or [yarn](https://yarnpkg.com).
```bash
Expand Down
34 changes: 33 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
"src/mutex",
"src/result",
"src/auto-url",
"src/config"
"src/config",
"src/env"
],
"license": "MIT",
"bugs": {
Expand Down
46 changes: 46 additions & 0 deletions src/env/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<p align="center"><h1 align="center">
Env
</h1>

<p align="center">
Safely load/parse ENV variables
</p>

<p align="center">
<a href="https://github.com/OpenAlly/npm-packages/src/env">
<img src="https://img.shields.io/github/package-json/v/OpenAlly/npm-packages/main/src/env?style=for-the-badge&label=version" alt="npm version">
</a>
<a href="https://github.com/OpenAlly/npm-packages/tree/main/src/LICENSE">
<img src="https://img.shields.io/github/license/OpenAlly/npm-packages?style=for-the-badge" alt="license">
</a>
<a href="https://github.com/OpenAlly/npm-packages/tree/main/src/env">
<img src="https://img.shields.io/npm/dw/@openally/env?style=for-the-badge" alt="download">
</a>
<a href="https://github.com/OpenAlly/npm-packages/tree/main/src/env">
<img src="https://img.shields.io/github/actions/workflow/status/OpenAlly/npm-packages/env.yml?style=for-the-badge">
</a>
</p>

## Requirements
- [Node.js](https://nodejs.org/en/) v20 or higher

## Getting Started

This package is available in the Node Package Repository and can be easily installed with [npm](https://docs.npmjs.com/getting-started/what-is-npm) or [yarn](https://yarnpkg.com).

```bash
$ npm i @openally/env
# or
$ yarn add @openally/env
```

## Usage example

```ts
```

## API
TBC

## License
MIT
32 changes: 32 additions & 0 deletions src/env/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"name": "@openally/env",
"version": "1.0.0",
"description": "Safely load/parse ENV variables",
"main": "./dist/index.js",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"exports": {
".": {
"require": "./dist/index.js",
"import": "./dist/index.mjs",
"types": "./dist/index.d.ts"
}
},
"scripts": {
"build": "tsup src/index.ts --format cjs,esm --dts --clean",
"prepublishOnly": "npm run build",
"test": "glob -c \"tsx --test\" \"./test/**/*.spec.ts\"",
"coverage": "c8 --all --src ./src -r html npm test",
"lint": "cross-env eslint src/**/*.ts"
},
"files": [
"dist"
],
"keywords": [],
"author": "GENTILHOMME Thomas <[email protected]>",
"license": "MIT",
"dependencies": {
"@openally/result": "^1.3.0",
"ts-pattern": "^5.3.1"
}
}
135 changes: 135 additions & 0 deletions src/env/src/class/EnvLoader.class.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
// Import Third-party Dependencies
import { Err, Ok, Some, None, type Result } from "@openally/result";
import { match } from "ts-pattern";

// Import Internal Dependencies
import {
SafeEnvNotFound,
EnvNotFound,
SafeUnknowEnvType,
UnknowEnvType
} from "../errors.js";
import {
type ExtractDescriptorType,
type AllEnvTypeDescriptor,
type StringTypeParameters,
ENV_STRING,
ENV_BOOLEAN,
ENV_NUMBER
} from "../types.js";
import * as utils from "../utils/index.js";

// CONSTANTS
function kNoopParsingHook() {
return void 0;
}

export type ParsingState = "found" | "notFound" | "parsingError";
export type ParsingHookFunction = (envName: string, state: ParsingState, value?: any) => void;

export interface EnvLoaderOptions {
parseHook?: ParsingHookFunction;
/**
* Avoid leaking secrets and password values
* @default true
*/
redact?: boolean;
prefix?: string;
}

export interface EnvLoaderParsingOptions {
env?: NodeJS.ProcessEnv;
prefix?: string;
}

export class EnvLoader {
private parseHook: ParsingHookFunction;
private prefix: string | undefined;

constructor(
options: EnvLoaderOptions = {}
) {
const {
parseHook = kNoopParsingHook,
prefix
} = options;

this.parseHook = parseHook;
this.prefix = prefix;
}

unsafeParse<T extends AllEnvTypeDescriptor>(
name: string,
descriptor: T,
options: EnvLoaderParsingOptions = {}
): ExtractDescriptorType<T> {
return this.parse(name, descriptor, options).unwrap();
}

parse<T extends AllEnvTypeDescriptor>(
name: string,
descriptor: T,
options: EnvLoaderParsingOptions = {}
): Result<ExtractDescriptorType<T>, SafeEnvNotFound | SafeUnknowEnvType> {
const {
env = process.env,
prefix = this.prefix
} = options;

const finalizedName = prefix && name.startsWith(prefix) ?
name : `${prefix}${name}`;
const rawEnv = env[finalizedName];

if (typeof rawEnv === "undefined") {
this.parseHook(finalizedName, "notFound");

return Err(EnvNotFound.create(finalizedName));
}

const parsingResult = match(descriptor.type)
.with(ENV_STRING, () => this.#parseString(rawEnv, descriptor.parameters))
.with(ENV_BOOLEAN, () => this.#parseBoolean(rawEnv))
.with(ENV_NUMBER, () => this.#parseNumber(rawEnv))
.otherwise(() => None);

if (parsingResult.some) {
const parsedValue = parsingResult.safeUnwrap();
this.parseHook(
finalizedName, "found", utils.redactEnv(finalizedName, parsedValue)
);

// TODO: not sure how to fix type here
return Ok(parsedValue as any);
}

this.parseHook(
finalizedName, "parsingError", utils.redactEnv(finalizedName, rawEnv)
);

return Err(UnknowEnvType.create());
}

#parseString(
value: string,
parameters: StringTypeParameters
) {
const { format } = parameters;

const trimedValue = value.trim();
if (format && !format.test(trimedValue)) {
return None;
}

return Some(trimedValue);
}

#parseBoolean(value: string) {
return Some(/^(?:y|yes|true|1|on)$/i.test(value.trim()));
}

#parseNumber(value: string) {
const castedValue = Number(value);

return Number.isNaN(castedValue) ? None : Some(castedValue);
}
}
29 changes: 29 additions & 0 deletions src/env/src/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
declare const __brand: unique symbol;
type Brand<B> = { [__brand]: B };
type Branded<T, B> = T & Brand<B>;

function createBranded<T, B>(instance: T): T & Brand<B> {
return instance as T & Brand<B>;
}

export class EnvNotFound extends Error {
static create(envName: string): SafeEnvNotFound {
return createBranded(new EnvNotFound(envName));
}

private constructor(envName: string) {
super(`Unable to find and load '${envName}'.`);
}
}
export type SafeEnvNotFound = Branded<EnvNotFound, "EnvNotFound">;

export class UnknowEnvType extends Error {
static create(): SafeUnknowEnvType {
return createBranded(new UnknowEnvType());
}

private constructor() {
super(`Unknown Env type`);
}
}
export type SafeUnknowEnvType = Branded<UnknowEnvType, "UnknowEnvType">;
37 changes: 37 additions & 0 deletions src/env/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Import Third-party Dependencies
import { type Result } from "@openally/result";

// Import Internal Dependencies
import {
SafeEnvNotFound,
SafeUnknowEnvType
} from "./errors.js";
import { EnvLoader } from "./class/EnvLoader.class.js";
import {
type EnvType,
type EnvTypeDescriptor,
type ExtractDescriptorSymbol,
type ExtractDescriptorType,
Types
} from "./types.js";

export interface PickOptions {
env?: NodeJS.ProcessEnv;
}

export function pick<
T extends EnvType = typeof Types.string
>(
name: string,
descriptor: T = Types.string as T,
options: PickOptions = {}
): Result<ExtractDescriptorType<T>, SafeEnvNotFound | SafeUnknowEnvType> {
const finalizedDescriptor: EnvTypeDescriptor<ExtractDescriptorSymbol<T>> = typeof descriptor === "function" ?
descriptor() :
descriptor;

return new EnvLoader()
.parse(name, finalizedDescriptor, options);
}

export { Types };
Loading
Loading