Skip to content

Commit

Permalink
Rollback Relationships
Browse files Browse the repository at this point in the history
This commit:

1. Allows one to rollback belongsTo and hasMany relationships.
2. Added 'shouldRemoveDeletedFromRelationshipsPriorToSave' flag
   to Adapter that allows one to opt back into the old deleted
   record from many array behavior (pre emberjs#3539).
3. Adds bin/build.js to build a standalone version of ember-data.

Known issues:

1. Rolling back a hasMany relationship from the parent side of that
   relationship does not work (doing the same from the child side works
   fine). See test that is commented out below as well as the discussion
   at the end of emberjs#2881#issuecomment-204634262

This was previously emberjs#2881 and is related to emberjs#3698
  • Loading branch information
mmpestorich committed Oct 31, 2022
1 parent 0988185 commit 82978dd
Show file tree
Hide file tree
Showing 21 changed files with 1,358 additions and 83 deletions.
3 changes: 3 additions & 0 deletions ember-data-types/q/record-data-store-wrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { StableRecordIdentifier } from './identifier';
import type { RecordData } from './record-data';
import type { AttributesSchema, RelationshipsSchema } from './record-data-schemas';
import { SchemaDefinitionService } from './schema-definition-service';
import Adapter from "@ember-data/adapter";

/**
@module @ember-data/store
Expand Down Expand Up @@ -250,6 +251,8 @@ export interface V2RecordDataStoreWrapper {
recordDataFor(identifier: StableRecordIdentifier): RecordData;

notifyChange(identifier: StableRecordIdentifier, namespace: NotificationType, key?: string): void;

adapterFor(modelName: string): Adapter;
}

export type RecordDataStoreWrapper = LegacyRecordDataStoreWrapper | V2RecordDataStoreWrapper;
22 changes: 18 additions & 4 deletions ember-data-types/q/record-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,14 @@ export interface ChangedAttributesHash {
[key: string]: [string, string];
}

export type Delta = {
added: unknown[],
removed: unknown[]
}
export interface ChangedRelationshipsHash {
[key: string]: Delta;
}

export interface MergeOperation {
op: 'mergeIdentifiers';
record: StableRecordIdentifier; // existing
Expand Down Expand Up @@ -87,17 +95,23 @@ export interface RecordData {

getAttr(identifier: StableRecordIdentifier, propertyName: string): unknown;
setAttr(identifier: StableRecordIdentifier, propertyName: string, value: unknown): void;
shouldDirtyAttr(identifier: StableRecordIdentifier, propertyName: string, oldValue: unknown, newValue: unknown): boolean;
isAttrDirty(identifier: StableRecordIdentifier, propertyName: string): boolean;
hasDirtyAttrs(identifier: StableRecordIdentifier): boolean;
changedAttrs(identifier: StableRecordIdentifier): ChangedAttributesHash;
hasChangedAttrs(identifier: StableRecordIdentifier): boolean;
rollbackAttrs(identifier: StableRecordIdentifier): string[];

// Relationships
// =============
getRelationship(
identifier: StableRecordIdentifier,
propertyName: string
): SingleResourceRelationship | CollectionResourceRelationship;
getRelationship(identifier: StableRecordIdentifier, propertyName: string): SingleResourceRelationship | CollectionResourceRelationship;
update(operation: LocalRelationshipOperation): void;
shouldDirtyRelationship(identifier: StableRecordIdentifier, propertyName: string, newValue: unknown): boolean;
isRelationshipDirty(identifier: StableRecordIdentifier, propertyName: string): boolean;
hasDirtyRelationships(identifier: StableRecordIdentifier): boolean;
changedRelationships(identifier: StableRecordIdentifier): ChangedRelationshipsHash;
hasChangedRelationships(identifier: StableRecordIdentifier): boolean;
rollbackRelationships(identifier: StableRecordIdentifier): string[];

// State
// =============
Expand Down
47 changes: 46 additions & 1 deletion packages/adapter/addon/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -820,7 +820,7 @@ export default class Adapter extends EmberObject implements MinimumAdapterInterf
@return {Boolean}
@public
*/
shouldBackgroundReloadRecord(store: Store, Snapshot): boolean {
shouldBackgroundReloadRecord(store: Store, snapshot: Snapshot): boolean {
return true;
}

Expand Down Expand Up @@ -860,6 +860,51 @@ export default class Adapter extends EmberObject implements MinimumAdapterInterf
shouldBackgroundReloadAll(store: Store, snapshotRecordArray: SnapshotRecordArray): boolean {
return true;
}

/**
This is used by the store to determine if the store should remove deleted
records from relationships prior to save.
If this is `true` records will remain part of any associated relationships
after being deleted prior to being saved.
If this is `false` records will be removed from any associated relationships
immediately after being deleted.
By default, this is `false`.
@since 4.8.0
*/
shouldRemoveDeletedFromRelationshipsPriorToSave: boolean = false;

// shouldDirtyAttribute(internalModel, context, value) {
// return value !== context.originalValue;
// },
//
// shouldDirtyBelongsTo(internalModel, context, value) {
// return value !== context.originalValue;
// },
//
// shouldDirtyHasMany(internalModel, context, value) {
// let relationshipType = internalModel.type.determineRelationshipType({
// key: context.key,
// kind: context.kind
// }, internalModel.store);
//
// if (relationshipType === 'manyToNone') {
// if (context.added) {
// return !context.originalValue.has(context.added);
// }
// return context.originalValue.has(context.removed);
// } else if (relationshipType === 'manyToMany') {
// const { canonicalMembers, members } = internalModel._relationships.get(context.key);
// if (canonicalMembers.size !== members.size) {
// return true;
// }
// return !canonicalMembers.list.every(x => members.list.includes(x));
// }
// return false;
// }
}

export { BuildURLMixin } from './-private';
Original file line number Diff line number Diff line change
Expand Up @@ -686,7 +686,7 @@ function handleCompletedRelationshipRequest(
}

if (isHasMany) {
(value as RelatedCollection).isLoaded = true;
(value as RelatedCollection).isLoaded = (relationship as ManyRelationship).isLoaded = true;
}

relationship.state.hasFailedLoadAttempt = false;
Expand Down
25 changes: 25 additions & 0 deletions packages/model/addon/-private/model.js
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,10 @@ class Model extends EmberObject {
get hasDirtyAttributes() {
return this.currentState.isDirty;
}
@dependentKeyCompat
get isDirty() {
return this.currentState.isDirty0;
}

/**
If this property is `true` the record is in the `saving` state. A
Expand Down Expand Up @@ -859,6 +863,27 @@ class Model extends EmberObject {
});
}

changedRelationships() {
return recordDataFor(this).changedRelationships(recordIdentifierFor(this));
}

rollbackRelationships() {
return recordDataFor(this).rollbackRelationships(recordIdentifierFor(this));
}

changes() {
const changes = Object.create(null);
for (const [key, [oldValue, newValue]] of Object.entries(this.changedAttributes())) {
changes[key] = { added: newValue === null ? [] : [newValue], removed: oldValue === null ? [] : [oldValue] }
}
return Object.assign(changes, this.changedRelationships());
}

rollback() {
this.rollbackRelationships();
this.rollbackAttributes();
}

/**
@method _createSnapshot
@private
Expand Down
14 changes: 14 additions & 0 deletions packages/model/addon/-private/record-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,7 @@ export default class RecordState {
this.fulfilledCount++;
this.notify('isLoading');
this.notify('isDirty');
this.notify('isDirty0');
notifyErrorsStateChanged(this);
this._errorRequests = [];
this._lastError = null;
Expand Down Expand Up @@ -241,11 +242,15 @@ export default class RecordState {
this.notify('isNew');
this.notify('isDeleted');
this.notify('isDirty');
this.notify('isDirty0');
break;
case 'attributes':
this.notify('isEmpty');
this.notify('isDirty');
break;
case 'relationships':
this.notify('isDirty0');
break;
case 'errors':
this.updateInvalidErrors(this.record.errors);
this.notify('isValid');
Expand Down Expand Up @@ -366,6 +371,15 @@ export default class RecordState {
return this.isNew || rd.hasChangedAttrs(this.identifier);
}

@tagged
get isDirty0() {
let rd = this.recordData;
if (rd.isDeletionCommitted(this.identifier) || (this.isDeleted && this.isNew)) {
return false;
}
return this.isNew || this.isDeleted || rd.hasDirtyAttrs(this.identifier) || rd.hasDirtyRelationships(this.identifier);
}

@tagged
get isError() {
let errorReq = this._errorRequests[this._errorRequests.length - 1];
Expand Down
38 changes: 18 additions & 20 deletions packages/record-data/addon/-private/graph/graph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@ import { MergeOperation } from '@ember-data/types/q/record-data';
import type { RecordDataStoreWrapper } from '@ember-data/types/q/record-data-store-wrapper';
import type { Dict } from '@ember-data/types/q/utils';

import { ImplicitRelationship } from '../relationships/state/relationships';
import BelongsToRelationship from '../relationships/state/belongs-to';
import ManyRelationship from '../relationships/state/has-many';
import type { EdgeCache, UpgradedMeta } from './-edge-definition';
import type { EdgeCache } from './-edge-definition';
import { isLHS, upgradeDefinition } from './-edge-definition';
import type {
DeleteRecordOperation,
Expand All @@ -35,13 +36,7 @@ import replaceRelatedRecord from './operations/replace-related-record';
import replaceRelatedRecords, { syncRemoteToLocal } from './operations/replace-related-records';
import updateRelationshipOperation from './operations/update-relationship';

export interface ImplicitRelationship {
definition: UpgradedMeta;
identifier: StableRecordIdentifier;
localMembers: Set<StableRecordIdentifier>;
remoteMembers: Set<StableRecordIdentifier>;
}

export { ImplicitRelationship };
export type RelationshipEdge = ImplicitRelationship | ManyRelationship | BelongsToRelationship;

export const Graphs = new Map<RecordDataStoreWrapper, Graph>();
Expand Down Expand Up @@ -96,6 +91,15 @@ export class Graph {
this._removing = null;
}

all(identifier: StableRecordIdentifier): Dict<RelationshipEdge> {
let relationships = this.identifiers.get(identifier);
if (relationships === undefined) {
relationships = Object.create(null) as Dict<RelationshipEdge>;
this.identifiers.set(identifier, relationships);
}
return relationships;
}

has(identifier: StableRecordIdentifier, propertyName: string): boolean {
let relationships = this.identifiers.get(identifier);
if (!relationships) {
Expand All @@ -106,12 +110,7 @@ export class Graph {

get(identifier: StableRecordIdentifier, propertyName: string): RelationshipEdge {
assert(`expected propertyName`, propertyName);
let relationships = this.identifiers.get(identifier);
if (!relationships) {
relationships = Object.create(null) as Dict<RelationshipEdge>;
this.identifiers.set(identifier, relationships);
}

const relationships = this.all(identifier);
let relationship = relationships[propertyName];
if (!relationship) {
const info = upgradeDefinition(this, identifier, propertyName);
Expand All @@ -122,12 +121,7 @@ export class Graph {
const Klass = meta.kind === 'hasMany' ? ManyRelationship : BelongsToRelationship;
relationship = relationships[propertyName] = new Klass(meta, identifier);
} else {
relationship = relationships[propertyName] = {
definition: meta,
identifier,
localMembers: new Set(),
remoteMembers: new Set(),
};
relationship = relationships[propertyName] = new ImplicitRelationship(meta, identifier);
}
}

Expand Down Expand Up @@ -459,6 +453,10 @@ function destroyRelationship(graph: Graph, rel: RelationshipEdge, silenceNotific
notifyChange(graph, rel.identifier, rel.definition.key);
}
}

if (isHasMany(rel)) {
rel.isLoaded = false;
}
}

function notifyInverseOfDematerialization(
Expand Down
Loading

0 comments on commit 82978dd

Please sign in to comment.