Initial commit

This commit is contained in:
Arnaud Nelissen
2021-07-16 10:18:13 +02:00
commit 3af7ddab06
5894 changed files with 590836 additions and 0 deletions

View File

@@ -0,0 +1,172 @@
'use strict';
const cleanPositionalOperators = require('../schema/cleanPositionalOperators');
const handleTimestampOption = require('../schema/handleTimestampOption');
module.exports = applyTimestampsToChildren;
/*!
* ignore
*/
function applyTimestampsToChildren(now, update, schema) {
if (update == null) {
return;
}
const keys = Object.keys(update);
const hasDollarKey = keys.some(key => key.startsWith('$'));
if (hasDollarKey) {
if (update.$push) {
for (const key of Object.keys(update.$push)) {
const $path = schema.path(key);
if (update.$push[key] &&
$path &&
$path.$isMongooseDocumentArray &&
$path.schema.options.timestamps) {
const timestamps = $path.schema.options.timestamps;
const createdAt = handleTimestampOption(timestamps, 'createdAt');
const updatedAt = handleTimestampOption(timestamps, 'updatedAt');
if (update.$push[key].$each) {
update.$push[key].$each.forEach(function(subdoc) {
if (updatedAt != null) {
subdoc[updatedAt] = now;
}
if (createdAt != null) {
subdoc[createdAt] = now;
}
});
} else {
if (updatedAt != null) {
update.$push[key][updatedAt] = now;
}
if (createdAt != null) {
update.$push[key][createdAt] = now;
}
}
}
}
}
if (update.$set != null) {
const keys = Object.keys(update.$set);
for (const key of keys) {
applyTimestampsToUpdateKey(schema, key, update.$set, now);
}
}
}
const updateKeys = Object.keys(update).filter(key => !key.startsWith('$'));
for (const key of updateKeys) {
applyTimestampsToUpdateKey(schema, key, update, now);
}
}
function applyTimestampsToDocumentArray(arr, schematype, now) {
const timestamps = schematype.schema.options.timestamps;
if (!timestamps) {
return;
}
const len = arr.length;
const createdAt = handleTimestampOption(timestamps, 'createdAt');
const updatedAt = handleTimestampOption(timestamps, 'updatedAt');
for (let i = 0; i < len; ++i) {
if (updatedAt != null) {
arr[i][updatedAt] = now;
}
if (createdAt != null) {
arr[i][createdAt] = now;
}
applyTimestampsToChildren(now, arr[i], schematype.schema);
}
}
function applyTimestampsToSingleNested(subdoc, schematype, now) {
const timestamps = schematype.schema.options.timestamps;
if (!timestamps) {
return;
}
const createdAt = handleTimestampOption(timestamps, 'createdAt');
const updatedAt = handleTimestampOption(timestamps, 'updatedAt');
if (updatedAt != null) {
subdoc[updatedAt] = now;
}
if (createdAt != null) {
subdoc[createdAt] = now;
}
applyTimestampsToChildren(now, subdoc, schematype.schema);
}
function applyTimestampsToUpdateKey(schema, key, update, now) {
// Replace positional operator `$` and array filters `$[]` and `$[.*]`
const keyToSearch = cleanPositionalOperators(key);
const path = schema.path(keyToSearch);
if (!path) {
return;
}
const parentSchemaTypes = [];
const pieces = keyToSearch.split('.');
for (let i = pieces.length - 1; i > 0; --i) {
const s = schema.path(pieces.slice(0, i).join('.'));
if (s != null &&
(s.$isMongooseDocumentArray || s.$isSingleNested)) {
parentSchemaTypes.push({ parentPath: key.split('.').slice(0, i).join('.'), parentSchemaType: s });
}
}
if (Array.isArray(update[key]) && path.$isMongooseDocumentArray) {
applyTimestampsToDocumentArray(update[key], path, now);
} else if (update[key] && path.$isSingleNested) {
applyTimestampsToSingleNested(update[key], path, now);
} else if (parentSchemaTypes.length > 0) {
for (const item of parentSchemaTypes) {
const parentPath = item.parentPath;
const parentSchemaType = item.parentSchemaType;
const timestamps = parentSchemaType.schema.options.timestamps;
const updatedAt = handleTimestampOption(timestamps, 'updatedAt');
if (!timestamps || updatedAt == null) {
continue;
}
if (parentSchemaType.$isSingleNested) {
// Single nested is easy
update[parentPath + '.' + updatedAt] = now;
} else if (parentSchemaType.$isMongooseDocumentArray) {
let childPath = key.substr(parentPath.length + 1);
if (/^\d+$/.test(childPath)) {
update[parentPath + '.' + childPath][updatedAt] = now;
continue;
}
const firstDot = childPath.indexOf('.');
childPath = firstDot !== -1 ? childPath.substr(0, firstDot) : childPath;
update[parentPath + '.' + childPath + '.' + updatedAt] = now;
}
}
} else if (path.schema != null && path.schema != schema && update[key]) {
const timestamps = path.schema.options.timestamps;
const createdAt = handleTimestampOption(timestamps, 'createdAt');
const updatedAt = handleTimestampOption(timestamps, 'updatedAt');
if (!timestamps) {
return;
}
if (updatedAt != null) {
update[key][updatedAt] = now;
}
if (createdAt != null) {
update[key][createdAt] = now;
}
}
}

