Changeset View
Changeset View
Standalone View
Standalone View
lib/chip-types/collection.js
"use strict"; | |||||
const locreq = require("locreq")(__dirname); | const locreq = require("locreq")(__dirname); | ||||
const Promise = require("bluebird"); | const Promise = require("bluebird"); | ||||
const assert = require("assert"); | const assert = require("assert"); | ||||
const merge = require("merge"); | const merge = require("merge"); | ||||
const clone = require("clone"); | const clone = require("clone"); | ||||
const Errors = locreq("lib/response/error.js"); | const Errors = locreq("lib/response/error.js"); | ||||
const Chip = require("./chip.js"); | const Chip = require("./chip.js"); | ||||
const Field = require("./field.js"); | const Field = require("./field.js"); | ||||
const CalculatedField = require("./calculated-field.js"); | const CalculatedField = require("./calculated-field.js"); | ||||
const AccessStrategy = require("./access-strategy.js"); | const AccessStrategy = require("./access-strategy.js"); | ||||
const AccessStrategyType = locreq("lib/chip-types/access-strategy-type.js"); | const AccessStrategyType = locreq("lib/chip-types/access-strategy-type.js"); | ||||
const Collection = function(app, declaration) { | const Collection = function(app, declaration) { | ||||
if (typeof declaration === "string") { | if (typeof declaration === "string") { | ||||
return app.ChipManager.get_chip("collection", declaration); | return app.ChipManager.get_chip("collection", declaration); | ||||
} else if (declaration instanceof Collection) { | } else if (declaration instanceof Collection) { | ||||
return declaration; | return declaration; | ||||
} | } | ||||
const self = this; | |||||
Chip.call(this, "collection", declaration.name); | Chip.call(this, "collection", declaration.name); | ||||
this.app = app; | this.app = app; | ||||
this.name = declaration.name; | this.name = declaration.name; | ||||
this.fields = {}; | this.fields = {}; | ||||
this.human_readable_name = declaration.human_readable_name; | this.human_readable_name = declaration.human_readable_name; | ||||
this.summary = declaration.summary; | this.summary = declaration.summary; | ||||
this.access_strategy = { | this.access_strategy = { | ||||
default: new AccessStrategy(app, "public", {}), | default: new AccessStrategy(app, "public", {}), | ||||
Show All 37 Lines | if (declaration) { | ||||
this.set_access_strategy(declaration.access_strategy); | this.set_access_strategy(declaration.access_strategy); | ||||
} | } | ||||
}; | }; | ||||
Collection.type_name = "collection"; | Collection.type_name = "collection"; | ||||
Collection.pure = { | Collection.pure = { | ||||
add_field: function( | add_field(app, field_type, fields, field_declaration, collection) { | ||||
app, | |||||
field_type, | |||||
fields, | |||||
field_declaration, | |||||
collection | |||||
) { | |||||
const field_object = new Field( | const field_object = new Field( | ||||
app, | app, | ||||
field_declaration, | field_declaration, | ||||
field_type, | field_type, | ||||
collection | collection | ||||
); | ); | ||||
const field_name = field_object.name; | const field_name = field_object.name; | ||||
if (!fields[field_name]) { | if (fields[field_name]) { | ||||
fields[field_name] = field_object; | |||||
} else { | |||||
throw new Errors.DeveloperError( | throw new Errors.DeveloperError( | ||||
`Duplicate field names: "${field_name}" in collection: "${ | `Duplicate field names: "${field_name}" in collection: "${ | ||||
field_type.name | field_type.name | ||||
}"` | }"` | ||||
); | ); | ||||
} else { | |||||
fields[field_name] = field_object; | |||||
} | } | ||||
}, | }, | ||||
add_fields: function(app, field_type, fields, field_declarations_array) { | add_fields(app, field_type, fields, field_declarations_array) { | ||||
for (const i in field_declarations_array) { | for (const i in field_declarations_array) { | ||||
const declaration = field_declarations_array[i]; | const declaration = field_declarations_array[i]; | ||||
Collection.pure.add_field(app, field_type, fields, declaration); | Collection.pure.add_field(app, field_type, fields, declaration); | ||||
} | } | ||||
}, | }, | ||||
add_calculated_field: function( | add_calculated_field( | ||||
app, | app, | ||||
collection, | collection, | ||||
calc_field_name, | calc_field_name, | ||||
calc_field_type_declaration, | calc_field_type_declaration, | ||||
calc_field_type_params | calc_field_type_params | ||||
) { | ) { | ||||
collection.calculated_fields[calc_field_name] = new CalculatedField( | collection.calculated_fields[calc_field_name] = new CalculatedField( | ||||
app, | app, | ||||
calc_field_name, | calc_field_name, | ||||
calc_field_type_declaration, | calc_field_type_declaration, | ||||
calc_field_type_params | calc_field_type_params | ||||
); | ); | ||||
}, | }, | ||||
/* eslint-disable no-shadow */ | |||||
add_special_filters(Collection, named_filters) { | add_special_filters(Collection, named_filters) { | ||||
Collection.named_filters = named_filters; | Collection.named_filters = named_filters; | ||||
}, | }, | ||||
get_unknown_field_errors: function(field_type_name, fields, values) { | get_unknown_field_errors(field_type_name, fields, values) { | ||||
const validation_errors = {}; | const validation_errors = {}; | ||||
for (const field_name in values) { | for (const field_name in values) { | ||||
if (fields[field_name] === undefined) { | if (fields[field_name] === undefined) { | ||||
validation_errors[field_name] = new Errors.ValidationError( | validation_errors[field_name] = new Errors.ValidationError( | ||||
`Unknown field '${field_name}' for resource-type '${field_type_name}'` | `Unknown field '${field_name}' for resource-type '${field_type_name}'` | ||||
); | ); | ||||
} | } | ||||
} | } | ||||
return validation_errors; | return validation_errors; | ||||
}, | }, | ||||
get_missing_values_checker: function( | get_missing_values_checker( | ||||
fields, | fields, | ||||
values, | values, | ||||
assume_delete_value_on_missing_key, | assume_delete_value_on_missing_key, | ||||
old_values | old_values | ||||
) { | ) { | ||||
if (assume_delete_value_on_missing_key) { | if (assume_delete_value_on_missing_key) { | ||||
return function(field_name) { | return function(field_name) { | ||||
return ( | return ( | ||||
fields[field_name].required && | fields[field_name].required && | ||||
values[field_name] === undefined | values[field_name] === undefined | ||||
); | ); | ||||
}; | }; | ||||
} else { | } | ||||
return function(field_name) { | return function(field_name) { | ||||
return ( | return ( | ||||
fields[field_name].required && | fields[field_name].required && | ||||
values[field_name] === undefined && | values[field_name] === undefined && | ||||
old_values[field_name] === undefined | old_values[field_name] === undefined | ||||
); | ); | ||||
}; | }; | ||||
} | |||||
}, | }, | ||||
get_missing_field_values_errors: function( | get_missing_field_values_errors( | ||||
fields, | fields, | ||||
values, | values, | ||||
assume_delete_value_on_missing_key, | assume_delete_value_on_missing_key, | ||||
old_values | old_values | ||||
) { | ) { | ||||
const errors = {}; | const errors = {}; | ||||
const checker_fn = Collection.pure.get_missing_values_checker( | const checker_fn = Collection.pure.get_missing_values_checker( | ||||
fields, | fields, | ||||
values, | values, | ||||
assume_delete_value_on_missing_key, | assume_delete_value_on_missing_key, | ||||
old_values | old_values | ||||
); | ); | ||||
return Promise.filter(Object.keys(fields), checker_fn) | return Promise.filter(Object.keys(fields), checker_fn) | ||||
.each(function(field_name) { | .each(field_name => { | ||||
errors[field_name] = new Errors.ValidationError( | errors[field_name] = new Errors.ValidationError( | ||||
`Missing value for field '${field_name}'` | `Missing value for field '${field_name}'` | ||||
); | ); | ||||
}) | }) | ||||
.then(function() { | .then(() => { | ||||
return errors; | return errors; | ||||
}); | }); | ||||
}, | }, | ||||
get_invalid_field_values_errors: function( | get_invalid_field_values_errors(fields, context, values, old_values) { | ||||
fields, | |||||
context, | |||||
values, | |||||
old_values | |||||
) { | |||||
const errors = {}; | const errors = {}; | ||||
const promises = []; | const promises = []; | ||||
for (const field_name in values) { | for (const field_name in values) { | ||||
if (fields[field_name]) { | if (fields[field_name]) { | ||||
const value = values[field_name]; | const value = values[field_name]; | ||||
if (value === "") continue; | if (value === "") continue; | ||||
const old_value = old_values | const old_value = old_values | ||||
? old_values[field_name] | ? old_values[field_name] | ||||
: undefined; | : undefined; | ||||
const promise = fields[field_name] | const promise = fields[field_name] | ||||
.is_proper_value(context, value, old_value) | .is_proper_value(context, value, old_value) | ||||
.catch(function(error) { | .catch(error => { | ||||
if ( | if ( | ||||
typeof error === "string" || | typeof error === "string" || | ||||
error.type === "validation" | error.type === "validation" | ||||
) { | ) { | ||||
errors[field_name] = new Errors.ValidationError( | errors[field_name] = new Errors.ValidationError( | ||||
error | error | ||||
); | ); | ||||
} else { | } else { | ||||
throw error; | throw error; | ||||
} | } | ||||
}); | }); | ||||
promises.push(promise); | promises.push(promise); | ||||
} | } | ||||
} | } | ||||
return Promise.all(promises).then(function() { | return Promise.all(promises).then(() => { | ||||
return errors; | return errors; | ||||
}); | }); | ||||
}, | }, | ||||
get_missing_required_field_values: function(fields, new_values) { | get_missing_required_field_values(fields, new_values) { | ||||
const errors = {}; | const errors = {}; | ||||
for (const field_name in new_values) { | for (const field_name in new_values) { | ||||
if ( | if ( | ||||
fields[field_name] && | fields[field_name] && | ||||
fields[field_name].required && | fields[field_name].required && | ||||
new_values[field_name] === "" | new_values[field_name] === "" | ||||
) { | ) { | ||||
errors[field_name] = new Errors.ValidationError( | errors[field_name] = new Errors.ValidationError( | ||||
`Missing value for required field: '${field_name}'` | `Missing value for required field: '${field_name}'` | ||||
); | ); | ||||
} | } | ||||
} | } | ||||
return errors; | return errors; | ||||
}, | }, | ||||
validate_field_values: function( | validate_field_values( | ||||
field_type_name, | field_type_name, | ||||
fields, | fields, | ||||
context, | context, | ||||
assume_delete_value_on_missing_key, | assume_delete_value_on_missing_key, | ||||
new_values, | new_values, | ||||
old_values | old_values | ||||
) { | ) { | ||||
const errors_array = [ | const errors_array = [ | ||||
Show All 17 Lines | const errors_array = [ | ||||
context, | context, | ||||
new_values, | new_values, | ||||
old_values | old_values | ||||
), | ), | ||||
]; | ]; | ||||
return Promise.all(errors_array) | return Promise.all(errors_array) | ||||
.reduce(merge) | .reduce(merge) | ||||
.then(function(errors) { | .then(errors => { | ||||
const user_errors = {}; | const user_errors = {}; | ||||
const non_user_errors = {}; | const non_user_errors = {}; | ||||
for (const field_name in errors) { | for (const field_name in errors) { | ||||
const error = errors[field_name]; | const error = errors[field_name]; | ||||
if (error.is_user_fault) { | if (error.is_user_fault) { | ||||
user_errors[field_name] = error; | user_errors[field_name] = error; | ||||
} else { | } else { | ||||
non_user_errors[field_name] = error; | non_user_errors[field_name] = error; | ||||
} | } | ||||
} | } | ||||
const non_user_errors_amount = Object.keys(non_user_errors) | const non_user_errors_amount = Object.keys(non_user_errors) | ||||
.length; | .length; | ||||
if (non_user_errors_amount > 0) { | if (non_user_errors_amount > 0) { | ||||
throw non_user_errors[Object.keys(non_user_errors)[0]]; | throw non_user_errors[Object.keys(non_user_errors)[0]]; | ||||
} | } | ||||
const user_errors_amount = Object.keys(user_errors).length; | const user_errors_amount = Object.keys(user_errors).length; | ||||
if (user_errors_amount > 0) { | if (user_errors_amount > 0) { | ||||
throw new Errors.ValidationError( | throw new Errors.ValidationError( | ||||
"There are problems with some of the provided values.", | "There are problems with some of the provided values.", | ||||
user_errors | user_errors | ||||
); | ); | ||||
} | } | ||||
}); | }); | ||||
}, | }, | ||||
encode_field_values: async function(fields, context, body, old_body) { | async encode_field_values(fields, context, body, old_body) { | ||||
const promises = {}; | const promises = {}; | ||||
for (let field_name in fields) { | for (let field_name in fields) { | ||||
const field = fields[field_name]; | const field = fields[field_name]; | ||||
const type = field.type; | const type = field.type; | ||||
if ( | if ( | ||||
!body[field_name] && | !body[field_name] && | ||||
type.get_default_value && | type.get_default_value && | ||||
(!old_body || old_body[field_name] === undefined) | (!old_body || old_body[field_name] === undefined) | ||||
Show All 11 Lines | for (const field_name in body) { | ||||
context, | context, | ||||
current_value, | current_value, | ||||
old_value | old_value | ||||
); | ); | ||||
} | } | ||||
} | } | ||||
return Promise.props(promises); | return Promise.props(promises); | ||||
}, | }, | ||||
get_specification: function( | get_specification( | ||||
name, | name, | ||||
human_readable_name, | human_readable_name, | ||||
summary, | summary, | ||||
fields, | fields, | ||||
with_validators | with_validators | ||||
) { | ) { | ||||
// with_validators:boolean - whether to include validator functions in field descriptions. Warning! If set to true, the output is not serializable in JSON. | // with_validators:boolean - whether to include validator functions in field descriptions. Warning! If set to true, the output is not serializable in JSON. | ||||
const collection_specification = {}; | const collection_specification = {}; | ||||
for (const field_name in fields) { | for (const field_name in fields) { | ||||
const field_specification = fields[field_name].get_specification( | const field_specification = fields[field_name].get_specification( | ||||
with_validators | with_validators | ||||
); | ); | ||||
collection_specification[field_name] = field_specification; | collection_specification[field_name] = field_specification; | ||||
} | } | ||||
const specification = { | const specification = { | ||||
name: name, | name, | ||||
human_readable_name: human_readable_name, | human_readable_name, | ||||
summary: summary, | summary, | ||||
fields: collection_specification, | fields: collection_specification, | ||||
}; | }; | ||||
return specification; | return specification; | ||||
}, | }, | ||||
set_access_strategy: function(app, collection, strategy_declaration) { | set_access_strategy(app, collection, strategy_declaration) { | ||||
if ( | if ( | ||||
typeof strategy_declaration === "string" || | typeof strategy_declaration === "string" || | ||||
strategy_declaration instanceof AccessStrategyType || | strategy_declaration instanceof AccessStrategyType || | ||||
strategy_declaration instanceof Array | strategy_declaration instanceof Array | ||||
) { | ) { | ||||
collection.access_strategy = { | collection.access_strategy = { | ||||
default: new AccessStrategy(strategy_declaration), | default: new AccessStrategy(strategy_declaration), | ||||
}; | }; | ||||
} else if (typeof strategy_declaration === "object") { | } else if (typeof strategy_declaration === "object") { | ||||
for (const action_name in strategy_declaration) { | for (const action_name in strategy_declaration) { | ||||
const access_strategy = strategy_declaration[action_name]; | const access_strategy = strategy_declaration[action_name]; | ||||
collection.access_strategy[action_name] = new AccessStrategy( | collection.access_strategy[action_name] = new AccessStrategy( | ||||
app, | app, | ||||
access_strategy | access_strategy | ||||
); | ); | ||||
} | } | ||||
} | } | ||||
}, | }, | ||||
get_access_strategy: function(access_strategy_map, action_name) { | get_access_strategy(access_strategy_map, action_name) { | ||||
const ret = | const ret = | ||||
access_strategy_map[action_name] || access_strategy_map["default"]; | access_strategy_map[action_name] || access_strategy_map.default; | ||||
return ret; | return ret; | ||||
}, | }, | ||||
has_large_data_fields: function(fields) { | has_large_data_fields(fields) { | ||||
for (const i in fields) { | for (const i in fields) { | ||||
const field = fields[i]; | const field = fields[i]; | ||||
if (field.type.handles_large_data) { | if (field.type.handles_large_data) { | ||||
return true; | return true; | ||||
} | } | ||||
} | } | ||||
return false; | return false; | ||||
}, | }, | ||||
is_old_value_sensitive: function(fields, action_name) { | is_old_value_sensitive(fields, action_name) { | ||||
for (const i in fields) { | for (const i in fields) { | ||||
if (fields[i].type.is_old_value_sensitive(action_name)) { | if (fields[i].type.is_old_value_sensitive(action_name)) { | ||||
return true; | return true; | ||||
} | } | ||||
} | } | ||||
return false; | return false; | ||||
}, | }, | ||||
decode_values: function(fields, context, values) { | decode_values(fields, context, values) { | ||||
const decoded_values = {}; | const decoded_values = {}; | ||||
for (const key in fields) { | for (const key in fields) { | ||||
const value = values[key]; | const value = values[key]; | ||||
const field = fields[key]; | const field = fields[key]; | ||||
if (field === undefined) { | if (field === undefined) { | ||||
continue; | continue; | ||||
} | } | ||||
decoded_values[key] = field.decode(context, value); | decoded_values[key] = field.decode(context, value); | ||||
} | } | ||||
return Promise.props(decoded_values); | return Promise.props(decoded_values); | ||||
}, | }, | ||||
format_decoded_values: function(fields, context, decoded_values, format) { | format_decoded_values(fields, context, decoded_values, format) { | ||||
const formatted_values = clone(decoded_values); | const formatted_values = clone(decoded_values); | ||||
for (const field_name in formatted_values) { | for (const field_name in formatted_values) { | ||||
const field_format = format[field_name] || undefined; | const field_format = format[field_name] || undefined; | ||||
const decoded_value = decoded_values[field_name]; | const decoded_value = decoded_values[field_name]; | ||||
formatted_values[field_name] = fields[field_name].format( | formatted_values[field_name] = fields[field_name].format( | ||||
context, | context, | ||||
decoded_value, | decoded_value, | ||||
field_format | field_format | ||||
); | ); | ||||
} | } | ||||
return Promise.props(formatted_values); | return Promise.props(formatted_values); | ||||
}, | }, | ||||
_get_body: async function(fields, context, db_document, format) { | async _get_body(fields, context, db_document, format) { | ||||
const decoded_values = await Collection.pure.decode_values( | const decoded_values = await Collection.pure.decode_values( | ||||
fields, | fields, | ||||
context, | context, | ||||
db_document | db_document | ||||
); | ); | ||||
return Collection.pure.format_decoded_values( | return Collection.pure.format_decoded_values( | ||||
fields, | fields, | ||||
context, | context, | ||||
decoded_values, | decoded_values, | ||||
format || {} | format || {} | ||||
); | ); | ||||
}, | }, | ||||
_get_calculated_fields: function( | _get_calculated_fields( | ||||
context, | context, | ||||
calculated_fields, | calculated_fields, | ||||
representation, | representation, | ||||
raw_db_entry, | raw_db_entry, | ||||
calculate | calculate | ||||
) { | ) { | ||||
const ret = {}; | const ret = {}; | ||||
for (const field_name in calculated_fields) { | for (const field_name in calculated_fields) { | ||||
if (calculate === true || calculate[field_name]) { | if (calculate === true || calculate[field_name]) { | ||||
ret[field_name] = calculated_fields[field_name].get_value( | ret[field_name] = calculated_fields[field_name].get_value( | ||||
context, | context, | ||||
representation, | representation, | ||||
raw_db_entry | raw_db_entry | ||||
); | ); | ||||
} | } | ||||
} | } | ||||
return Promise.props(ret); | return Promise.props(ret); | ||||
}, | }, | ||||
get_resource_representation: async function( | async get_resource_representation( | ||||
fields, | fields, | ||||
field_type_name, | field_type_name, | ||||
context, | context, | ||||
db_document, | db_document, | ||||
format, | format, | ||||
calculated_fields, | calculated_fields, | ||||
calculate | calculate = true | ||||
) { | ) { | ||||
if (calculate === undefined) calculate = true; | |||||
const representation = await Collection.pure._get_body( | const representation = await Collection.pure._get_body( | ||||
fields, | fields, | ||||
context, | context, | ||||
db_document, | db_document, | ||||
format | format | ||||
); | ); | ||||
representation.id = db_document.sealious_id; | representation.id = db_document.sealious_id; | ||||
representation._metadata = db_document._metadata; | representation._metadata = db_document._metadata; | ||||
representation._metadata.collection_name = field_type_name; | representation._metadata.collection_name = field_type_name; | ||||
if (calculate) { | if (calculate) { | ||||
representation.calculated_fields = await Collection.pure._get_calculated_fields( | representation.calculated_fields = await Collection.pure._get_calculated_fields( | ||||
context, | context, | ||||
calculated_fields, | calculated_fields, | ||||
representation, | representation, | ||||
db_document, | db_document, | ||||
calculate | calculate | ||||
); | ); | ||||
} | } | ||||
return representation; | return representation; | ||||
}, | }, | ||||
check_if_action_is_allowed: function( | check_if_action_is_allowed( | ||||
access_strategy_map, | access_strategy_map, | ||||
context, | context, | ||||
action_name, | action_name, | ||||
resource_representation | resource_representation | ||||
) { | ) { | ||||
const access_strategy = Collection.pure.get_access_strategy( | const access_strategy = Collection.pure.get_access_strategy( | ||||
access_strategy_map, | access_strategy_map, | ||||
action_name | action_name | ||||
); | ); | ||||
return access_strategy | return access_strategy | ||||
.check(context, resource_representation) | .check(context, resource_representation) | ||||
.then(function(results) { | .then(results => { | ||||
return results; | return results; | ||||
}); | }); | ||||
}, | }, | ||||
get_aggregation_stages: function( | get_aggregation_stages( | ||||
collection, | collection, | ||||
context, | context, | ||||
action_name, | action_name, | ||||
query_params, | query_params, | ||||
named_filters = [] | named_filters = [] | ||||
) { | ) { | ||||
const fields = collection.fields; | const fields = collection.fields; | ||||
const access_strategy_map = collection.access_strategy; | const access_strategy_map = collection.access_strategy; | ||||
▲ Show 20 Lines • Show All 70 Lines • ▼ Show 20 Lines | add_calculated_field(calc_field_name, type_declaration, type_params) { | ||||
return pure.add_calculated_field( | return pure.add_calculated_field( | ||||
this.app, | this.app, | ||||
this, | this, | ||||
calc_field_name, | calc_field_name, | ||||
type_declaration, | type_declaration, | ||||
type_params | type_params | ||||
); | ); | ||||
}, | }, | ||||
add_special_filters: function(named_filters = []) { | add_special_filters(named_filters = []) { | ||||
return pure.add_special_filters(this, named_filters); | return pure.add_special_filters(this, named_filters); | ||||
}, | }, | ||||
get_named_filter: function(filter_name) { | get_named_filter(filter_name) { | ||||
return this.named_filters[filter_name]; | return this.named_filters[filter_name]; | ||||
}, | }, | ||||
add_named_filter: function(filter_name, filter) { | add_named_filter(filter_name, filter) { | ||||
this.named_filters[filter_name] = filter; | this.named_filters[filter_name] = filter; | ||||
}, | }, | ||||
validate_field_values( | validate_field_values( | ||||
context, | context, | ||||
assume_delete_value_on_missing_key, | assume_delete_value_on_missing_key, | ||||
new_values, | new_values, | ||||
old_values | old_values | ||||
) { | ) { | ||||
▲ Show 20 Lines • Show All 78 Lines • Show Last 20 Lines |