/modelize

Single utility function for your model instance to monitor changes, validate, and more...

Primary LanguageTypeScriptMIT LicenseMIT

@marianmeres/modelize

Single utility function modelize which proxies your model instance to monitor changes, validate, and more...

Install

npm i @marianmeres/modelize

Usage example

// SET UP ///////////////////////////////////////////////////////////////////////


// some model class
class User {
    // some public props ...
    firstname: string = '';
    lastname: string = '';
    // some methods ...
    whoami = () => [this.firstname, this.lastname].filter(Boolean).join(' ');
    // some getters ...
    get initials() {
        return [this.firstname.slice(0, 1), this.lastname.slice(0, 1)].join('').toUpperCase();
    }
}

// and model instance
const user = new User();
user.firstname = 'John';
user.lastname = 'Doe';
assert(user.whoami() === 'John Doe');
assert(user.initials === 'JD');


// BEGIN ACTUAL EXAMPLE /////////////////////////////////////////////////////////


// now create a new "modelized" version of the user instance
const modelized = modelize<User>(user);

// the "modelized" object is a new and different instance
assert(modelized !== user);

// but all props and methods are still available
assert(modelized.firstname === 'John');
assert(modelized.whoami() === 'John Doe');
assert(modelized.initials === 'JD');

// now, the new modelized version implements a bunch of new "virtual" utility methods
// (internally via proxy trap). They all start with "__" prefix to minimize the name
// collision risk with the original model class.
interface ModelizedMethods<T> {
    toJSON: () => Record<keyof T, any>;
    __hydrate: (data?: Partial<Record<keyof T, any>>, forceClean?: boolean) => any;
    __isDirty: () => (keyof T)[];
    __setClean: () => Modelized<T>;
    __setDirty: (keys: (keyof T)[]) => Modelized<T>;
    __getDirty: () => Partial<Record<keyof T, any>>;
    __validate: (assert?: boolean) => boolean;
    __setSchema: (schema: any) => Modelized<T>;
    __getSchema: () => any;
    __setValidator: (validator: Validator<T>) => Modelized<T>;
    __getValidator: () => Validator<T>;
    __setAllowAdditionalProps: (flag: boolean) => Modelized<T>;
    __onChange: (
        cb: (model: T, changed: { property: keyof T; old: any; new: any }) => any
    ) => Function;
    __pauseValidate: () => Modelized<T>;
    __resumeValidate: () => Modelized<T>;
}

// from now on, every instance update is monitored for changes
modelized.lastname = 'Lennon';
assert(modelized.__isDirty());
assert(_.isEqual({ lastname: 'Lennon' }, modelized.__getDirty()));

// and can be later marked as clean
modelized.__setClean();
assert(!modelized.__isDirty());

// you can also subscribe to changes
let log;
const unsubscribe = modelized.__onChange((model, changed) => (log = changed));
modelized.lastname = 'Wick';
assert(_.isEqual(log, { property: 'lastname', old: 'Lennon', new: 'Wick' }));
log = null; // reset

// and unsubscribe
unsubscribe();
modelized.lastname = 'McEnroe';
assert(log === null); // not subscribed anymore

// you can populate/hydrate multiple props at once, and by default, unknown props
// are silently ignored
modelized.__hydrate({ lastname: 'Depp', email: 'johny@depp.com' })
assert(modelized.lastname === 'Depp');
assert(modelized.email === undefined);

// but you can allow setting additional props if needed
modelized.__setAllowAdditionalProps(true);
modelized.__hydrate({ lastname: 'Cash', email: 'johny@cash.com' })
assert(modelized.lastname === 'Cash');
assert(modelized.email === 'johny@cash.com');

// changes are synced with the original user instance as well
assert(user.email === 'johny@cash.com');
assert(user.initials === 'JC');

// and, you can set json-schema to you model, to be auto validated all the time
modelized.__setSchema({
    type: 'object',
    properties: {
        firstname: { type: 'string' },
        lastname: { type: 'string' },
    },
});
assert(modelized.__validate());
assert.throws(() => (modelized.firstname = 123));

// if json-schema is not your thing, you can create your own validator function
modelized.__setValidator((model, schema, assert) => {
    const isJohn = !model.firstname || model.firstname === 'John';
    if (assert && !isJohn) throw new ModelizeValidationError('John only');
    return isJohn;
});
assert(modelized.__validate());

// only John is allowed
assert.throws(() => (modelized.firstname = 'Peter'));
assert(modelized.firstname === 'John');

It all works via shorthand notation and on POJO objects as well.

// modelize<T extends object>(
//     model: T,
//     data: Partial<Record<keyof T, any>> = {},
//     config: Partial<ModelizeConfig<T>> = {}
// ): Modelized<T>
const user = modelize(
    {}, // empty "pojo" instance
    { firstname: 'James', lastname: 'Bond' }, // initial data
    // `additionalProperties` must be set to `true` with empty pojos
    { additionalProperties: true, schema: null, validator: null } // config
);
assert(_.isEqual({ firstname: 'James', lastname: 'Bond' }, user.toJSON()));