View File

@@ -0,0 +1,119 @@
'use strict';
/*!
* ignore
*/
const get = require('../get');
module.exports = applyTimestampsToUpdate;
/*!
* ignore
*/
function applyTimestampsToUpdate(now, createdAt, updatedAt, currentUpdate, options) {
const updates = currentUpdate;
let _updates = updates;
const overwrite = get(options, 'overwrite', false);
const timestamps = get(options, 'timestamps', true);
// Support skipping timestamps at the query level, see gh-6980
if (!timestamps || updates == null) {
return currentUpdate;
}
const skipCreatedAt = timestamps != null && timestamps.createdAt === false;
const skipUpdatedAt = timestamps != null && timestamps.updatedAt === false;
if (overwrite) {
if (currentUpdate && currentUpdate.$set) {
currentUpdate = currentUpdate.$set;
updates.$set = {};
_updates = updates.$set;
}
if (!skipUpdatedAt && updatedAt && !currentUpdate[updatedAt]) {
_updates[updatedAt] = now;
}
if (!skipCreatedAt && createdAt && !currentUpdate[createdAt]) {
_updates[createdAt] = now;
}
return updates;
}
currentUpdate = currentUpdate || {};
if (Array.isArray(updates)) {
// Update with aggregation pipeline
updates.push({ $set: { updatedAt: now } });
return updates;
}
updates.$set = updates.$set || {};
if (!skipUpdatedAt && updatedAt &&
(!currentUpdate.$currentDate || !currentUpdate.$currentDate[updatedAt])) {
let timestampSet = false;
if (updatedAt.indexOf('.') !== -1) {
const pieces = updatedAt.split('.');
for (let i = 1; i < pieces.length; ++i) {
const remnant = pieces.slice(-i).join('.');
const start = pieces.slice(0, -i).join('.');
if (currentUpdate[start] != null) {
currentUpdate[start][remnant] = now;
timestampSet = true;
break;
} else if (currentUpdate.$set && currentUpdate.$set[start]) {
currentUpdate.$set[start][remnant] = now;
timestampSet = true;
break;
}
}
}
if (!timestampSet) {
updates.$set[updatedAt] = now;
}
if (updates.hasOwnProperty(updatedAt)) {
delete updates[updatedAt];
}
}
if (!skipCreatedAt && createdAt) {
if (currentUpdate[createdAt]) {
delete currentUpdate[createdAt];
}
if (currentUpdate.$set && currentUpdate.$set[createdAt]) {
delete currentUpdate.$set[createdAt];
}
let timestampSet = false;
if (createdAt.indexOf('.') !== -1) {
const pieces = createdAt.split('.');
for (let i = 1; i < pieces.length; ++i) {
const remnant = pieces.slice(-i).join('.');
const start = pieces.slice(0, -i).join('.');
if (currentUpdate[start] != null) {
currentUpdate[start][remnant] = now;
timestampSet = true;
break;
} else if (currentUpdate.$set && currentUpdate.$set[start]) {
currentUpdate.$set[start][remnant] = now;
timestampSet = true;
break;
}
}
}
if (!timestampSet) {
updates.$setOnInsert = updates.$setOnInsert || {};
updates.$setOnInsert[createdAt] = now;
}
}
if (Object.keys(updates.$set).length === 0) {
delete updates.$set;
}
return updates;
}

View File

