diff --git a/backend/db.js b/backend/db.js index 26663be..0f394a2 100644 --- a/backend/db.js +++ b/backend/db.js @@ -698,9 +698,15 @@ exports.getRecords = async (table, filter_obj = null, return_count = false, sort // Update -exports.updateRecord = async (table, filter_obj, update_obj) => { +exports.updateRecord = async (table, filter_obj, update_obj, nested_mode = false) => { // local db override if (using_local_db) { + if (nested_mode) { + // if object is nested we need to handle it differently + update_obj = utils.convertFlatObjectToNestedObject(update_obj); + exports.applyFilterLocalDB(local_db.get(table), filter_obj, 'find').merge(update_obj).write(); + return true; + } exports.applyFilterLocalDB(local_db.get(table), filter_obj, 'find').assign(update_obj).write(); return true; } @@ -722,6 +728,18 @@ exports.updateRecords = async (table, filter_obj, update_obj) => { return !!(output['result']['ok']); } +exports.removePropertyFromRecord = async (table, filter_obj, remove_obj) => { + // local db override + if (using_local_db) { + const props_to_remove = Object.keys(remove_obj); + exports.applyFilterLocalDB(local_db.get(table), filter_obj, 'find').unset(props_to_remove).write(); + return true; + } + + const output = await database.collection(table).updateOne(filter_obj, {$unset: remove_obj}); + return !!(output['result']['ok']); +} + exports.bulkUpdateRecordsByKey = async (table, key_label, update_obj) => { // local db override if (using_local_db) { diff --git a/backend/tasks.js b/backend/tasks.js index 1c7315c..0cc465d 100644 --- a/backend/tasks.js +++ b/backend/tasks.js @@ -101,7 +101,7 @@ exports.setupTasks = async () => { const tasks_keys = Object.keys(TASKS); for (let i = 0; i < tasks_keys.length; i++) { const task_key = tasks_keys[i]; - const mergedDefaultOptions = Object.assign(defaultOptions['all'], defaultOptions[task_key] || {}); + const mergedDefaultOptions = Object.assign({}, defaultOptions['all'], defaultOptions[task_key] || {}); const task_in_db = await db_api.getRecord('tasks', {key: task_key}); if (!task_in_db) { // insert task metadata into table if missing, eventually move title to UI @@ -115,14 +115,16 @@ exports.setupTasks = async () => { data: null, error: null, schedule: null, - options: Object.assign(defaultOptions['all'], defaultOptions[task_key] || {}) + options: Object.assign({}, defaultOptions['all'], defaultOptions[task_key] || {}) }); } else { // verify all options exist in task for (const key of Object.keys(mergedDefaultOptions)) { + const option_key = `options.${key}`; + // Remove any potential mangled option keys (#861) + await db_api.removePropertyFromRecord('tasks', {key: task_key}, {[option_key]: true}); if (!(task_in_db.options && task_in_db.options.hasOwnProperty(key))) { - const option_key = `options.${key}` - await db_api.updateRecord('tasks', {key: task_key}, {[option_key]: mergedDefaultOptions[key]}); + await db_api.updateRecord('tasks', {key: task_key}, {[option_key]: mergedDefaultOptions[key]}, true); } } diff --git a/backend/test/tests.js b/backend/test/tests.js index f0050b1..7bee912 100644 --- a/backend/test/tests.js +++ b/backend/test/tests.js @@ -175,6 +175,15 @@ describe('Database', async function() { await db_api.removeRecord('test', {test_update: 'test'}); }); + it('Remove property from record', async function() { + await db_api.insertRecordIntoTable('test', {test_keep: 'test', test_remove: 'test'}); + await db_api.removePropertyFromRecord('test', {test_keep: 'test'}, {test_remove: true}); + const updated_record = await db_api.getRecord('test', {test_keep: 'test'}); + assert(updated_record['test_keep']); + assert(!updated_record['test_remove']); + await db_api.removeRecord('test', {test_keep: 'test'}); + }); + it('Remove record', async function() { await db_api.insertRecordIntoTable('test', {test_remove: 'test'}); const delete_succeeded = await db_api.removeRecord('test', {test_remove: 'test'}); @@ -699,4 +708,24 @@ describe('Utils', async function() { const stripped_obj = utils.stripPropertiesFromObject(test_obj, ['test1', 'test3']); assert(!stripped_obj['test1'] && stripped_obj['test2'] && !stripped_obj['test3']) }); + + it('Convert flat object to nested object', async function() { + // No modfication + const flat_obj0 = {'test1': {'test_sub': true}, 'test2': {test_sub: true}}; + const nested_obj0 = utils.convertFlatObjectToNestedObject(flat_obj0); + assert(nested_obj0['test1'] && nested_obj0['test1']['test_sub']); + assert(nested_obj0['test2'] && nested_obj0['test2']['test_sub']); + + // Standard setup + const flat_obj1 = {'test1.test_sub': true, 'test2.test_sub': true}; + const nested_obj1 = utils.convertFlatObjectToNestedObject(flat_obj1); + assert(nested_obj1['test1'] && nested_obj1['test1']['test_sub']); + assert(nested_obj1['test2'] && nested_obj1['test2']['test_sub']); + + // Nested branches + const flat_obj2 = {'test1.test_sub': true, 'test1.test2.test_sub': true}; + const nested_obj2 = utils.convertFlatObjectToNestedObject(flat_obj2); + assert(nested_obj2['test1'] && nested_obj2['test1']['test_sub']); + assert(nested_obj2['test1'] && nested_obj2['test1']['test2'] && nested_obj2['test1']['test2']['test_sub']); + }); }); \ No newline at end of file diff --git a/backend/utils.js b/backend/utils.js index 984ae4c..f4df0f8 100644 --- a/backend/utils.js +++ b/backend/utils.js @@ -501,6 +501,23 @@ exports.updateLoggerLevel = (new_logger_level) => { logger.transports[2].level = new_logger_level; } +exports.convertFlatObjectToNestedObject = (obj) => { + const result = {}; + for (const key in obj) { + const nestedKeys = key.split('.'); + let currentObj = result; + for (let i = 0; i < nestedKeys.length; i++) { + if (i === nestedKeys.length - 1) { + currentObj[nestedKeys[i]] = obj[key]; + } else { + currentObj[nestedKeys[i]] = currentObj[nestedKeys[i]] || {}; + currentObj = currentObj[nestedKeys[i]]; + } + } + } + return result; +} + // objects function File(id, title, thumbnailURL, isAudio, duration, url, uploader, size, path, upload_date, description, view_count, height, abr) {