some progress

This commit is contained in:
Jonas_Jones 2023-03-30 20:40:42 +02:00
parent aea93a5527
commit e3c15bd288
1388 changed files with 306946 additions and 68323 deletions

View file

@ -0,0 +1,193 @@
'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[0] === '$');
if (hasDollarKey) {
if (update.$push) {
_applyTimestampToUpdateOperator(update.$push);
}
if (update.$addToSet) {
_applyTimestampToUpdateOperator(update.$addToSet);
}
if (update.$set != null) {
const keys = Object.keys(update.$set);
for (const key of keys) {
applyTimestampsToUpdateKey(schema, key, update.$set, now);
}
}
if (update.$setOnInsert != null) {
const keys = Object.keys(update.$setOnInsert);
for (const key of keys) {
applyTimestampsToUpdateKey(schema, key, update.$setOnInsert, now);
}
}
}
const updateKeys = Object.keys(update).filter(key => key[0] !== '$');
for (const key of updateKeys) {
applyTimestampsToUpdateKey(schema, key, update, now);
}
function _applyTimestampToUpdateOperator(op) {
for (const key of Object.keys(op)) {
const $path = schema.path(key.replace(/\.\$\./i, '.').replace(/.\$$/, ''));
if (op[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 (op[key].$each) {
op[key].$each.forEach(function(subdoc) {
if (updatedAt != null) {
subdoc[updatedAt] = now;
}
if (createdAt != null) {
subdoc[createdAt] = now;
}
applyTimestampsToChildren(now, subdoc, $path.schema);
});
} else {
if (updatedAt != null) {
op[key][updatedAt] = now;
}
if (createdAt != null) {
op[key][createdAt] = now;
}
applyTimestampsToChildren(now, op[key], $path.schema);
}
}
}
}
}
function applyTimestampsToDocumentArray(arr, schematype, now) {
const timestamps = schematype.schema.options.timestamps;
const len = arr.length;
if (!timestamps) {
for (let i = 0; i < len; ++i) {
applyTimestampsToChildren(now, arr[i], schematype.schema);
}
return;
}
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) {
applyTimestampsToChildren(now, subdoc, schematype.schema);
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.substring(parentPath.length + 1);
if (/^\d+$/.test(childPath)) {
update[parentPath + '.' + childPath][updatedAt] = now;
continue;
}
const firstDot = childPath.indexOf('.');
childPath = firstDot !== -1 ? childPath.substring(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,117 @@
'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,109 @@
'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;
const update = query.getUpdate();
const schema = query.schema;
const updatedPathsByFilter = updatedPathsByArrayFilter(update);
let strictQuery = schema.options.strict;
if (query._mongooseOptions.strict != null) {
strictQuery = query._mongooseOptions.strict;
}
if (query.model && query.model.base.options.strictQuery != null) {
strictQuery = query.model.base.options.strictQuery;
}
if (schema._userProvidedOptions.strictQuery != null) {
strictQuery = schema._userProvidedOptions.strictQuery;
}
if (query._mongooseOptions.strictQuery != null) {
strictQuery = query._mongooseOptions.strictQuery;
}
_castArrayFilters(arrayFilters, schema, strictQuery, updatedPathsByFilter, query);
};
function _castArrayFilters(arrayFilters, schema, strictQuery, updatedPathsByFilter, query) {
if (!Array.isArray(arrayFilters)) {
return;
}
for (const filter of arrayFilters) {
if (filter == null) {
throw new Error(`Got null array filter in ${arrayFilters}`);
}
const keys = Object.keys(filter).filter(key => filter[key] != null);
if (keys.length === 0) {
continue;
}
const firstKey = keys[0];
if (firstKey === '$and' || firstKey === '$or') {
for (const key of keys) {
_castArrayFilters(filter[key], schema, strictQuery, updatedPathsByFilter, query);
}
continue;
}
const dot = firstKey.indexOf('.');
const filterWildcardPath = dot === -1 ? firstKey : firstKey.substring(0, dot);
if (updatedPathsByFilter[filterWildcardPath] == null) {
continue;
}
const baseFilterPath = cleanPositionalOperators(
updatedPathsByFilter[filterWildcardPath]
);
const baseSchematype = getPath(schema, baseFilterPath);
let filterBaseSchema = baseSchematype != null ? baseSchematype.schema : null;
if (filterBaseSchema != null &&
filterBaseSchema.discriminators != null &&
filter[filterWildcardPath + '.' + filterBaseSchema.options.discriminatorKey]) {
filterBaseSchema = filterBaseSchema.discriminators[filter[filterWildcardPath + '.' + filterBaseSchema.options.discriminatorKey]] || filterBaseSchema;
}
for (const key of keys) {
if (updatedPathsByFilter[key] === null) {
continue;
}
if (Object.keys(updatedPathsByFilter).length === 0) {
continue;
}
const dot = key.indexOf('.');
let filterPathRelativeToBase = dot === -1 ? null : key.substring(dot);
let schematype;
if (filterPathRelativeToBase == null || filterBaseSchema == null) {
schematype = baseSchematype;
} else {
// If there are multiple array filters in the path being updated, make sure
// to replace them so we can get the schema path.
filterPathRelativeToBase = cleanPositionalOperators(filterPathRelativeToBase);
schematype = getPath(filterBaseSchema, filterPathRelativeToBase);
}
if (schematype == null) {
if (!strictQuery) {
return;
}
const filterPath = filterPathRelativeToBase == null ?
baseFilterPath + '.0' :
baseFilterPath + '.0' + filterPathRelativeToBase;
// 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(null, 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,32 @@
'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 => {
return _checkSingleFilterKey(obj, updateKeys);
});
};
function _checkSingleFilterKey(arrayFilter, updateKeys) {
const firstKey = Object.keys(arrayFilter)[0];
if (firstKey === '$and' || firstKey === '$or') {
if (!Array.isArray(arrayFilter[firstKey])) {
return false;
}
return arrayFilter[firstKey].find(filter => _checkSingleFilterKey(filter, updateKeys)) != null;
}
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,27 @@
'use strict';
const modifiedPaths = require('./modifiedPaths');
module.exports = function updatedPathsByArrayFilter(update) {
if (update == null) {
return {};
}
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.
substring(0, firstMatch - 1).
replace(/\$\[[^\]]+\]/g, '0');
}
return cur;
}, {});
};