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

Expanding test coverage #70

Merged
merged 7 commits into from
Sep 28, 2024
Merged
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
41 changes: 14 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,49 +3,36 @@

[![codecov](https://codecov.io/github/Digital-Alchemy-TS/core/graph/badge.svg?token=IBGLY3RY68)](https://codecov.io/github/Digital-Alchemy-TS/core)
[![version](https://img.shields.io/github/package-json/version/Digital-Alchemy-TS/core)](https://www.npmjs.com/package/@digital-alchemy/core)

---

# 🚀 Project Overview - Digital Alchemy TypeScript Libraries

Welcome to `@digital-alchemy/core`!
This repository contains the boilerplate code for building applications and wiring logic.

- [Extended docs](https://docs.digital-alchemy.app/docs/core/)
- [Discord](https://discord.gg/JkZ35Gv97Y)

## 🌐 Core Library Overview
Welcome to `@digital-alchemy/core`

### 🗃️ Cache
The core library is a minimal dependency framework for building Typescript based applications and libraries.
It aims to provide some basic workflows and tools that can be used to create a variety of different types of projects.

Flexible caching solutions with support for memory and Redis cache drivers.
> 📚 [Extended docs](https://docs.digital-alchemy.app/docs/core/)

### ⚙️ Configuration
## 🌐 Tools Overview

Dynamic application settings management, prioritizing configuration from multiple sources for maximum flexibility.
### ⚙️ [Configuration](https://docs.digital-alchemy.app/docs/core/configuration)

### 🌍 Fetch Wrapper
Define and load structured configuration data from files, merge data from environment variables, and more.

Simplifies RESTful service integration, streamlining the development of verbose adapters.

### 📝 Logger
### 📝 [Logger](https://docs.digital-alchemy.app/docs/core/logger/api)

Advanced logging interface for detailed and customizable output, compatible with external libraries for specialized logging needs.

### ⏲️ Scheduler
### ⏲️ [Scheduler](https://docs.digital-alchemy.app/docs/core/scheduler)

Lifecycle-aware task scheduling, featuring flexible timing functions and robust error handling.

### 🔄 Application Lifecycle Management

Comprehensive support for managing the application's lifecycle, from initialization to shutdown.
### 🔄 [Lifecycle Hooks](https://docs.digital-alchemy.app/docs/core/lifecycle)

## 📚 Explore Our Libraries and Applications
Run commands at a variety of predetermined times during your application's boot or shutdown sequence.

Dive deeper into the ecosystem by exploring related libraries and applications published under the Digital Alchemy organization.
Each is designed to extend the capabilities the core library, offering specialized functionalities for a wide range of use cases.
### ⁉️ [Testing Utilities](https://docs.digital-alchemy.app/docs/testing/)

| Library/Application | Description | GitHub | npm |
| ------------------- | ------------------------------------------------------------------------------- | ---------------------------------------------------------- | ---------------------------------------------------------------------- |
| `hass` | Home Assistant integration for smart home automation. | [GitHub](https://github.com/Digital-Alchemy-TS/hass) | [npm](https://www.npmjs.com/package/@digital-alchemy/hass) |
| `synapse` | Tools for generating entities within home assistant | [GitHub](https://github.com/Digital-Alchemy-TS/synapse) | [npm](https://www.npmjs.com/package/@digital-alchemy/synapse) |
| `automation` | Advanced automation tools for creating dynamic workflows. | [GitHub](https://github.com/Digital-Alchemy-TS/automation) | [npm](https://www.npmjs.com/package/@digital-alchemy/automation) |
Convert your application into a testing module - append extra libraries and reconfigure modules to get the coverage you're looking for.
1 change: 1 addition & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export default [
"unicorn/switch-case-braces": "off",
"unicorn/prefer-module": "off",
"@typescript-eslint/no-magic-numbers": "warn",
"unicorn/no-process-exit": "off",
"unicorn/no-object-as-default-parameter": "off",
"@cspell/spellchecker": ["warn", { checkComments: false, autoFix: true }],
"unicorn/no-null": "off",
Expand Down
21 changes: 10 additions & 11 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"repository": {
"url": "git+https://github.com/Digital-Alchemy-TS/core"
},
"version": "24.9.3",
"version": "24.9.4",
"author": {
"url": "https://github.com/zoe-codez",
"name": "Zoe Codez"
Expand Down Expand Up @@ -46,43 +46,42 @@
"engines": {
"node": ">=20"
},
"peerDependencies": {
"dependencies": {
"chalk": "^5",
"dayjs": "^1",
"dotenv": "^16",
"ini": "^4",
"js-yaml": "^4",
"minimist": "^1",
"node-cron": "^3",
"uuid": "*"
"uuid": "^9 || ^10"
},
"devDependencies": {
"@cspell/eslint-plugin": "^8.14.4",
"@eslint/compat": "^1.1.1",
"@eslint/eslintrc": "^3.1.0",
"@eslint/js": "^9.10.0",
"@faker-js/faker": "^9.0.1",
"@eslint/js": "^9.11.1",
"@faker-js/faker": "^9.0.3",
"@jest/globals": "^29.7.0",
"@types/dotenv": "^8.2.0",
"@types/ini": "^4.1.1",
"@types/jest": "^29.5.13",
"@types/js-yaml": "^4.0.9",
"@types/minimist": "^1.2.5",
"@types/node": "^22.5.5",
"@types/node": "^22.7.3",
"@types/node-cron": "^3.0.11",
"@types/semver": "^7.5.8",
"@types/sinonjs__fake-timers": "^8.1.5",
"@types/uuid": "^10.0.0",
"@typescript-eslint/eslint-plugin": "8.6.0",
"@typescript-eslint/parser": "8.6.0",
"@typescript-eslint/eslint-plugin": "8.7.0",
"@typescript-eslint/parser": "8.7.0",
"chalk": "^5.3.0",
"dayjs": "^1.11.13",
"dotenv": "^16.4.5",
"eslint": "9.10.0",
"eslint": "9.11.1",
"eslint-config-prettier": "9.1.0",
"eslint-plugin-import": "^2.30.0",
"eslint-plugin-jsonc": "^2.16.0",
"eslint-plugin-no-unsanitized": "^4.1.0",
"eslint-plugin-no-unsanitized": "^4.1.1",
"eslint-plugin-prettier": "^5.2.1",
"eslint-plugin-security": "^3.0.1",
"eslint-plugin-simple-import-sort": "^12.1.1",
Expand Down
57 changes: 26 additions & 31 deletions src/helpers/config-file-loader.helper.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,25 @@
import { existsSync, readFileSync, statSync } from "fs";
import { decode } from "ini";
import { load } from "js-yaml";
import fs from "fs";
import ini from "ini";
import yaml from "js-yaml";
import minimist from "minimist";
import { homedir } from "os";
import { join } from "path";
import { cwd, exit, platform } from "process";
import { cwd, platform } from "process";

import { deepExtend, INVERT_VALUE, is, ServiceMap, START } from "..";
import {
AbstractConfig,
ConfigLoaderParams,
ConfigLoaderReturn,
ModuleConfiguration,
} from "./config.helper";
import { deepExtend, INVERT_VALUE, is, PartialConfiguration, ServiceMap, START } from "..";
import { ConfigLoaderParams, ConfigLoaderReturn, ModuleConfiguration } from "./config.helper";

const isWindows = platform === "win32";

export const SUPPORTED_CONFIG_EXTENSIONS = ["json", "ini", "yaml", "yml"];
function withExtensions(path: string): string[] {
export function withExtensions(path: string): string[] {
return [path, join(path, "config")].flatMap(path => [
path,
...SUPPORTED_CONFIG_EXTENSIONS.map(i => `${path}.${i}`),
]);
}

export function configFilePaths(name = "digital-alchemy"): string[] {
export function configFilePaths(name: string): string[] {
const out: string[] = [];
if (!isWindows) {
out.push(...withExtensions(join(`/etc`, `${name}`)));
Expand All @@ -40,10 +35,10 @@ export function configFilePaths(name = "digital-alchemy"): string[] {
current = next;
}
out.push(...withExtensions(join(homedir(), ".config", name)));
return out.filter(filePath => existsSync(filePath) && statSync(filePath).isFile());
return out.filter(filePath => fs.existsSync(filePath) && fs.statSync(filePath).isFile());
}

export async function ConfigLoaderFile<
export async function configLoaderFile<
S extends ServiceMap = ServiceMap,
C extends ModuleConfiguration = ModuleConfiguration,
>({ application, logger }: ConfigLoaderParams<S, C>): ConfigLoaderReturn {
Expand All @@ -52,45 +47,45 @@ export async function ConfigLoaderFile<
let files: string[];
if (is.empty(configFile)) {
files = configFilePaths(application.name);
logger.trace({ files, name: ConfigLoaderFile }, `identified config files`);
logger.trace({ files, name: configLoaderFile }, `identified config files`);
} else {
if (!existsSync(configFile)) {
if (!fs.existsSync(configFile)) {
logger.fatal(
{ configFile, name: ConfigLoaderFile },
{ configFile, name: configLoaderFile },
`used {--config} to specify path that does not exist`,
);
exit();
process.exit();
}
files = [configFile];
logger.debug(
{ configFile, name: ConfigLoaderFile },
{ configFile, name: configLoaderFile },
`used {--config}, loading from target file`,
);
}

if (is.empty(files)) {
return {};
}
const out: Partial<AbstractConfig> = {};
logger.trace({ files, name: ConfigLoaderFile }, `loading configuration files`);
const out: Partial<PartialConfiguration> = {};
logger.trace({ files, name: configLoaderFile }, `loading configuration files`);
files.forEach(file => loadConfigFromFile(out, file));
return out;
}

function loadConfigFromFile(out: Partial<AbstractConfig>, filePath: string) {
const fileContent = readFileSync(filePath, "utf8").trim();
export function loadConfigFromFile(out: PartialConfiguration, filePath: string) {
const fileContent = fs.readFileSync(filePath, "utf8").trim();
const hasExtension = SUPPORTED_CONFIG_EXTENSIONS.some(extension => {
if (filePath.slice(extension.length * INVERT_VALUE).toLowerCase() === extension) {
switch (extension) {
case "ini":
deepExtend(out, decode(fileContent) as unknown as AbstractConfig);
deepExtend(out, ini.decode(fileContent) as PartialConfiguration);
return true;
case "yaml":
case "yml":
deepExtend(out, load(fileContent) as AbstractConfig);
deepExtend(out, yaml.load(fileContent) as PartialConfiguration);
return true;
case "json":
deepExtend(out, JSON.parse(fileContent) as unknown as AbstractConfig);
deepExtend(out, JSON.parse(fileContent) as PartialConfiguration);
return true;
}
}
Expand All @@ -101,19 +96,19 @@ function loadConfigFromFile(out: Partial<AbstractConfig>, filePath: string) {
}
// Guessing JSON
if (fileContent[START] === "{") {
deepExtend(out, JSON.parse(fileContent) as unknown as AbstractConfig);
deepExtend(out, JSON.parse(fileContent) as PartialConfiguration);
return;
}
// Guessing yaml
try {
const content = load(fileContent);
const content = yaml.load(fileContent);
if (is.object(content)) {
deepExtend(out, content as unknown as AbstractConfig);
deepExtend(out, content as PartialConfiguration);
return;
}
} catch {
// Is not a yaml file
}
// Final fallback: INI
deepExtend(out, decode(fileContent) as unknown as AbstractConfig);
deepExtend(out, ini.decode(fileContent) as PartialConfiguration);
}
13 changes: 8 additions & 5 deletions src/helpers/config.helper.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import dotenv from "dotenv";
import fs from "fs";
import { ParsedArgs } from "minimist";
import { isAbsolute, join, normalize } from "path";
import path from "path";
import { cwd } from "process";

import {
Expand Down Expand Up @@ -147,7 +147,7 @@ export type ConfigLoader = <S extends ServiceMap, C extends OptionalModuleConfig
params: ConfigLoaderParams<S, C>,
) => ConfigLoaderReturn;

export function cast<T = unknown>(data: string | string[], type: string): T {
export function cast<T = unknown>(data: boolean | number[] | string | string[], type: string): T {
switch (type) {
case "boolean": {
data ??= "";
Expand Down Expand Up @@ -208,7 +208,8 @@ export function loadDotenv(
CLI_SWITCHES: ParsedArgs,
logger: ILogger,
) {
let { envFile } = internal.boot.options ?? {};
internal.boot.options ??= {};
let { envFile } = internal.boot.options;
const switchKeys = Object.keys(CLI_SWITCHES);
const searched = iSearchKey("env-file", switchKeys);

Expand All @@ -221,7 +222,9 @@ export function loadDotenv(

// * was provided an --env-file or something via boot
if (!is.empty(envFile)) {
const checkFile = isAbsolute(envFile) ? normalize(envFile) : join(cwd(), envFile);
const checkFile = path.isAbsolute(envFile)
? path.normalize(envFile)
: path.join(cwd(), envFile);
if (fs.existsSync(checkFile)) {
file = checkFile;
} else {
Expand All @@ -231,7 +234,7 @@ export function loadDotenv(

// * attempt default file
if (is.empty(file)) {
const defaultFile = join(cwd(), ".env");
const defaultFile = path.join(cwd(), ".env");
if (fs.existsSync(defaultFile)) {
file = defaultFile;
} else {
Expand Down
18 changes: 9 additions & 9 deletions src/helpers/extend.helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ function isSpecificValue(value: unknown) {
return value instanceof Date || value instanceof RegExp;
}

function cloneSpecificValue(value: unknown) {
export function cloneSpecificValue(value: unknown) {
if (value instanceof Date) {
return new Date(value.getTime());
}
Expand All @@ -17,25 +17,25 @@ function cloneSpecificValue(value: unknown) {
export function deepCloneArray<TYPE = unknown>(array: Array<TYPE>): Array<TYPE> {
// eslint-disable-next-line sonarjs/function-return-type
return array.map(item => {
if (is.array(item)) {
return deepCloneArray(item);
}
if (isSpecificValue(item)) {
return cloneSpecificValue(item);
}
if (is.object(item)) {
if (is.array(item)) {
return deepCloneArray(item);
}
if (isSpecificValue(item)) {
return cloneSpecificValue(item);
}
return deepExtend({}, item);
}
return item;
}) as Array<TYPE>;
}

function safeGetProperty(object: unknown, key: string) {
export function safeGetProperty(object: unknown, key: string) {
return key === "__proto__" ? undefined : (object as Record<string, unknown>)[key];
}

export function deepExtend<A, B>(target: A, object: B): A & B {
if (typeof object !== "object" || object === null || is.array(object)) {
if (!is.object(object)) {
return target as A & B;
}
Object.keys(object).forEach(key => {
Expand Down
Loading
Loading