Skip to content
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

fix(hadron-document): allow expanding an element that has its type changed to array or object COMPASS-7929 #6013

Merged
merged 4 commits into from
Jul 16, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 24 additions & 16 deletions packages/hadron-document/src/element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,12 @@ const UNEDITABLE_TYPES = [
'DBRef',
];

export function isValueExpandable(
value: BSONValue
): value is BSONObject | BSONArray {
return isPlainObject(value) || isArray(value);
}

/**
* Represents an element in a document.
*/
Expand All @@ -58,6 +64,11 @@ export class Element extends EventEmitter {
added: boolean;
removed: boolean;
elements?: ElementList;
// With Arrays and Objects we store the value in the
// `elements` instead of currentValue. We use `originalExpandableValue`
// to store the original value that was passed in. This is necessary
// to be able to revert to the original value when the user cancels
// any changes.
originalExpandableValue?: BSONObject | BSONArray;
parent: Element | Document | null;
type: TypeCastTypes;
Expand Down Expand Up @@ -116,7 +127,7 @@ export class Element extends EventEmitter {
value = TypeChecker.cast(value, TypeChecker.type(value));
}

if (this._isExpandable(value)) {
if (isValueExpandable(value)) {
// NB: Important to set `originalExpandableValue` first as element
// generation will depend on it
this.originalExpandableValue = value;
Expand Down Expand Up @@ -168,10 +179,10 @@ export class Element extends EventEmitter {
*/
edit(value: BSONValue): void {
this.currentType = TypeChecker.type(value);
if (this._isExpandable(value) && !this._isExpandable(this.currentValue)) {
if (isValueExpandable(value) && !isValueExpandable(this.currentValue)) {
this.currentValue = null;
this.elements = this._generateElements(value);
} else if (!this._isExpandable(value) && this.elements) {
} else if (!isValueExpandable(value) && this.elements) {
this.currentValue = value;
this.elements = undefined;
} else {
Expand Down Expand Up @@ -455,6 +466,14 @@ export class Element extends EventEmitter {
}
}

_isExpandable(): boolean {
return (
isValueExpandable(this.originalExpandableValue) ||
(this.isModified() &&
(this.currentType === 'Array' || this.currentType === 'Object'))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know that this is just preserving existing code, but the more I look at it, the less sense it makes, sorry 😅 If you edited the element to a non-expandable type, changed Object to String say, this would still return true, which just doesn't sound right? Feels like the check you are adding instead, the one that looks at the currentType, is actually the best way to check for this instead of checking the value and is the only thing we need to check for, does this makes sense or is there a case I'm missing where looking at the original value is required?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good callout! Removed everything except the current type check.

);
}

/**
* Is the element the last in the elements.
*
Expand Down Expand Up @@ -726,7 +745,7 @@ export class Element extends EventEmitter {
* will most expand the element itself.
*/
expand(expandChildren = false): void {
if (!this._isExpandable(this.originalExpandableValue)) {
if (!this._isExpandable()) {
return;
}

Expand All @@ -743,7 +762,7 @@ export class Element extends EventEmitter {
* Collapses only the target element
*/
collapse(): void {
if (!this._isExpandable(this.originalExpandableValue)) {
if (!this._isExpandable()) {
return;
}

Expand Down Expand Up @@ -801,17 +820,6 @@ export class Element extends EventEmitter {
return !!element && element.isAdded() && element.isBlank();
}

/**
* Check if the value is expandable.
*
* @param value - The value to check.
*
* @returns If the value is expandable.
*/
_isExpandable(value: BSONValue): value is BSONObject | BSONArray {
return isPlainObject(value) || isArray(value);
}

/**
* Generates a sequence of child elements.
*
Expand Down
31 changes: 29 additions & 2 deletions packages/hadron-document/test/element.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
} from 'bson';
import { expect } from 'chai';
import { Document, Element, ElementEvents } from '../src/';
import { DATE_FORMAT } from '../src/element';
import { DATE_FORMAT, isValueExpandable } from '../src/element';
import moment from 'moment';
import Sinon from 'sinon';

Expand Down Expand Up @@ -2773,10 +2773,37 @@ describe('Element', function () {
expect(element.expanded).to.be.true;

for (const el of element.elements ?? []) {
if (el._isExpandable(el.originalExpandableValue)) {
if (isValueExpandable(el.originalExpandableValue)) {
expect(el.expanded).to.be.true;
}
}
});
});

context(
'when expanding an element that has been added with a changed type',
function () {
it('should expand the target element and its children', function () {
const element = new Element('names', {
firstName: 'A',
addresses: [1, 2],
});
expect(element.expanded).to.be.false;

element.insertEnd('pineapple', 'to be changed');
element.get('pineapple')?.changeType('Object');

element.insertEnd('pie', new Int32(123));
element.get('pie')?.changeType('Array');

expect(element.get('pineapple')?.expanded).to.be.false;
expect(element.get('pie')?.expanded).to.be.false;

element.expand(true);
expect(element.expanded).to.be.true;
expect(element.get('pineapple')?.expanded).to.be.true;
expect(element.get('pie')?.expanded).to.be.true;
});
}
);
});
Loading