Skip to content

English__Theory__The Environment Object

Ramirez Vargas, José Pablo edited this page Nov 7, 2024 · 1 revision

The Environment Object

wj-config has been designed to collect all the information about environments in a single object, and this object serves as the single source of truth for environment information. This object is used internally by the package to provide certain features, and it is also exposed to the consumer so logic that is environment-specific or environment-aware can be easily coded.

This, like all other features of the package, is an opt-in feature and will only be present if the consumer of the package makes use of the includeEnvironment() function call while building the configuration.

Overloads of includeEnvironment()

There are two ways to include this handy environment object in the final configuration object:

  1. The consumer creates the environment object and then provides it.
  2. The consumer just provides the information necessary to create the object directly.

Creating an Environment Object

This (the first choice) is probably the more common one because often times, building the configuration requires knowledge of the current environment.

import wjConfig, { buildEnvironment } from "wj-config";

const env = buildEnvironment('myCurrentEnvironment', [
    'My',
    'List',
    'Of',
    'Possible',
    'Environments'
]);
const config = await wjConfig()
    .includeEnvironment(env)
    ...
    .build();

export default config;

Use this method of creating the environment object yourself if you have the need to use it during the configuration building process.

As seen in the code sample, creating the object is very simple: Pass the current environment name and a list of all possible environment names.

The latter might be confusing: Why do we want to specify all possible environment names? This is why:

  1. It allows the package to create handy testing functions for each environment (see Environment Testing).
  2. It enables the use of the shortcut builder function addPerEnvironment().
  3. It enables environment coverage checking (see Using forEnvironment()).
  4. It assists you, the developer, in catching any mistyped environment names.

The list of environments is actually optional. If not provided, then the default list of environments is used. This default list contains: Development, PreProduction and Production.

IMPORTANT: Even though providing the list of environment is optional, you are required to provide as current environment name a name that is in the list of possible environments, be it the default list or the list you provide. Long story short, as best practice, enumerate all your environments.

The typical use case looks something like this:

import wjConfig, { buildEnvironment } from "wj-config";
import mainConfig from './config.json';

const env = buildEnvironment('myCurrentEnvironment', [
    'My',
    'List',
    'Of',
    'Possible',
    'Environments'
]);
const config = await wjConfig()
    // Forward the created Environment object.
    .includeEnvironment(env)
    .addObject(mainConfig).name('Main')
    // Use the environment object to build your environment-specific data source.
    // This is referred to as "classic" per-environment configuration.
    .addFetched(`./config.${env.current.name}.json`, false)
    .build();

export default config;

Requesting Inclusion of the Environment Object

If there is no need to make use of the functionality that the environment object provides during configuration building, then use option 2: Just call includeEnvironment() with the necessary data to build the environment object:

import wjConfig from "wj-config";

const config = await wjConfig()
    .includeEnvironment('myCurrentEnvironment', [
        'My',
        'List',
        'Of',
        'Possible',
        'Environments'
    ])
    ...
    .build();

export default config;

This is the preferred way to go when you opt for the "conditional" per-environment configuration (see here).

Functionality Provided by the Environment Object

Ok, so we've seen one piece of information coming out of the environment object: The current environment's name.

Let's see about all the available functionality.

Current Environment Information

The environment object exposes the current property of type IEnvironmentDefinition. This means that the value of current is an object with 2 properties: name and traits.

The name property returns the current environment's name and this is what is seen in the example of use in the Creating an Environment Object section. Yes, its data type is string.

The traits property, on the other hand, is far more interesting: It can be a number or it can be an array of strings. It is used in Per-Trait Configuration and basically contains the list of all traits assigned to the current environment.

While traits is a concept developed to facilitate configuration building, it is not limited to configuration building. The traits property can be tested at any time for the presence of traits anywhere in your project, if you so need to or desire. The environment object provides 2 trait-testing functions, which are the subject of the next section.

Trait Testing

As stated in the previous section, the current environment can be assigned traits. Said traits, in return, can be tested for whatever reason. If you find the use of traits helpful for your project development, you'll find 2 trait-testing functions in the environment object: hasTraits() and hasAnyTrait().

The hasTraits() function returns true if the current environment's traits contain all of the specified traits.

The hasAnyTrait() function returns true if the current environment's traits contain any of the specified traits.

Let's pose an example of use.

Imagine you are creating a project that you customize and sell to multiple clients by hosting instances of the customized application. There are basic (or core) features and there are premium features. Not all customers pay for the premium features. Your job as a developer is to create code that only shows the premium pieces of the user interface if the client has paid for them.

Step 1: Define the Premium Trait

Individual traits can be bitmasked numbers or can be strings, making the value of the traits property a number or an array of strings because it is meant to hold all traits (plural form). Let's define a traits enumeration. Why if we only need the Premium trait? Because you might want to define more traits in the future. Preparing an enumeration from day 1 is just good practice.

export default Object.freeze({
    None: 0x0,
    Premium: 0x1 // Least significant bit:  0000 0001
});

Step 2: Test the Current Environment

Now in your UI project (React, Vue, Svelte, Preact, etc.) you can import the traits enumeration and test:

import myTraits from './myTraits.js';
import { environment } from './config.js';

// For just 1 trait, there is no difference between hasTraits() and hasAnyTrait().
if (environment.hasTraits(myTraits.Premium)) {
    // Ok to show the premium pieces of the UI.
}

That's it. This is how traits essentially work.

Defining the Current Environment's Traits

To define traits for the current environment, pass an object to buildEnvironment() instead of the current environment name. The shape of this object must satisfy the IEnvironmentDefinition interface.

To do this, you may create a new EnvironmentDefinition object, or just a POJO object with the name and traits properties:

import wjConfig, { buildEnvironment, EnvironmentDefinition } from "wj-config";
import mainConfig from './config.json';

// Bitmasked value of all traits assigned to the current environment:
const myCurTraits = parseInt(someEnvironmentVariable);
const curEnv = new EnvironmentDefinition('myCurrentEnvironment', myCurTraits);
// Pass the current environment definition as first argument.
const env = buildEnvironment(curEnv, [
    'My',
    'List',
    'Of',
    'Possible',
    'Environments'
]);

const config = await wjConfig()
    .includeEnvironment(env)
    ...
    .build();

export default config;

The variable someEnvironmentVariable represents any mechanism that your project uses to communicate the traits you want assigned to this deployed instance of the application. It typically would be a defined environment variable, but remember that in browser applications there is no environment, so it would be, for example, an artificial environment in, say, window.env.

Environment Testing

The environment object will carry environment-testing functions for all of the defined environment names passed to buildEnvironment(). The names of these functions follow the pattern isXXX(), where XXX is the environment's name.

Note: The environment name is capitalized in these environment-testing functions. Example: Environment name dev produces a function named isDev().

If you don't define your environment names, the default environment names will produce the isDevelopment(), isPreProduction() and isProduction() functions. Each function will return true if the current environment's name equals the name associated to the function.

Looking at the list of environments being used in the examples in this page, the environment object will contain the following:

import { environment } from './config.js';

if (environment.isMy()) {

}
else if (environment.isList()) {

}
else if (environment.isOf()) {

}
else if (environment.isPossible()) {

}
else if (environment.isEnvironments()) {

}
else {
    console.log('This application achieved the impossible!');
}

Clone this wiki locally