Skip to content

Commit

Permalink
✨ Recompute field values on the fly
Browse files Browse the repository at this point in the history
  • Loading branch information
skerit committed Nov 6, 2023
1 parent 114d059 commit 1fa23a9
Show file tree
Hide file tree
Showing 6 changed files with 171 additions and 4 deletions.
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 1.3.21 (WIP)

* Add `Schema#addComputedField()` method

## 1.3.20 (2023-11-04)

* Make `Base#callOrNext(name, args, next)` handle methods without callbacks
Expand All @@ -11,7 +15,6 @@
* Make the `Decimal` classes globally available
* Allow passing a custom context to `PathEvaluator#getValue(context)`
* Fix `TaskService#initSchedules()` attempting to recreate already existing system schedules
* Add `Schema#addComputedField()` method

## 1.3.19 (2023-10-18)

Expand Down
110 changes: 110 additions & 0 deletions lib/app/helper_model/document.js
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,25 @@ Document.setStatic(function setMethod(key, method, on_server) {
return Blast.Collection.Function.setMethod(this, key, method);
});

/**
* Set a getter for this computed field
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 1.3.21
* @version 1.3.21
*
* @param {String} name Name of the property
* @param {Boolean} on_server Also set on the server implementation
*/
Document.setStatic(function setComputedFieldGetter(name, on_server) {
this.setProperty(name, function getComputedFieldValue() {
this.recomputeFieldIfNecessary(name);
return this.$main[name];
}, function setComputedFieldValue(value) {
console.error('Can not set computed field "' + name + '" to', value);
}, on_server);
});

/**
* Set a getter for this field
*
Expand Down Expand Up @@ -858,6 +877,97 @@ Document.setMethod(function setValues(values) {

});

/**
* Recompute the given field if required
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 1.3.21
* @version 1.3.21
*
* @return {Pledge|undefined}
*/
Document.setMethod(function recomputeFieldIfNecessary(name, force = false) {

const field = this.$model.schema.computed_fields[name];

if (!field) {
return;
}

let options = field.options,
required_field_count = options.required_fields?.length || 0,
optional_field_count = options.optional_fields?.length || 0;

if (!force && this.$main[name] == null) {
// If the value is null, it hasn't been computed yet
// and we need to compute it
force = true;
}

if (!force) {
let has_changed = false;

if (required_field_count) {
for (let required_field of options.required_fields) {
if (this.hasChanged(required_field)) {
has_changed = true;
break;
}
}
}

if (!has_changed && optional_field_count) {
for (let optional_field of options.optional_fields) {
if (this.hasChanged(optional_field)) {
has_changed = true;
break;
}
}
}

if (!has_changed) {
return;
}
}

// Make sure the required fields are set
if (required_field_count) {
let has_value = true;

for (let required_field of options.required_fields) {
if (this[required_field] == null) {
has_value = false;
break;
}
}

if (!has_value) {
// If not all required field values are set,
// the result will also be undefined
this.$main[name] = undefined;
return;
}
}

let compute_method = options.compute_method;

if (typeof compute_method == 'string') {
compute_method = this[compute_method];
}

let result = compute_method.call(this);

if (Pledge.isThenable(result)) {
result.then(value => {
this.$main[name] = value;
});

return result;
} else {
this.$main[name] = result;
}
});

/**
* Recompute values of computed fields.
* This might be async. If it is, a pledge will be returned.
Expand Down
20 changes: 20 additions & 0 deletions lib/class/document.js
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,26 @@ Document.setStatic(function unDry(obj, cloned) {
return result;
});

/**
* Set the getter for this computed field
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 1.3.21
* @version 1.3.21
*
* @param {String} name Name of the property
* @param {Function} getter Optional getter function
* @param {Function} setter Optional setter function
*/
Document.setStatic(function setComputedFieldGetter(name) {
this.setProperty(name, function getComputedFieldValue() {
this.recomputeFieldIfNecessary(name);
return this.$main[name];
}, function setComputedFieldValue(value) {
console.error('Can not set computed field "' + name + '" to', value);
});
});

/**
* Set the getter for this field
*
Expand Down
4 changes: 2 additions & 2 deletions lib/class/model.js
Original file line number Diff line number Diff line change
Expand Up @@ -383,12 +383,12 @@ Model.setStatic(function addComputedField(name, type, options) {

if (is_new) {
// Add it to the Document class
this.Document.setFieldGetter(name);
this.Document.setComputedFieldGetter(name);

// False means it should not be set on the server implementation
// (because that's where it's coming from)
// Yes, this also sets private fields on the server-side client document.
this.ClientDocument.setFieldGetter(name, null, null, false);
this.ClientDocument.setComputedFieldGetter(name, null, null, false);
}

return field;
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "alchemymvc",
"description": "MVC framework for Node.js",
"version": "1.3.20",
"version": "1.3.21-alpha",
"author": "Jelle De Loecker <jelle@elevenways.be>",
"keywords": [
"alchemy",
Expand Down
34 changes: 34 additions & 0 deletions test/03-model.js
Original file line number Diff line number Diff line change
Expand Up @@ -700,6 +700,7 @@ describe('Model', function() {
const ComputedPerson = Function.inherits('Alchemy.Model', 'ComputedPerson');

ComputedPerson.constitute(function addFields() {
this.addField('number', 'Integer');
this.addField('firstname', 'String');
this.addField('lastname', 'String');
this.addComputedField('fullname', 'String', {
Expand All @@ -726,6 +727,7 @@ describe('Model', function() {

let doc = Model.get('ComputedPerson').createDocument();

doc.number = 1;
doc.firstname = 'Jelle';
doc.lastname = 'De Loecker';

Expand All @@ -738,6 +740,38 @@ describe('Model', function() {

assert.strictEqual(doc.fullname, 'Jellie De Loecker');
});

it('should update synchronous field values on get', async function() {

let doc = Model.get('ComputedPerson').createDocument();

doc.number = 2;
doc.firstname = 'Jelle';
doc.lastname = 'De Loecker';

assert.strictEqual(doc.fullname, 'Jelle De Loecker');

doc.firstname = 'Jellie';

assert.strictEqual(doc.fullname, 'Jellie De Loecker');

await doc.save();
assert.strictEqual(doc.fullname, 'Jellie De Loecker');
assert.strictEqual(doc.$main.fullname, 'Jellie De Loecker');

doc.$main.fullname = null;
assert.strictEqual(doc.fullname, 'Jellie De Loecker');
assert.strictEqual(doc.$main.fullname, 'Jellie De Loecker');

doc.$main.fullname = null;
await doc.save();
assert.strictEqual(doc.fullname, 'Jellie De Loecker');
assert.strictEqual(doc.$main.fullname, 'Jellie De Loecker');

doc = await Model.get('ComputedPerson').findByValues({number: 2});
assert.strictEqual(doc.fullname, 'Jellie De Loecker');
assert.strictEqual(doc.$main.fullname, 'Jellie De Loecker');
});
});

/**
Expand Down

0 comments on commit 1fa23a9

Please sign in to comment.