-
Notifications
You must be signed in to change notification settings - Fork 236
feat(hadron-document)!: handle nested fields and dots & dollars well COMPASS-5805 #3239
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,7 +5,7 @@ import EventEmitter from 'eventemitter3'; | |
import { EJSON, UUID } from 'bson'; | ||
import type { ObjectGeneratorOptions } from './object-generator'; | ||
import ObjectGenerator from './object-generator'; | ||
import type { BSONObject, BSONValue } from './utils'; | ||
import type { BSONArray, BSONObject, BSONValue } from './utils'; | ||
import { objectToIdiomaticEJSON } from './utils'; | ||
import type { HadronEJSONOptions } from './utils'; | ||
|
||
|
@@ -109,40 +109,40 @@ export class Document extends EventEmitter { | |
* where the update only succeeds when the changed document's elements have | ||
* not been changed in the background. | ||
* | ||
* `query` and `updateDoc` may use $getField and $setField if field names | ||
* contain either `.` or start with `$`. These operators are only available | ||
* on MongoDB 5.0+. (Note that field names starting with `$` are also only | ||
* allowed in MongoDB 5.0+.) | ||
* | ||
* @param {Object} alwaysIncludeKeys - An object whose keys are used as keys | ||
* that are always included in the generated query. | ||
* that are always included in the generated query. Dots inside key names | ||
* are interpreted as referring to nested properties. | ||
* | ||
* @returns {Object} An object containing the `query` and `updateDoc` to be | ||
* used in an update operation. | ||
*/ | ||
generateUpdateUnlessChangedInBackgroundQuery( | ||
alwaysIncludeKeys: BSONObject | null = null | ||
alwaysIncludeKeys: string[] = [] | ||
): { | ||
query: BSONObject; | ||
updateDoc: { $set?: BSONObject; $unset?: BSONObject }; | ||
updateDoc: { $set?: BSONObject; $unset?: BSONObject } | BSONArray; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Totally can see that this PR is marked as a breaking change with a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oof, thanks for bringing this up! Pinged them on Slack (as you might have seen) |
||
} { | ||
// Build a query that will find the document to update only if it has the | ||
// values of elements that were changed with their original value. | ||
// This query won't find the document if an updated element's value isn't | ||
// the same value as it was when it was originally loaded. | ||
const originalFieldsThatWillBeUpdated = | ||
this.getOriginalKeysAndValuesForFieldsThatWereUpdated(alwaysIncludeKeys); | ||
ObjectGenerator.getQueryForOriginalKeysAndValuesForSpecifiedFields( | ||
this, | ||
alwaysIncludeKeys, | ||
true | ||
); | ||
const query = { | ||
_id: this.getId(), | ||
...originalFieldsThatWillBeUpdated, | ||
}; | ||
|
||
// Build the update document to be used in an update operation with `$set` | ||
// and `$unset` reflecting the changes that have occured in the document. | ||
const setUpdateObject = this.getSetUpdateForDocumentChanges(); | ||
const unsetUpdateObject = this.getUnsetUpdateForDocumentChanges(); | ||
const updateDoc: { $set?: BSONObject; $unset?: BSONObject } = {}; | ||
if (setUpdateObject && Object.keys(setUpdateObject).length > 0) { | ||
updateDoc.$set = setUpdateObject; | ||
} | ||
if (unsetUpdateObject && Object.keys(unsetUpdateObject).length > 0) { | ||
updateDoc.$unset = unsetUpdateObject; | ||
} | ||
const updateDoc = ObjectGenerator.generateUpdateDoc(this); | ||
|
||
return { | ||
query, | ||
|
@@ -197,98 +197,24 @@ export class Document extends EventEmitter { | |
return element ? element.generateObject() : null; | ||
} | ||
|
||
/** | ||
* Generate the query javascript object reflecting the elements that | ||
* were updated in this document. The values of this object are the original | ||
* values, this can be used when querying for an update to see if the original | ||
* document was changed in the background while it was being updated elsewhere. | ||
* | ||
* @param {Object} alwaysIncludeKeys - An object whose keys are used as keys | ||
* that are always included in the generated query. | ||
* | ||
* @returns {Object} The javascript object. | ||
*/ | ||
getOriginalKeysAndValuesForFieldsThatWereUpdated( | ||
alwaysIncludeKeys: BSONObject | null = null | ||
): BSONObject { | ||
const object: BSONObject = {}; | ||
|
||
if (this.elements) { | ||
for (const element of this.elements) { | ||
if ( | ||
(element.isModified() && !element.isAdded()) || | ||
(alwaysIncludeKeys && element.key in alwaysIncludeKeys) | ||
) { | ||
// Using `.key` instead of `.currentKey` to ensure we look at | ||
// the original field's value. | ||
object[element.key] = element.generateOriginalObject(); | ||
} | ||
if (element.isAdded() && element.currentKey !== '') { | ||
// When a new field is added, check if that field | ||
// was already added in the background. | ||
object[element.currentKey] = { $exists: false }; | ||
} | ||
} | ||
} | ||
|
||
return object; | ||
} | ||
|
||
/** | ||
* Generate the query javascript object reflecting the elements that | ||
* are specified by the keys listed in `keys`. The values of this object are | ||
* the original values, this can be used when querying for an update based | ||
* on multiple criteria. | ||
* | ||
* @param {Object} keys - An object whose keys are used as keys | ||
* that are included in the generated query. | ||
* @param keys - An array whose entries are used as keys | ||
* that are included in the generated query. Dots inside key names | ||
* are interpreted as referring to nested properties. | ||
* | ||
* @returns {Object} The javascript object. | ||
*/ | ||
getOriginalKeysAndValuesForSpecifiedKeys(keys: BSONObject): BSONObject { | ||
const object: BSONObject = {}; | ||
|
||
if (this.elements) { | ||
for (const element of this.elements) { | ||
if (element.key in keys) { | ||
// Using `.key` instead of `.currentKey` to ensure we look at | ||
// the original field's value. | ||
object[element.key] = element.generateOriginalObject(); | ||
} | ||
} | ||
} | ||
|
||
return object; | ||
} | ||
|
||
/** | ||
* Generate an $set javascript object, that can be used in update operations to | ||
* set the changes which have occured in the document since it was loaded. | ||
* | ||
* @returns {Object} The javascript update object. | ||
**/ | ||
getSetUpdateForDocumentChanges(): BSONObject { | ||
const object: BSONObject = {}; | ||
|
||
if (this.elements) { | ||
for (const element of this.elements) { | ||
if ( | ||
!element.isRemoved() && | ||
element.currentKey !== '' && | ||
element.isModified() | ||
) { | ||
// Include the full modified element. | ||
// We don't individually set nested fields because we can't guarantee a | ||
// path to the element using '.' dot notation will update | ||
// the correct field, because field names can contain dots as of 3.6. | ||
// When a nested field has been altered (changed/added/removed) it is | ||
// set at the top level field. This means we overwrite possible | ||
// background changes that occur within sub documents. | ||
object[element.currentKey] = element.generateObject(); | ||
} | ||
} | ||
} | ||
return object; | ||
getQueryForOriginalKeysAndValuesForSpecifiedKeys(keys: string[]): BSONObject { | ||
return ObjectGenerator.getQueryForOriginalKeysAndValuesForSpecifiedFields( | ||
this, | ||
keys, | ||
false | ||
); | ||
} | ||
|
||
/** | ||
|
@@ -309,29 +235,6 @@ export class Document extends EventEmitter { | |
return String(element.value); | ||
} | ||
|
||
/** | ||
* Generate an $unset javascript object, that can be used in update | ||
* operations, with the removals from the document. | ||
* | ||
* @returns {Object} The javascript update object. | ||
**/ | ||
getUnsetUpdateForDocumentChanges(): BSONObject { | ||
const object: BSONObject = {}; | ||
|
||
if (this.elements) { | ||
for (const element of this.elements) { | ||
if (!element.isAdded() && element.isRemoved() && element.key !== '') { | ||
object[element.key] = true; | ||
} | ||
if (!element.isAdded() && element.isRenamed() && element.key !== '') { | ||
// Remove the original field when an element is renamed. | ||
object[element.key] = true; | ||
} | ||
} | ||
} | ||
return object; | ||
} | ||
|
||
/** | ||
* Insert a placeholder element at the end of the document. | ||
* | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
❤️