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 authored and jghansell committed Jun 7, 2024
1 parent cd70bee commit 721e8a1
Show file tree
Hide file tree
Showing 35 changed files with 2,408 additions and 96 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,4 @@ benchmarks/results/*.json
.vscode/
.idea/
*.iml
packed
113 changes: 113 additions & 0 deletions BUILD.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# Package Ember Data for Local Use

## Overview

Ember Data uses `pnpm` but our projects use `npm`. In order to develop against
Ember Data locally (i.e. so we get proper code completion, navigation, etc...) we
need to do a few things first:

1) [Environment](#environment)
2) [Build Ember Data Packages](#build-ember-data-packages)
3) [Link Ember Data into a Project](#link-ember-data-into-a-project)

## Environment

Make sure pnpm is available and configure our environment:

```sh
PROJECTS="$HOME/Source/IdeaProjects"
PROJECT="$PROJECTS/MP Foods Inc"
EMBER_DATA="$PROJECTS/Ember/ember-data"
EMBER_DATA_VERSION="4.8.8"
PACKAGES="-ember-data:adapter:canary-features:debug:model:private-build-infra:record-data:serializer:store:tracking"

# Setup array of packages
OLD_IFS="$IFS"
IFS=:
set -- $PACKAGES
IFS="$OLD_IFS"
unset OLD_IFS

# Ensure a sane development environment
cd "$EMBER_DATA" || return 1
corepack enable
corepack prepare "$(jq -r '.packageManager | @text' package.json)" --activate
pnpm install
```

## Build Ember Data Packages

Generate the local packages that comprise Ember Data:

```sh
cd "$EMBER_DATA" || return 1
rm -rf "$EMBER_DATA/packed"
mkdir "$EMBER_DATA/packed"
printf "Copy the following into your project's package.json:\n\n"
for pkg; do (
set -e
cd "$EMBER_DATA/packages/$pkg"
if [ "$pkg" = -ember-data ]; then
pkgName="${pkg#-}-$EMBER_DATA_VERSION"
else
pkgName="ember-data-$pkg-$EMBER_DATA_VERSION"
fi
pnpm pack --pack-destination="$EMBER_DATA/packed" >/dev/null 2>&1
cd "$EMBER_DATA/packed"
tar -s "/^package/$pkgName/" -xf "$pkgName.tgz"
rm "$pkgName.tgz"
sed -E -i '' "/\"dependencies\": {/,/}/s|\"@ember-data/([-a-z]*)\": .*|\"@ember-data/\1\": \"file:$EMBER_DATA/packed/ember-data-\1-$EMBER_DATA_VERSION\",|" "$pkgName/package.json"
if [ "$pkg" = -ember-data ]; then
printf '"ember-data": "file:%s/packed/%s",\n' "$EMBER_DATA" "$pkgName"
else
printf '"@ember-data/%s": "file:%s/packed/%s",\n' "$pkg" "$EMBER_DATA" "$pkgName"
fi
) || return 1; done
unset pkg pkgName
```

## Link Ember Data into a Project

```sh
cd "$PROJECT"

# Optionally, ensure npm installs links for local dependencies instead of copying (i.e. 'installing') them. The name of
# this is confusing. `npm config set install-links true` or `npm install --install-links` means npm will actually install
# (i.e. copy) local dependencies into node_modules instead of just linking to them. If `install-links` is `true` then
# you must re-run `npm install` *everytime* you (re)generate the packages in the previous step above.
npm config set install-links false

# *HOWEVER*, npm install from sb-pkg just doesn't handle symlinks correctly and I have not found a work around (`npm
# --preserve-symlinks` and the like doesn't seem to help, so for now, stick to `install-links true`.
npm config set install-links true

# Use `npm` to install dependencies to the generated packages, *or* copy the output from the section above and paste
# into the relevant dependencies section of the project's package.json (i.e. `dependencies` or `devDependencies`).
for pkg; do
if [ "$pkg" = '-ember-data' ]; then
npm install --save-dev "$EMBER_DATA/packed/${pkg#-}-$EMBER_DATA_VERSION"
else
npm install --save-dev "$EMBER_DATA/packed/ember-data-$pkg-$EMBER_DATA_VERSION"
fi
done
# Finally, for posterity, (re)run:
npm install
```

Reference:

- https://docs.npmjs.com/cli/v9/commands/npm-link

#### Build the Ember Data Addon using Swarmbox Web Package

```sh
# Build ember-data addon...
cd "$PROJECT"/swarmbox-web-framework
# This is important after making any changes above to make sure we install the new version of ember-data built above
# (node doesn't re-install these when using install-links=true)...
dash -e ./setup.sh
cd "$PROJECT"/swarmbox-web-framework/modules/swarmbox-web-data
node "$PROJECTS/SwarmBox WebPackage/swarmbox-web-package/lib/cli.js" -e production addons
# ...generated addons will be in public/vendor/ember-addons-new
cp -av public/vendor/ember-addons-new/ember-data.* public/vendor/ember-addons/
```
12 changes: 12 additions & 0 deletions ember-data-types/cache/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
* @module @ember-data/experimental-preview-types
*/
import { StoreRequestContext } from '@ember-data/store/-private/cache-handler';
import { ChangedRelationshipsHash } from '@ember-data/types/q/cache';
import { StableRecordIdentifier } from '@ember-data/types/q/identifier';