@@ -0,0 +1,60 @@
'use strict';
const castFilterPath = require('../query/castFilterPath');
const cleanPositionalOperators = require('../schema/cleanPositionalOperators');
const getPath = require('../schema/getPath');
const updatedPathsByArrayFilter = require('./updatedPathsByArrayFilter');
module.exports = function castArrayFilters(query) {
const arrayFilters = query.options.arrayFilters;
if (!Array.isArray(arrayFilters)) {
return;
}
const update = query.getUpdate();
const schema = query.schema;
const strictQuery = schema.options.strictQuery;
const updatedPathsByFilter = updatedPathsByArrayFilter(update);
for (const filter of arrayFilters) {
if (filter == null) {
throw new Error(`Got null array filter in ${arrayFilters}`);
}
for (const key of Object.keys(filter)) {
if (filter[key] == null) {
continue;
}
const dot = key.indexOf('.');
let filterPath = dot === -1 ?
updatedPathsByFilter[key] + '.0' :
updatedPathsByFilter[key.substr(0, dot)] + '.0' + key.substr(dot);
if (filterPath == null) {
throw new Error(`Filter path not found for ${key}`);
}
// If there are multiple array filters in the path being updated, make sure
// to replace them so we can get the schema path.
filterPath = cleanPositionalOperators(filterPath);
const schematype = getPath(schema, filterPath);
if (schematype == null) {
if (!strictQuery) {
return;
}
// For now, treat `strictQuery = true` and `strictQuery = 'throw'` as
// equivalent for casting array filters. `strictQuery = true` doesn't
// quite work in this context because we never want to silently strip out
// array filters, even if the path isn't in the schema.
throw new Error(`Could not find path "${filterPath}" in schema`);
}
if (typeof filter[key] === 'object') {
filter[key] = castFilterPath(query, schematype, filter[key]);
} else {
filter[key] = schematype.castForQuery(filter[key]);
}
}
}
};

View File

@@ -0,0 +1,33 @@
'use strict';
const _modifiedPaths = require('../common').modifiedPaths;
/**
* Given an update document with potential update operators (`$set`, etc.)
* returns an object whose keys are the directly modified paths.
*
* If there are any top-level keys that don't start with `$`, we assume those
* will get wrapped in a `$set`. The Mongoose Query is responsible for wrapping
* top-level keys in `$set`.
*
* @param {Object} update
* @return {Object} modified
*/
module.exports = function modifiedPaths(update) {
const keys = Object.keys(update);
const res = {};
const withoutDollarKeys = {};
for (const key of keys) {
if (key.startsWith('$')) {
_modifiedPaths(update[key], '', res);
continue;
}
withoutDollarKeys[key] = update[key];
}
_modifiedPaths(withoutDollarKeys, '', res);
return res;
};

View File

@@ -0,0 +1,53 @@
'use strict';
const get = require('../get');
/**
* Given an update, move all $set on immutable properties to $setOnInsert.
* This should only be called for upserts, because $setOnInsert bypasses the
* strictness check for immutable properties.
*/
module.exports = function moveImmutableProperties(schema, update, ctx) {
if (update == null) {
return;
}
const keys = Object.keys(update);
for (const key of keys) {
const isDollarKey = key.startsWith('$');
if (key === '$set') {
const updatedPaths = Object.keys(update[key]);
for (const path of updatedPaths) {
_walkUpdatePath(schema, update[key], path, update, ctx);
}
} else if (!isDollarKey) {
_walkUpdatePath(schema, update, key, update, ctx);
}
}
};
function _walkUpdatePath(schema, op, path, update, ctx) {
const schematype = schema.path(path);
if (schematype == null) {
return;
}
let immutable = get(schematype, 'options.immutable', null);
if (immutable == null) {
return;
}
if (typeof immutable === 'function') {
immutable = immutable.call(ctx, ctx);
}
if (!immutable) {
return;
}
update.$setOnInsert = update.$setOnInsert || {};
update.$setOnInsert[path] = op[path];
delete op[path];
}

View File

@@ -0,0 +1,18 @@
'use strict';
/**
* MongoDB throws an error if there's unused array filters. That is, if `options.arrayFilters` defines
* a filter, but none of the `update` keys use it. This should be enough to filter out all unused array
* filters.
*/
module.exports = function removeUnusedArrayFilters(update, arrayFilters) {
const updateKeys = Object.keys(update).map(key => Object.keys(update[key])).reduce((cur, arr) => cur.concat(arr), []);
return arrayFilters.filter(obj => {
const firstKey = Object.keys(obj)[0];
const firstDot = firstKey.indexOf('.');
const arrayFilterKey = firstDot === -1 ? firstKey : firstKey.slice(0, firstDot);
return updateKeys.find(key => key.includes('$[' + arrayFilterKey + ']')) != null;
});
};

View File

@@ -0,0 +1,24 @@
'use strict';
const modifiedPaths = require('./modifiedPaths');
module.exports = function updatedPathsByArrayFilter(update) {
const updatedPaths = modifiedPaths(update);
return Object.keys(updatedPaths).reduce((cur, path) => {
const matches = path.match(/\$\[[^\]]+\]/g);
if (matches == null) {
return cur;
}
for (const match of matches) {
const firstMatch = path.indexOf(match);
if (firstMatch !== path.lastIndexOf(match)) {
throw new Error(`Path '${path}' contains the same array filter multiple times`);
}
cur[match.substring(2, match.length - 1)] = path.
substr(0, firstMatch - 1).
replace(/\$\[[^\]]+\]/g, '0');
}
return cur;
}, {});
};