Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found
Select Git revision

Target

Select target project
  • libs/config
1 result
Select Git revision
Show changes
import { promises as fs } from 'fs';
import { createConfig } from './index';
// Test JSON data
const jsonDefaults = {
setting: 'defaultvalue',
another: {
setting: 'avalue',
},
};
const jsonLocals = {
setting: 'localvalue',
another: {
more: 'stuff',
},
even: {
deeper: {
key: 'sodeep',
},
},
};
describe('Config', () => {
it('should only have values from config.defaults.json', async () => {
await fs.writeFile('config.defaults.json', JSON.stringify(jsonDefaults));
const config = createConfig();
await fs.unlink('config.defaults.json');
expect(config.setting).toBe('defaultvalue');
expect(config.another.setting).toBe('avalue');
});
it('should only have values from config.json', async () => {
await fs.writeFile('config.json', JSON.stringify(jsonLocals));
const config = createConfig();
await fs.unlink('config.json');
expect(config.setting).toBe('localvalue');
expect(config.another.more).toBe('stuff');
});
it('should have both values with preference to config.json', async () => {
await fs.writeFile('config.defaults.json', JSON.stringify(jsonDefaults));
await fs.writeFile('config.json', JSON.stringify(jsonLocals));
const config = createConfig();
await fs.unlink('config.json');
await fs.unlink('config.defaults.json');
expect(config.setting).toBe('localvalue');
expect(config.another.more).toBe('stuff');
expect(config.another.setting).toBe('avalue');
});
it('should respect a given basePath', async () => {
// Create the tmp directory only if it doesn't exist
await fs.mkdir('tmp', { recursive: true });
await fs.writeFile('tmp/config.defaults.json', JSON.stringify(jsonDefaults));
await fs.writeFile('tmp/config.json', JSON.stringify(jsonLocals));
const config = createConfig('tmp');
await fs.unlink('tmp/config.json');
await fs.unlink('tmp/config.defaults.json');
// Remove the tmp directory after the test
await fs.rmdir('tmp', { recursive: true });
expect(config.setting).toBe('localvalue');
expect(config.another.more).toBe('stuff');
expect(config.another.setting).toBe('avalue');
});
it('should have all values with preference to env', async () => {
await fs.writeFile('config.defaults.json', JSON.stringify(jsonDefaults));
await fs.writeFile('config.json', JSON.stringify(jsonLocals));
process.env['SETTING'] = 'overwritten-by-env';
process.env['ANOTHER_MORE'] = 'false';
process.env['EVEN_DEEPER_KEY'] = 'true';
const config = createConfig();
await fs.unlink('config.json');
await fs.unlink('config.defaults.json');
delete process.env['SETTING'];
delete process.env['ANOTHER_MORE'];
delete process.env['EVEN_DEEPER_KEY'];
expect(config.setting).toBe('overwritten-by-env');
expect(config.another.more).toBe(false);
expect(config.even.deeper.key).toBe(true);
expect(config.another.setting).toBe('avalue');
});
it('should have all values with preference to env and prefix', async () => {
await fs.writeFile('config.defaults.json', JSON.stringify(jsonDefaults));
await fs.writeFile('config.json', JSON.stringify(jsonLocals));
process.env['P_SETTING'] = 'overwritten-by-env';
process.env['P_ANOTHER_MORE'] = 'false';
process.env['P_EVEN_DEEPER_KEY'] = 'true';
const config = createConfig(undefined, 'p');
await fs.unlink('config.json');
await fs.unlink('config.defaults.json');
delete process.env['P_SETTING'];
delete process.env['P_ANOTHER_MORE'];
delete process.env['P_EVEN_DEEPER_KEY'];
expect(config.setting).toBe('overwritten-by-env');
expect(config.another.more).toBe(false);
expect(config.even.deeper.key).toBe(true);
expect(config.another.setting).toBe('avalue');
});
it('should read in a file when given', async () => {
jsonLocals.setting = 'file:file.txt';
await fs.writeFile('config.defaults.json', JSON.stringify(jsonDefaults));
await fs.writeFile('config.json', JSON.stringify(jsonLocals));
await fs.writeFile('file.txt', 'value-from-file');
const config = createConfig();
await fs.unlink('config.json');
await fs.unlink('config.defaults.json');
await fs.unlink('file.txt');
jsonLocals.setting = 'localvalue';
expect(config.setting).toBe('value-from-file');
});
it('should show a message and keep the setting when file given but not found', async () => {
jsonLocals.setting = 'file:file.txt';
await fs.writeFile('config.defaults.json', JSON.stringify(jsonDefaults));
await fs.writeFile('config.json', JSON.stringify(jsonLocals));
const config = createConfig();
await fs.unlink('config.json');
await fs.unlink('config.defaults.json');
jsonLocals.setting = 'localvalue';
expect(config.setting).toBe('file:file.txt');
});
it('should _reload if asked', async () => {
await fs.writeFile('config.defaults.json', JSON.stringify(jsonDefaults));
await fs.writeFile('config.json', JSON.stringify(jsonLocals));
const config = createConfig();
expect(config.setting).toBe('localvalue');
jsonLocals.setting = 'reloaded-value';
await fs.writeFile('config.json', JSON.stringify(jsonLocals));
config._reload();
await fs.unlink('config.json');
await fs.unlink('config.defaults.json');
jsonLocals.setting = 'localvalue';
expect(config.setting).toBe('reloaded-value');
});
it('should redact passwords if using _show', async () => {
jsonLocals.even.deeper.key = 'password123';
await fs.writeFile('config.defaults.json', JSON.stringify(jsonDefaults));
await fs.writeFile('config.json', JSON.stringify(jsonLocals));
const config = createConfig();
await fs.unlink('config.json');
await fs.unlink('config.defaults.json');
jsonLocals.even.deeper.key = 'sodeep';
expect(config._show().even.deeper.key).toBe('REDACTED');
});
it('should handle missing config.defaults.json gracefully', async () => {
// Ensure config.defaults.json does not exist
await fs.unlink('config.defaults.json').catch(() => {}); // Ignore errors if the file doesn't exist
const consoleSpy = jest.spyOn(console, 'log').mockImplementation();
const config = createConfig();
// Ensure no crash and empty config
expect(config.setting).toBeUndefined();
// Check if the missing file message was logged
expect(consoleSpy).toHaveBeenCalledWith(
expect.stringContaining(`Defaults file missing`)
);
consoleSpy.mockRestore();
});
it('should handle missing config.json gracefully', async () => {
// Create config.defaults.json but ensure config.json does not exist
await fs.writeFile('config.defaults.json', JSON.stringify(jsonDefaults));
await fs.unlink('config.json').catch(() => {});
const consoleSpy = jest.spyOn(console, 'log').mockImplementation();
const config = createConfig();
await fs.unlink('config.defaults.json');
// Expect log message for the missing file
expect(consoleSpy).toHaveBeenCalledWith(
expect.stringContaining('local file missing')
);
// Defaults should still be loaded
expect(config.setting).toBe('defaultvalue');
expect(config.another.setting).toBe('avalue');
consoleSpy.mockRestore();
});
it('should handle invalid JSON content in config.defaults.json gracefully', async () => {
// Write invalid JSON to config.defaults.json
await fs.writeFile('config.defaults.json', '{ invalid json }');
await fs.writeFile('config.json', JSON.stringify(jsonLocals));
const consoleSpy = jest.spyOn(console, 'error').mockImplementation();
const config = createConfig();
// Expect a log message for invalid JSON
expect(consoleSpy).toHaveBeenCalledWith(
expect.stringContaining('Error reading defaults file')
);
// Clean up files
await fs.unlink('config.defaults.json');
await fs.unlink('config.json');
// Expect config.json to still load successfully
expect(config.setting).toBe('localvalue');
expect(config.another.more).toBe('stuff');
consoleSpy.mockRestore();
});
it('should handle invalid JSON content in config.json gracefully', async () => {
// Write invalid JSON to config.json
await fs.writeFile('config.defaults.json', JSON.stringify(jsonDefaults));
await fs.writeFile('config.json', '{ invalid json }');
const consoleSpy = jest.spyOn(console, 'error').mockImplementation();
const config = createConfig();
// Expect a log message for invalid JSON
expect(consoleSpy).toHaveBeenCalledWith(
expect.stringContaining('Error reading local config file')
);
// Clean up files
await fs.unlink('config.defaults.json');
await fs.unlink('config.json');
// Expect defaults to still load successfully
expect(config.setting).toBe('defaultvalue');
expect(config.another.setting).toBe('avalue');
consoleSpy.mockRestore();
});
it('should serialize only configData using toJSON()', async () => {
// Create test configuration files
await fs.writeFile('config.defaults.json', JSON.stringify(jsonDefaults));
await fs.writeFile('config.json', JSON.stringify(jsonLocals));
const config = createConfig();
// Serialize the config object to JSON
const serializedConfig = JSON.stringify(config);
// Clean up test files
await fs.unlink('config.defaults.json');
await fs.unlink('config.json');
// Parse the serialized JSON
const parsedConfig = JSON.parse(serializedConfig);
// Check that all expected configData properties are present
expect(parsedConfig.setting).toBe('localvalue');
expect(parsedConfig.another.setting).toBe('avalue'); // From defaults
expect(parsedConfig.another.more).toBe('stuff'); // From local
expect(parsedConfig.even.deeper.key).toBe('sodeep'); // From local
// Ensure internal properties are not present
expect(parsedConfig).not.toHaveProperty('configDefaults');
expect(parsedConfig).not.toHaveProperty('configLocal');
expect(parsedConfig).not.toHaveProperty('envPrefix');
});
it('should log an error if a file referenced by "file:" is missing', async () => {
// Write test configuration file
await fs.writeFile(
'config.json',
JSON.stringify({
setting: 'file:nonexistent-file.txt',
})
);
// Spy on console.error
const consoleSpy = jest.spyOn(console, 'error').mockImplementation();
const config = createConfig();
// Clean up test files
await fs.unlink('config.json');
// Ensure the original "file:" reference remains unchanged
expect(config.setting).toBe('file:nonexistent-file.txt');
// Ensure error is logged for the missing file
expect(consoleSpy).toHaveBeenCalledWith(
expect.stringContaining('An unknown error occurred')
);
consoleSpy.mockRestore();
});
});
\ No newline at end of file
import * as fs from 'fs';
import * as path from 'path';
type ConfigObject = Record<string, any>;
export class Config {
[key: string]: any; // Index signature to allow dynamic properties
private basePath: string | undefined;
private envPrefix: string | undefined;
private configDefaults: string;
private configLocal: string;
private configData: ConfigObject;
constructor(basePath: string | undefined = undefined, envPrefix: string | undefined = undefined) {
this.basePath = basePath;
this.envPrefix = envPrefix ? envPrefix.toUpperCase() + "_" : "";
this.configDefaults = this.basePath
? path.join(this.basePath, 'config.defaults.json')
: path.join(path.dirname(require.main?.filename || ''), 'config.defaults.json');
this.configLocal = this.basePath
? path.join(this.basePath, 'config.json')
: path.join(path.dirname(require.main?.filename || ''), 'config.json');
this.configData = {};
this._reload();
}
private objectDeepKeys(obj: ConfigObject): string[] {
return Object.keys(obj)
.filter((key) => obj[key] instanceof Object)
.map((key) => this.objectDeepKeys(obj[key]).map((k) => `${key}.${k}`))
.reduce((x, y) => x.concat(y), Object.keys(obj));
}
private set(obj: ConfigObject, path: string, value: any): void {
const keys = path.split('.');
let schema = obj;
keys.slice(0, -1).forEach((key) => {
if (!schema[key]) schema[key] = {};
schema = schema[key];
});
schema[keys[keys.length - 1]] = value;
}
private get(obj: ConfigObject, path: string): any {
const keys = path.split('.');
let schema = obj;
keys.slice(0, -1).forEach((key) => {
if (!schema[key]) schema[key] = {};
schema = schema[key];
});
return schema[keys[keys.length - 1]];
}
private merge(target: ConfigObject, source: ConfigObject): ConfigObject {
for (const key in source) {
if (
Object.prototype.hasOwnProperty.call(source, key) &&
typeof source[key] === 'object' &&
!Array.isArray(source[key])
) {
if (!target[key] || typeof target[key] !== 'object') {
target[key] = {};
}
this.merge(target[key], source[key]);
} else {
target[key] = source[key];
}
}
return target;
}
public _reload(): void {
// Handle config.defaults.json
try {
if (fs.existsSync(this.configDefaults)) {
const defaults = JSON.parse(fs.readFileSync(this.configDefaults, 'utf8'));
this.configData = this.merge(this.configData, defaults);
} else {
console.log(`Defaults file missing: ${this.configDefaults}`);
}
} catch (error) {
console.error(`Error reading defaults file: ${this.configDefaults}`);
}
// Handle config.json
try {
if (fs.existsSync(this.configLocal)) {
const local = JSON.parse(fs.readFileSync(this.configLocal, 'utf8'));
this.configData = this.merge(this.configData, local);
} else {
console.log(`local file missing: ${this.configLocal}`);
}
} catch (error) {
console.error(`Error reading local config file: ${this.configLocal}`);
}
// Apply environment variables
const keys = this.objectDeepKeys(this.configData);
for (const key of keys) {
const envKey = this.envPrefix + key.toUpperCase().replace(/\./g, '_');
const envValue = process.env[envKey];
if (envValue !== undefined) {
const parsedValue =
envValue === 'true' ? true : envValue === 'false' ? false : envValue;
this.set(this.configData, key, parsedValue); // Update configData
}
}
// Handle "file:" values
for (const key of keys) {
const value = this.get(this.configData, key)?.toString();
if (value?.startsWith('file:')) {
try {
const fileValue = fs.readFileSync(value.replace('file:', ''), 'utf8');
this.set(this.configData, key, fileValue);
} catch (error) {
console.error(`An unknown error occurred:${error}`);
}
}
}
// Assign the configData properties to the instance
Object.assign(this, this.configData);
}
public _show(): ConfigObject {
const redactedConfig = JSON.parse(JSON.stringify(this.configData));
const keys = this.objectDeepKeys(redactedConfig);
for (const key of keys) {
if (
/password|secret|token|key|apikey|apitoken|apisecret|user|username/i.test(key)
) {
this.set(redactedConfig, key, 'REDACTED');
}
}
return redactedConfig;
}
public toJSON(): Record<string, any> {
// Return only the properties of configData for JSON serialization
return { ...this.configData };
}
}
// Factory function for usage compatibility
export function createConfig(basePath?: string, envPrefix?: string): Config {
return new Config(basePath, envPrefix);
}
export default Config;
\ No newline at end of file
module.exports = {
preset: 'ts-jest', // Use ts-jest to handle TypeScript files
testEnvironment: 'node', // Set the test environment to Node.js
moduleFileExtensions: ['ts', 'js'], // Support both TypeScript and JavaScript files
testMatch: ['**/*.test.ts'], // Match test files ending with .test.ts
collectCoverage: true,
coverageReporters: [
"json",
"lcov",
"text",
"clover",
"html"
],
coverageDirectory: "docs/coverage"
};
\ No newline at end of file
This diff is collapsed.
{
"name": "@libs/config",
"version": "1.13.6",
"version": "1.14.0",
"description": "Simple Config with ENV Support",
"main": "index.js",
"main": "index.ts",
"scripts": {
"test": "jest",
"test": "jest --config jest.config.js",
"test:mutation": "stryker run"
},
"keywords": [
......@@ -13,28 +13,17 @@
],
"author": "Dominik Sigmund <dominik.sigmund@br.de>",
"license": "ISC",
"dependencies": {
"lodash.merge": "^4.6.2"
},
"devDependencies": {
"@stryker-mutator/core": "^8.2.6",
"@stryker-mutator/javascript-mutator": "^4.0.0",
"@stryker-mutator/jest-runner": "^8.2.6",
"@stryker-mutator/typescript": "^4.0.0",
"jest": "^29.7.0"
"@types/jest": "^29.5.14",
"jest": "^29.7.0",
"ts-jest": "^29.2.5",
"typescript": "^5.7.3"
},
"publishConfig": {
"@libs:registry": "https://gitlab.ard.de/api/v4/projects/919/packages/npm/"
},
"jest": {
"collectCoverage": true,
"coverageReporters": [
"json",
"lcov",
"text",
"clover",
"html"
],
"coverageDirectory": "docs/coverage"
}
}
{
"compilerOptions": {
"target": "ESNext",
"module": "CommonJS",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"outDir": "./dist",
"resolveJsonModule": true
},
"include": ["examples/**/*.ts", "index.ts", "examples/**/*.json"],
"exclude": ["node_modules"]
}
\ No newline at end of file