import { CollectionResourceRelationship, SingleResourceRelationship } from '../q/ember-data-json-api';
Expand Down Expand Up @@ -327,6 +328,10 @@ export interface Cache {
*/
setAttr(identifier: StableRecordIdentifier, field: string, value: unknown): void;

shouldDirtyAttr(identifier: StableRecordIdentifier, propertyName: string, oldValue: unknown, newValue: unknown): boolean;

Check failure on line 331 in ember-data-types/cache/cache.ts

View workflow job for this annotation

GitHub Actions / lint

Replace `identifier:·StableRecordIdentifier,·propertyName:·string,·oldValue:·unknown,·newValue:·unknown` with `⏎····identifier:·StableRecordIdentifier,⏎····propertyName:·string,⏎····oldValue:·unknown,⏎····newValue:·unknown⏎··`
isAttrDirty(identifier: StableRecordIdentifier, propertyName: string): boolean;
hasDirtyAttrs(identifier: StableRecordIdentifier): boolean;

/**
* Query the cache for the changed attributes of a resource.
*
Expand Down Expand Up @@ -375,6 +380,13 @@ export interface Cache {
isCollection?: boolean
): SingleResourceRelationship | CollectionResourceRelationship;

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[];

// Resource State
// ===============

Expand Down
3 changes: 3 additions & 0 deletions ember-data-types/q/cache-store-wrapper.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import Adapter from '@ember-data/adapter';
import { IdentifierCache } from '@ember-data/store/-private/caches/identifier-cache';
import { NotificationType } from '@ember-data/store/-private/managers/notification-manager';

Expand Down Expand Up @@ -272,6 +273,8 @@ export interface V2CacheStoreWrapper {
namespace: NotificationType | 'added' | 'removed' | 'updated',
key?: string
): void;

adapterFor(modelName: string): Adapter;
}

export type CacheStoreWrapper = LegacyCacheStoreWrapper | V2CacheStoreWrapper;
19 changes: 19 additions & 0 deletions ember-data-types/q/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,15 @@ import { Dict } from './utils';

export type ChangedAttributesHash = Record<string, [unknown, unknown]>;

export type Delta = {
added: StableRecordIdentifier[];
removed: StableRecordIdentifier[];
};

export interface ChangedRelationshipsHash {
[key: string]: Delta;
}

export interface MergeOperation {
op: 'mergeIdentifiers';
record: StableRecordIdentifier; // existing
Expand Down Expand Up @@ -38,6 +47,9 @@ export interface CacheV1 {
// =====
getAttr(key: string): unknown;
setDirtyAttribute(key: string, value: unknown): void;
shouldDirtyAttr(key: string, propertyName: string, oldValue: unknown, newValue: unknown): boolean;
isAttrDirty(key: string, propertyName: string): boolean;
hasDirtyAttrs(key: string): boolean;
changedAttributes(): ChangedAttributesHash;
hasChangedAttributes(): boolean;
rollbackAttributes(): string[];
Expand All @@ -52,6 +64,13 @@ export interface CacheV1 {
addToHasMany(key: string, recordDatas: Cache[], idx?: number): void;
removeFromHasMany(key: string, recordDatas: Cache[]): void;

shouldDirtyRelationship(key: string, propertyName: string, newValue: unknown): boolean;
isRelationshipDirty(key: string, propertyName: string): boolean;
hasDirtyRelationships(key: string): boolean;
changedRelationships(key: string): ChangedRelationshipsHash;
hasChangedRelationships(key: string): boolean;
rollbackRelationships(key: string): string[];

// State
// =============
setIsDeleted(isDeleted: boolean): void;
Expand Down
47 changes: 46 additions & 1 deletion packages/adapter/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -863,7 +863,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 @@ -903,6 +903,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';
2 changes: 2 additions & 0 deletions packages/graph/src/-private.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
export { graphFor, peekGraph } from './-private/graph/index';
export { isBelongsTo, isHasMany } from './-private/graph/-utils';
export { addToInverse, removeFromInverse } from './-private/graph/operations/replace-related-records';

/**
* <p align="center">
Expand Down
38 changes: 18 additions & 20 deletions packages/graph/src/-private/graph/graph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import type { Dict } from '@ember-data/types/q/utils';

import BelongsToRelationship from '../relationships/state/belongs-to';
import ManyRelationship from '../relationships/state/has-many';
import type { EdgeCache, UpgradedMeta } from './-edge-definition';
import { ImplicitRelationship } from '../relationships/state/relationships';
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<CacheStoreWrapper, 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 @@ -129,12 +128,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 @@ -492,6 +486,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 721e8a1

Please sign in to comment.