alchemymvc
Version:
MVC framework for Node.js
744 lines (592 loc) • 18.2 kB
JavaScript
/**
* Schema fields are nested schema's
*
* @constructor
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 0.2.0
* @version 1.3.16
*/
const SchemaField = Function.inherits('Alchemy.Field', function Schema(schema, name, options) {
if (!options) {
options = {};
}
// If the contents are array-like, the field itself can't be
if (this.force_array_contents) {
options.is_array = false;
}
Schema.super.call(this, schema, name, options);
if (options.schema == null || typeof options.schema != 'object') {
return;
}
// Field schema
this.field_schema = options.schema;
if (!this.field_schema.name) {
this.field_schema.setName(name);
}
if (schema) {
if (!this.field_schema.parent || schema.root_schema == this.field_schema.parent) {
this.field_schema.setParent(schema);
} else {
// It's possible the given schema already had the correct parent
// But if that's not the case, throw an error
if (this.field_schema.parent != schema) {
throw new Error('Unable to re-use a schema for field "' + name + '", use `schema.clone()`!');
}
}
if (!this.field_schema.model_name) {
this.field_schema.setModel(schema.model_name);
}
}
});
SchemaField.setDeprecatedProperty('fieldSchema', 'field_schema');
/**
* Schema fields are stored as objects
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 1.1.0
* @version 1.1.0
*/
SchemaField.setDatatype('object');
/**
* Is this schema field always an array?
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 1.3.16
* @version 1.3.16
*
* @return {boolean}
*/
SchemaField.setProperty('force_array_contents', false);
/**
* Get the subschema of this field
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 0.2.0
* @version 0.2.0
*
* @return {string}
*/
SchemaField.setProperty(function root_model() {
return this.parent_schema.root_model || this.parent_schema.model_name;
});
/**
* Does this field need to be translated in some way?
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 1.1.4
* @version 1.3.0
*
* @type {boolean}
*/
SchemaField.setProperty(function requires_translating() {
if (this.is_translatable) {
return true;
}
if (this.field_schema) {
return !!this.field_schema.has_translations;
}
return false;
});
/**
* Deprecated alias
*
* @deprecated
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 0.2.0
* @version 1.4.0
*
* @param {Alchemy.OperationalContext.Schema} context
*
* @return {Alchemy.Schema|Pledge<Alchemy.Schema>}
*/
SchemaField.setMethod(function getSubschema(context) {
return this.getSubSchema(context);
});
/**
* Get the subschema of this field
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 0.2.0
* @version 1.4.0
*
* @param {Alchemy.OperationalContext.Schema} context
*
* @return {Alchemy.Schema|Pledge<Alchemy.Schema>}
*/
SchemaField.setMethod(function getSubSchema(context) {
let schema = this.options.schema;
// If schema is a string,
// it needs to be extracted from another field's value
if (typeof schema == 'string') {
if (context) {
context = context.createChild();
} else {
context = new Classes.Alchemy.OperationalContext.Schema();
context.setSchema(this.schema);
}
context.setSubSchemaPath(schema);
schema = this.resolveSchemaPath(context);
}
return schema;
});
/**
* Get the subschema of a given schema
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 1.3.21
* @version 1.4.0
*
* @param {Alchemy.OperationalContext.Schema} context
*
* @return {Alchemy.Schema|Pledge<Alchemy.Schema>}
*/
SchemaField.setMethod(function resolveSchemaPath(context) {
const schema_path = context.getSubSchemaPath();
if (typeof schema_path != 'string') {
return schema_path;
}
// Get the schema of the current holder
let context_schema = context.getSchema();
// Get the current holder
let holder = context.getHolder();
// When there are 2 pieces, the second piece is the property name
let pieces = schema_path.split('.');
// The first piece is the external field name
let external_field_name = pieces.shift();
// The rest of the pieces is the property path
let property_name = pieces.join('.') || 'schema';
// Get that other field by its name
let field = context_schema.getField(external_field_name);
// Possible association
let association;
if (!field) {
association = context_schema.getAssociation(external_field_name);
} else if (field.is_foreign_key) {
association = field.getAssociation();
}
if (association) {
let new_context = context.createChild();
new_context.setValuePropertyName(property_name);
new_context.setAssociation(association);
return this.getSubSchemaFromAssociation(new_context);
}
if (!field) {
console.error('Failed to get subschema', external_field_name, 'of', context_schema, context);
return null;
}
// Get the values that field can have (probably an enum)
let values = field.getValues();
if (values == null) {
return null;
}
// Now get the actual external value from the record
let record_value = field.getRecordValue(holder);
// I'm not sure if this will help
if (record_value == null) {
record_value = field.getRecordValue(holder, external_field_name);
}
// If there is no value, return false
if (!record_value) {
return false;
}
let schema;
// Get the correct field value
let enum_value = values.get(record_value);
if (!enum_value) {
schema = false;
} else if (enum_value[property_name]) {
schema = enum_value[property_name];
} else if (enum_value.schema) {
schema = enum_value.schema;
} else if (enum_value.value) {
if (enum_value.value.via_model) {
let sub_context = context.createChild();
sub_context.setRemoteModelName(enum_value.value.via_model);
sub_context.setRemoteForeignKey(enum_value.value.foreign_key);
sub_context.setLocalValue(enum_value.value.pk || record_value);
sub_context.setValuePropertyName(property_name);
// The value is actually referring to a specific record
return this.getSubSchemaFromModel(sub_context);
}
schema = enum_value.value[property_name] || enum_value.value.schema;
} else {
console.warn('Failed to find schema path', JSON.stringify(schema_path), 'in holder', holder);
console.warn(' »»', 'Enum values:', enum_value);
console.warn(' »»', 'Of field:', field);
console.warn(' »»', 'Field value:', record_value);
console.warn(' »»', 'Property name:', property_name);
}
return schema;
});
/**
* Get the subschema via an association
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 0.2.0
* @version 1.4.0
*
* @param {Alchemy.OperationalContext.Schema} context
*
* @return {Pledge<Schema>|Schema}
*/
SchemaField.setMethod(function getSubSchemaFromAssociation(context) {
let holder = context.getHolder(),
association = context.getAssociation();
let local_value = holder[association.options.local_key];
if (!local_value) {
return false;
}
context.setLocalValue(local_value);
context.setRemoteModelName(association.model_name);
context.setRemoteForeignKey(association.options.foreign_key);
return this.getSubSchemaFromModel(context);
});
/**
* Get the subschema via an association
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 0.2.0
* @version 1.4.0
*
* @param {Alchemy.OperationalContext.Schema} context
*
* @return {Pledge<Schema>|Schema}
*/
SchemaField.setMethod(function getSubSchemaFromModel(context) {
const model_name = context.getRemoteModelName();
const model = alchemy.getModel(model_name);
if (!model) {
return false;
}
let remote_context = context.createChild();
remote_context.setExternalField(this);
remote_context.setOurFieldName(context.getRemoteForeignKey() || '_id');
remote_context.setOurFieldValue(context.getLocalValue());
remote_context.setSubSchemaPath(context.getValuePropertyName());
let remote_request = model.resolveRemoteSchemaRequest(remote_context);
let pledge = Pledge.Swift.waterfall(remote_request, result => {
if (!result) {
return null;
}
// If it already is a schema, return that
if (Classes.Alchemy.Client.Schema.isSchema(result)) {
return result;
}
let found_schema = this.resolveSchemaPath(model.schema, result, path, path);
return found_schema;
});
return pledge;
});
/**
* Cast all the subschema values using their _toDatasource method
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 0.2.0
* @version 1.4.0
*
* @param {Alchemy.OperationalContext.SaveFieldToDatasource} context
* @param {*} value
*
* @return {Pledge<*>|*}
*/
SchemaField.setMethod(function _toDatasource(context, value) {
if (!this.force_array_contents) {
return this._toDatasourceFromValue(context, value);
}
if (!Array.isArray(value)) {
value = Array.cast(value);
context.setWorkingValue(value);
}
// @TODO: What about the holder?
let tasks = value.map(entry => this._toDatasourceFromValue(context.withWorkingValue(entry), entry));
return Pledge.Swift.parallel(tasks);
});
/**
* Cast all the subschema values using their _toDatasource method
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 0.2.0
* @version 1.4.0
*
* @param {Alchemy.OperationalContext.SaveFieldToDatasource} context
* @param {Object} value
*
* @return {Pledge<*>|*}
*/
SchemaField.setMethod(function _toDatasourceFromValue(context, value) {
let schema_context = new Classes.Alchemy.OperationalContext.Schema(context);
schema_context.setSchema(this.schema);
return Pledge.Swift.waterfall(
this.getSubSchema(schema_context),
sub_schema => context.withValueOfSubSchema(value, sub_schema),
new_context => this._toDatasourceFromValueWithSubSchema(new_context, value),
);
});
/**
* Cast all the subschema values using their _toDatasource method
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 0.2.0
* @version 1.4.0
*
* @param {Alchemy.OperationalContext.SaveFieldToDatasource} context
* @param {Object} value
*
* @return {Pledge<*>|*}
*/
SchemaField.setMethod(function _toDatasourceFromValueWithSubSchema(context, value) {
let sub_schema = context.getSchema(),
datasource = context.getDatasource();
// If the sub schema has been found, return it now
if (sub_schema) {
let result = datasource.toDatasource(context);
return result;
}
if (sub_schema === false) {
return null;
}
// @WARNING:
// Doing a model.save with ONLY updates can break subschema behaviour!
// If it has not been found, and the linked schema is actually a string,
// it's possible the linked field was not provided
if (typeof this.options.schema == 'string') {
let model = this.root_model;
if (model) {
model = Model.get(model);
let pledge = new Pledge.Swift();
model.find('first', {document: false, fields: [this.options.schema]}, function gotRecord(err, result) {
if (err) {
return pledge.reject(err);
}
if (!result.length) {
log.warning('Subschema was not found for', that.name, 'in', model.name, 'model');
return pledge.resolve(datasource.toDatasource(context));
}
let holder = context.getHolder();
// Add the newly found data
let temp = Object.assign({}, holder);
let record = {};
record[that.schema.name] = temp;
Object.assign(record, result[0]);
// Try getting the schema again
sub_schema = that.getSubSchema(record);
Pledge.Swift.done(sub_schema, (err, sub_schema) => {
if (err) {
return pledge.reject(err);
}
context.setSchema(sub_schema);
pledge.resolve(datasource.toDatasource(context));
});
});
return pledge;
} else {
log.warn('Model not found for subschema', this.options.schema);
}
}
log.warning('Model and subschema were not found for', that.path);
return Pledge.Swift.done(datasource.toDatasource(context), callback);
});
/**
* Turn datasource data into app data
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 0.2.0
* @version 1.4.0
*
* @param {Alchemy.OperationalContext.ReadFieldFromDatasource} context
* @param {*} value
*
* @return {Pledge<*>|*}
*/
SchemaField.setMethod(function _toApp(context, value) {
if (!this.force_array_contents) {
return this._toAppFromValue(context, value);
}
value = Array.cast(value);
let tasks = value.map(entry => this._toAppFromValue(context.withWorkingValue(entry), entry));
let result = Pledge.Swift.waterfall(
Pledge.Swift.parallel(tasks),
result => this.cast(result)
);
return result;
});
/**
* Turn datasource data into app data
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 0.2.0
* @version 1.4.0
*
* @param {Alchemy.OperationalContext.ReadFieldFromDatasource} context
* @param {*} value
*
* @return {Pledge<*>|*}
*/
SchemaField.setMethod(function _toAppFromValue(context, value) {
let schema_context = new Classes.Alchemy.OperationalContext.Schema(context);
schema_context.setSchema(this.schema);
let result = Pledge.Swift.waterfall(
this.getSubSchema(schema_context),
sub_schema => context.withValueOfSubSchema(value, sub_schema),
new_context => this._toAppFromValueWithSubSchema(new_context, value)
);
return result;
});
/**
* Turn datasource data into app data
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 0.2.0
* @version 1.4.0
*
* @param {Alchemy.OperationalContext.ReadFieldFromDatasource} context
* @param {*} value
*
* @return {Pledge<*>|*}
*/
SchemaField.setMethod(function _toAppFromValueWithSubSchema(context, value) {
let sub_schema = context.getSchema();
// Explicit false means there is no schema at the moment,
// and the value can be ignored
if (sub_schema === false) {
return null;
}
const datasource = context.getDatasource();
let that = this,
Dummy,
item,
name;
let options = context.getQueryOptions() || {};
let criteria = context.getCriteria();
// Don't get schema associated records if recursive is disabled
let get_associations_recursive_level = options.recursive;
if (get_associations_recursive_level == null) {
if (this.options?.recursive != null) {
get_associations_recursive_level = this.options.recursive;
} else {
get_associations_recursive_level = 1;
}
}
if (get_associations_recursive_level && Blast.isBrowser) {
// @TODO: this will mostly fail on the browser, so disable it for now.
// Maybe make it configurable later
get_associations_recursive_level = 0;
}
// If the sub schema has been found, return it now
if (sub_schema) {
value = Swift.map(value, (field_value, field_name) => {
let field_context = context.getFieldContext(field_name);
if (field_context) {
return datasource.valueToApp(field_context);
}
});
} else {
console.warn('Failed to find sub schema for', this.name, 'in', record);
}
return Swift.waterfall(
value,
value => {
// Get associated records if the subschema has associations defined
if (get_associations_recursive_level && this.field_schema && !Object.isEmpty(this.field_schema.associations)) {
name = this.name + 'FieldModel';
Dummy = alchemy.getModel('Model', false);
Dummy = new Dummy({
root_model : this.root_model,
name : name
});
item = {};
item[name] = value;
let sub_criteria = Dummy.find();
// Disable generating Document instances
// if the original find did the same
sub_criteria.setOption('document', options.document);
sub_criteria.setOption('original_query', criteria);
sub_criteria.setOption('_root_data', options._root_data);
sub_criteria.setOption('_parent_field', that);
sub_criteria.setOption('_parent_model', that.schema.model_name);
sub_criteria.setOption('recursive', get_associations_recursive_level);
sub_criteria.setOption('associations', this.field_schema.associations);
// @todo: inherit other original find options?
let pledge = new Pledge.Swift();
Dummy.addAssociatedDataToRecord(sub_criteria, item, function gotAssociatedData(err, result) {
if (err) {
return pledge.reject(err);
}
let key;
for (key in result) {
if (key == name) {
continue;
}
value[key] = result[key];
}
pledge.resolve(value);
});
return pledge;
}
return this.castEntry(value);
}
);
});
/**
* Translate the given value
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 1.1.4
* @version 1.3.0
*/
SchemaField.setMethod(function translateRecord(prefixes, record, allow_empty) {
// Maybe the entire schema is translatable?
if (this.options.translatable) {
translateRecord.super.call(this, prefixes, record, allow_empty);
}
if (!record.$translated_fields) {
record.$translated_fields = {};
}
// Turn it into an array, some already are (is_array fields)
let subject = Array.cast(record[this.name]),
schema_record,
count = -1;
for (schema_record of subject) {
count++;
if (this.field_schema.has_translations && schema_record) {
let field_name,
field;
for (field_name in this.field_schema.translatable_fields) {
field = this.field_schema.translatable_fields[field_name];
field.translateRecord(prefixes, schema_record, allow_empty);
if (schema_record.$translated_fields) {
for (let key in schema_record.$translated_fields) {
let path = this.name;
if (this.is_array) {
path += '.' + count;
}
path += '.' + key;
record.$translated_fields[path] = schema_record.$translated_fields[key];
}
}
}
}
}
});
/**
* Cast an entry
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 1.3.16
* @version 1.3.16
*/
SchemaField.setMethod(function castEntry(value, to_datasource) {
return value;
});
/**
* Cast the value to a document
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 1.1.5
* @version 1.1.5
*/
SchemaField.setCastFunction(function cast(value, to_datasource) {
return value;
});