Skip to content
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
26 changes: 26 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,29 @@ jobs:

- name: Test code compilation
run: sh scripts/build

cov-report:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Use Node.js 18.x
uses: actions/setup-node@v1
with:
node-version: 18.x

- name: Install dependencies
run: npm install

- name: Run tests
run: npm run test:coverage

- name: Generate Code Coverage report
id: code-coverage
uses: barecheck/code-coverage-action@v1
with:
barecheck-github-app-token: ${{ secrets.BARECHECK_GITHUB_APP_TOKEN }}
lcov-file: "./tests/Coverage/lcov.info"
send-summary-comment: true
show-annotations: "warning"
4 changes: 2 additions & 2 deletions 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
@@ -1,6 +1,6 @@
{
"name": "@athenna/config",
"version": "3.6.0",
"version": "3.7.0",
"description": "Cache and handle environment variables and config files of Athenna.",
"license": "MIT",
"author": "João Lenon <lenon@athenna.io>",
Expand Down Expand Up @@ -81,6 +81,7 @@
"exclude": [],
"reporter": [
"text-summary",
"lcovonly",
"html"
],
"report-dir": "./tests/Coverage",
Expand Down
41 changes: 39 additions & 2 deletions src/Config/Config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,20 @@
* file that was distributed with this source code.
*/

import { sep, parse } from 'node:path'
import {
File,
Json,
Path,
ObjectBuilder,
Exec,
Module,
Is,
} from '@athenna/common'
import { loadFile, writeFile } from 'magicast'
import { File, Json, Path, ObjectBuilder, Exec, Module } from '@athenna/common'
import { sep, parse, extname } from 'node:path'
import { RecursiveConfigException } from '#src/Exceptions/RecursiveConfigException'
import { NotSupportedKeyException } from '#src/Exceptions/NotSupportedKeyException'
import { NotValidArrayConfigException } from '#src/Exceptions/NotValidArrayConfigException'

export class Config {
/**
Expand Down Expand Up @@ -111,6 +120,28 @@ export class Config {
return this
}

/**
* Push a value to a configuration key that is a valid array.
* If configuration is not an array, an exception will be thrown.
*/
public static push(key: string, value: any | any[]): typeof Config {
const config = this.configs.get(key, [])

if (!Is.Array(config)) {
throw new NotValidArrayConfigException(key)
}

if (Is.Array(value)) {
config.push(...value)
} else {
config.push(value)
}

this.configs.set(key, config)

return this
}

/**
* Delete the configuration key.
*/
Expand Down Expand Up @@ -168,6 +199,12 @@ export class Config {
path = Path.config(),
safe = false,
): Promise<void> {
if (extname(path)) {
safe ? this.safeLoad(path) : this.load(path)

return
}

const files = await Module.getAllJSFilesFrom(path)

this.fatherConfigPath = path
Expand Down
34 changes: 34 additions & 0 deletions src/Decorators/Value.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/**
* @athenna/config
*
* (c) João Lenon <lenon@athenna.io>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

import 'reflect-metadata'

import { Is } from '@athenna/common'
import { NotFoundConfigException } from '#src/Exceptions/NotFoundConfigException'

/**
* Set the value of some configuration in your class property.
*/
export function Value(key: string, defaultValue?: any): PropertyDecorator {
return (target: any, propKey: string | symbol) => {
if (Is.Defined(defaultValue) || defaultValue === null) {
Object.defineProperty(target, propKey, {
value: Config.get(key, defaultValue),
})

return
}

if (!Config.exists(key)) {
throw new NotFoundConfigException(key)
}

Object.defineProperty(target, propKey, { value: Config.get(key) })
}
}
21 changes: 21 additions & 0 deletions src/Exceptions/NotFoundConfigException.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/**
* @athenna/config
*
* (c) João Lenon <lenon@athenna.io>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

import { Exception } from '@athenna/common'

export class NotFoundConfigException extends Exception {
public constructor(key: string) {
super({
status: 500,
code: 'E_NOT_FOUND_CONFIG',
message: `The configuration ${key} does not exist or the value is a hardcoded undefined.`,
help: `To solve this problem you can set a default value when trying to get your configuration or setting a value to your ${key} configuration.`,
})
}
}
21 changes: 21 additions & 0 deletions src/Exceptions/NotValidArrayConfigException.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/**
* @athenna/config
*
* (c) João Lenon <lenon@athenna.io>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

import { Exception } from '@athenna/common'

export class NotValidArrayConfigException extends Exception {
public constructor(key: string) {
super({
status: 500,
code: 'E_NOT_VALID_CONFIG_ARRAY',
message: `The configuration ${key} is not a valid array, and it is not possible to push values to it.`,
help: `Try changing your configuration key when calling push and pushAll methods or transform the value of your configuration ${key} to an array.`,
})
}
}
9 changes: 9 additions & 0 deletions tests/Stubs/classes/AppService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Value } from '#src/Decorators/Value'

export class AppService {
@Value('app')
public app: any

@Value('app.name')
public name: any
}
9 changes: 9 additions & 0 deletions tests/Stubs/classes/DoesNotThrowNotFound.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Value } from '#src/Decorators/Value'

export class DoesNotThrowNotFound {
@Value('app.notFound', null)
public defined: any

@Value('app.notFoundApp', 'Athenna')
public definedApp: any
}
6 changes: 6 additions & 0 deletions tests/Stubs/classes/ThrowNotFound.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { Value } from '#src/Decorators/Value'

export class ThrowNotFound {
@Value('app.notFound')
public app: any
}
1 change: 1 addition & 0 deletions tests/Stubs/config/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,6 @@

export default {
name: 'Athenna',
environments: ['default'],
env: Env('NODE_ENV', 'test'),
}
45 changes: 40 additions & 5 deletions tests/Unit/Config/ConfigTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { File, Folder, Path } from '@athenna/common'
import { Test, TestContext, Cleanup, BeforeEach, AfterEach } from '@athenna/test'
import { RecursiveConfigException } from '#src/Exceptions/RecursiveConfigException'
import { NotSupportedKeyException } from '#src/Exceptions/NotSupportedKeyException'
import { NotValidArrayConfigException } from '#src/Exceptions/NotValidArrayConfigException'

export default class ConfigTest {
@BeforeEach()
Expand All @@ -35,7 +36,7 @@ export default class ConfigTest {
public async shouldBeAbleToGetAllConfigurationsValuesFromConfigClass({ assert }: TestContext) {
const configs = Config.get()

assert.deepEqual(configs.app, { name: 'Athenna', env: 'test' })
assert.deepEqual(configs.app, { name: 'Athenna', env: 'test', environments: ['default'] })
assert.deepEqual(configs.database, { username: 'Athenna', env: 'test' })
}

Expand All @@ -46,6 +47,7 @@ export default class ConfigTest {
assert.deepEqual(app, {
name: 'Athenna',
env: 'test',
environments: ['default'],
})
}

Expand Down Expand Up @@ -111,6 +113,7 @@ export default class ConfigTest {
subName: 'Framework',
},
env: 'test',
environments: ['default'],
})

Config.set('app', { hello: 'world' })
Expand Down Expand Up @@ -138,6 +141,28 @@ export default class ConfigTest {
Config.set('app', mainConfig)
}

@Test()
public async shouldBeAbleToPushValuesToConfigurationsThatAreArrays({ assert }: TestContext) {
Config.push('app.environments', 'http')
Config.push('app.environments', ['repl', 'console'])

assert.deepEqual(Config.get('app.environments'), ['default', 'http', 'repl', 'console'])
}

@Test()
public async shouldBeAbleToPushValuesToConfigurationsThatDoesNotExist({ assert }: TestContext) {
Config.push('app.newArray', 'http')
Config.push('app.newArrayArray', ['repl', 'console'])

assert.deepEqual(Config.get('app.newArray'), ['http'])
assert.deepEqual(Config.get('app.newArrayArray'), ['repl', 'console'])
}

@Test()
public async shouldThrowAnExceptionIfTryingToPushValueToAConfigurationThatIsNotAnArray({ assert }: TestContext) {
assert.throws(() => Config.push('app.name', 'http'), NotValidArrayConfigException)
}

@Test()
public async shouldBeAbleToDeleteValuesFromConfigurations({ assert }: TestContext) {
Config.delete('notFound')
Expand All @@ -163,7 +188,7 @@ export default class ConfigTest {
}

@Test()
public async shouldThrownAnErrorWhenLoadingAConfigurationFileThatRecursivellyLoadsOther({ assert }: TestContext) {
public async shouldThrownAnErrorWhenLoadingAConfigurationFileThatRecursivelyLoadsOther({ assert }: TestContext) {
const useCase = async () => await Config.load(Path.stubs('config/recursiveOne.ts'))

await assert.rejects(useCase, RecursiveConfigException)
Expand All @@ -178,7 +203,7 @@ export default class ConfigTest {
}

@Test()
public async shouldBeAbleToRealodConfigurationValues({ assert }: TestContext) {
public async shouldBeAbleToReloadConfigurationValues({ assert }: TestContext) {
assert.equal(Config.get('app.env'), 'test')

process.env.NODE_ENV = 'example'
Expand Down Expand Up @@ -217,14 +242,24 @@ export default class ConfigTest {

@Test()
@Cleanup(() => Config.delete('app'))
public async shouldBeAbleToLoadAllConfigurationPathSafelly({ assert }: TestContext) {
public async shouldBeAbleToLoadAllConfigurationPathSafely({ assert }: TestContext) {
Config.set('app', {})

await Config.loadAll(Path.config(), true)

assert.deepEqual(Config.get('app'), {})
}

@Test()
@Cleanup(() => Config.delete('app'))
public async shouldBeAbleToLoadASingleFileInLoadAllMethod({ assert }: TestContext) {
Config.set('app', {})

await Config.loadAll(Path.config('app.ts'), true)

assert.deepEqual(Config.get('app'), {})
}

@Test()
@Cleanup(() => (process.env.IS_TS = 'true'))
public async shouldBeAbleToLoadAllJsFilesButNotTsFilesWhenEnvTsIsFalse({ assert }: TestContext) {
Expand All @@ -238,7 +273,7 @@ export default class ConfigTest {
}

@Test()
public async shouldBeAbleToLoadConfigFoldersRecursivelly({ assert }: TestContext) {
public async shouldBeAbleToLoadConfigFoldersRecursively({ assert }: TestContext) {
Config.clear()

await Config.loadAll(Path.stubs('recursive'), false)
Expand Down
Loading