CRUD mixins for JavaScript. Use cases: Manipulating data, creating plugin systems, and creating data-oriented coupling middleware.
The classes Mixin
, MixinPlugin
, and MixinCompositor
are exported by the mixin.js
module, and can be combined by your application to compose a data-oriented plugin system. Mixin
instances implement CRUD operations as methods.
new Mixin( type, { [ make = null, get = null, set = null, del = null ] });
When Mixin
is instantiated, the methods supplied to the constructor are assigned to the instance.
-
type
StringThe unique type definition string used to identify the mixin and its data.
Constructor parameters are optionally supplied via an object with the following properties:
make
(Optional) Function:- For creating new data of a specific type.
get
(Optional) Function:- For reading persistent data of a specific type.
set
(Optional) Function:- For writing persistent data of a specific type.
del
(Optional) Function:- For deleting persistent data of a specific type.
mixin.patchProperty( mixin2, accessor );
Implements compositing between instances of Mixin
; the method attempts to copy properties from the given object to the parent object if that property is missing from the parent.
mixin.patchMixin( mixin2 );
Calls this.patchProperty
on all of its own CRUD methods.
A real-world example of CRUD mixins is HockIt's config.js
module and its JSONConfig
mixin.
HockIt loads initial configuration data from a JSON file, and it does so via the mixin JSONConfig
. All methods in Mixin JSONConfig
are implemented by Plugin JSONConfigPlugin
(named config.js
).
Here is a very simplified example of how the plugin is implemented:
// Import the Mixin and MixinPlugin classes:
const { Mixin } = require("crudMixin.js");
// The filesystem path of the default configuration file:
const defaultConfPath = __dirname + "/config.json";
// A Mixin for creating the default configuration object:
const JSONConfig = new Mixin("JSONConfig", {
make: () => fs.readFileSync(defaultConfPath).toString("utf8");
});
// How the Mixin should be used:
const config = JSONConfig.make();
new MixinPlugin({ [ loadCondition = null, initPlugin = null, allowOverride = false, ...mixins ] });
Constructor parameters are supplied via an object with the following properties:
loadCondition
(Optional) Function:- When the given function returns false, the plugin will not be loaded.
initPlugin
(Optional) Function:- This function is arbitrarily executed after mixins are patched. Use this function to perform preparatory steps.
allowOverride
- When set to
true
, automatically marks all CRUD methods of all child mixins as overwritable.
- When set to
...mixins
Mixin:- Instances of
Mixin
.
- Instances of
In this example scenario, we construct a "guestbook" application by combining functionalities supplied by two different plugins.
The following mixin plugin exports a MyGreeting
mixin with its make
method defined.
// Import the Mixin and MixinPlugin classes:
const { Mixin, MixinPlugin } = require("../crudMixin.js");
// Instantiate a new Mixin with type definition "MyGreeting":
const MyGreeting = new Mixin("MyGreeting",
// Define the "make" method to output "Hello world!" or use a custom name:
make = (name) => `Hello ${name}!`
);
// Export the Mixin inside a new MixinPlugin:
module.exports = new MixinPlugin({
// Name of the plugin:
name: "MyGreetingPlugin",
// Version of the plugin:
version: "1.2.3",
// The mixin to be exported:
MyGreeting
});
The following mixin plugin exports a similar MyGreeting
mixin, however it lacks make
and instead supplies get
and set
methods.
The set
method logs greetings into a "guestbook" Map, whereas get
retrirves previously logged greetings.
// Import the Mixin and MixinPlugin classes:
const { Mixin, MixinPlugin } = require("../crudMixin.js");
// Local variable for storing the data:
const guestbook = new Map();
// Instantiate a new Mixin with type definition "MyGreeting":
const MyGreeting = new Mixin("MyGreeting",
// Define the "get" method to retrieve a greeting by name:
get = (name) => {
return guestbook.get(name);
},
// Define the "set" method to store a greeting by name:
set = (name, greeting = null) => {
guestbook.set(name, greeting);
}
);
// Export the Mixin inside a new MixinPlugin:
module.exports = new MixinPlugin({
name: "MyStoragePlugin",
version: "4.5.6",
MyGreeting
});
An instance of MixinCompositor
composites mixins together, effectively "mixing" them. If at least two mixins are of the same type, then the constructor or addMixin
method joins them together via calling the patchMixin
methods of each mixin. The resulting instance of MixinCompositor
assigns all given mixins to itself for easy access.
Mixins and mixin plugins can be manually added via methods addMixin
and addPlugin
.
Now in our main module (the business logic) we can combine the mixins and use them:
// Import the Mixin, MixinPlugin, and SetupMixin classes:
const { Mixin, MixinPlugin, MixinCompositor } = require("../crudMixin.js");
// Instantiate a new Mixin with type definition "MyGreeting".
// This instance lacks method definitions, but will be populated by "MixinCompositor":
const MyGreeting = new Mixin("MyGreeting");
// Define a plugin to contain the Mixins you intend to use:
const myPlugin = {
name: "MyGuestbookPlugin",
version: "7.8.9",
MyGreeting
};
// Cross-inherit the Mixins via MixinCompositor, which returns a Promise:
const setupPromise = new MixinCompositor(myPlugin);
// Now that the "MyGreeting" Mixin is cross-inherited between plugins, we can use it.
// This is the main logic of our application:
setupPromise.then(() => {
const name = "Testy McFly";
// Generate a new greeting string:
const greeting = MyGreeting.make(name);
// Add the greeting to the guestbook:
MyGreeting.set(greeting);
// Logs "Hello Testy McFly!" to console:
console.log(MyGreeting.get(name));
});
Mixin
is a class which implements CRUD methods for manipulating any non-volatile/persistent data.
In object-oriented programming, a mixin is a way to reuse a class's code in multiple class hierarchies. Mixins are a powerful concept that allows for flexible and modular code organization, and promoting code reuse.
In this context, CRUD operations are "mixed in" with the Mixin
objects that lack those methods. This combination ensures that each object contains an equivalent set of methods without overriding existing ones.
CRUD Mixins should be created as a single-instance uniqueness type within each module, and should be preserved in-memory for any subsequent needs. Instances of Mixin
should be invoked as a result of user input, but never created as a result of user input.
The patchProperty(mixin, accessor)
and patchMixin(mixin)
methods implement compositing between instances of Mixin
; the methods attempt to copy methods from the given mixin to the parent mixin if those methods are missing.
Instances of Mixin
store a write-protection descriptor object in the property writeProtectedMethods
, which is an object containing CRUD method accessors mapped to booleans to indicate which methods are write-protected. The write-protection descriptor is utilized in the method patchProperty
.
If the parent mixin already has a method, then patchProperty
and patchMixin
will opt to skip writing that method. If overwriting is desired anyways, then write-protection must be disabled for that method. That is because patchProperty
enforces a no-override principle when compositing mixins, such that by default each mixin is not allowed to overwrite another mixin's methods. Conversely, any locally declared instances of Mixin
can still have their own methods defined, and they won't be overwritten after they are exported. This also means that as mixins are patched, each missing method is patched semi-permanently and can't be patched again unless the restriction is bypassed.
The no-override restriction may be bypassed manually via three different ways:
- Exporting the parent
MixinPlugin
withallowOverride = true
. - Calling the
Mixin
constructor withallowOverride = true
as an input operand. - Calling the
Mixin
instance methodallowOverride()
:
- Example:
MyMixin.allowOverride(allowMake = true, allowGet = true, allowSet = true, allowDel = true)
patchProperty
allows a single overwrite of each CRUD method before overwrite-protecting them again.
Plugins are composed of exported MixinPlugin
instances which are later parsed by MixinCompositor
.
Each plugin module pre-loads functions into local instances of Mixin
which are then packaged into a MixinPlugin
instance and exported. The MixinPlugin
instances are later imported into an instance of MixinCompositor
.
A plugin module exports a MixinPlugin
instance which contains Mixin
instances and the following:
name
: A string name for the plugin. Helps to differentiate stack traces.version
: A semantic versioning string. It is arbitrary for now.allowOverride
(Optional): A boolean, if set totrue
, allows methods of the given mixins to be overridden at least once.initPlugin
(Optional): A function to execute after allMixin
instances have been prepared.loadCondition
(Optional): A function which cancels plugin loading if it returns falsy.