From 6089e680698f600e0b805dce67b8c57abab089db Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Fri, 27 Dec 2019 21:50:26 +0000 Subject: [PATCH] Add relations interfaces and deprecate bunch of cache/model methods --- addon/-private/cache.ts | 5 +- addon/-private/fields/attr.ts | 6 +- addon/-private/fields/has-many.ts | 37 ++++++- addon/-private/fields/has-one.ts | 12 ++- addon/-private/fields/key.ts | 6 +- addon/-private/model.ts | 126 ++++++++++++----------- addon/-private/relations.ts | 110 ++++++++++++++++++++ addon/-private/relationships/has-many.js | 11 -- addon/-private/store.ts | 5 +- package.json | 1 + tests/integration/model-test.js | 75 +++++++------- yarn.lock | 5 + 12 files changed, 274 insertions(+), 125 deletions(-) create mode 100644 addon/-private/relations.ts delete mode 100644 addon/-private/relationships/has-many.js diff --git a/addon/-private/cache.ts b/addon/-private/cache.ts index c8aa09c6..a0a0d0f8 100644 --- a/addon/-private/cache.ts +++ b/addon/-private/cache.ts @@ -261,9 +261,12 @@ export default class Cache { } } + /** + * @deprecated + */ findAll(type: string, options?: object): Model[] { deprecate( - '`Cache.findAll(type)` is deprecated, use `Cache.findRecords(type)`.' + '`Cache#findAll(type)` is deprecated, use `Cache#findRecords(type)`.' ); return this.findRecords(type, options); } diff --git a/addon/-private/fields/attr.ts b/addon/-private/fields/attr.ts index 0822ee3a..cf152ee5 100644 --- a/addon/-private/fields/attr.ts +++ b/addon/-private/fields/attr.ts @@ -1,14 +1,16 @@ import { computed } from '@ember/object'; import { Dict } from '@orbit/utils'; +import Model from '../model'; + export default function(type: string, options: Dict = {}) { options.type = type; return computed({ - get(key) { + get(this: Model, key) { return this.getAttribute(key); }, - set(key, value) { + set(this: Model, key, value) { const oldValue = this.getAttribute(key); if (value !== oldValue) { diff --git a/addon/-private/fields/has-many.ts b/addon/-private/fields/has-many.ts index fbbe469d..2e62e725 100644 --- a/addon/-private/fields/has-many.ts +++ b/addon/-private/fields/has-many.ts @@ -1,13 +1,46 @@ import { computed } from '@ember/object'; import { Dict } from '@orbit/utils'; +import { RecordIdentity } from '@orbit/data'; +import Orbit from '@orbit/core'; +import { DEBUG } from '@glimmer/env'; + +import Model from '../model'; + +const { deprecate } = Orbit; export default function(type: string, options: Dict = {}) { options.kind = 'hasMany'; options.type = type; return computed({ - get(key) { - return this.getRelatedRecords(key); + get(this: Model, key): Model[] { + let records = this.hasMany(key).value; + + if (DEBUG) { + records = [...records]; + } + + const pushObject = (record: RecordIdentity) => { + deprecate( + '`HasMany#pushObject(record)` is deprecated, use `Model#hasMany(relationship).add(record)`.' + ); + return this.hasMany(key).add(record); + }; + const removeObject = (record: RecordIdentity) => { + deprecate( + '`HasMany#removeObject(record)` is deprecated, use `Model#hasMany(relationship).remove(record)`.' + ); + return this.hasMany(key).remove(record); + }; + + Object.defineProperty(records, 'pushObject', { value: pushObject }); + Object.defineProperty(records, 'removeObject', { value: removeObject }); + + if (DEBUG) { + Object.freeze(records); + } + + return records; } }) .meta({ diff --git a/addon/-private/fields/has-one.ts b/addon/-private/fields/has-one.ts index 4e99fbdd..3e4d231d 100644 --- a/addon/-private/fields/has-one.ts +++ b/addon/-private/fields/has-one.ts @@ -1,19 +1,21 @@ import { computed } from '@ember/object'; import { Dict } from '@orbit/utils'; +import Model from '../model'; + export default function(type: string, options: Dict = {}) { options.kind = 'hasOne'; options.type = type; return computed({ - get(key) { - return this.getRelatedRecord(key); + get(this: Model, key) { + return this.hasOne(key).value; }, - set(key, value) { - const oldValue = this.getRelatedRecord(key); + set(this: Model, key, value: Model | null) { + const oldValue = this.hasOne(key).value; if (value !== oldValue) { - this.replaceRelatedRecord(key, value); + this.hasOne(key).replace(value); } return value; diff --git a/addon/-private/fields/key.ts b/addon/-private/fields/key.ts index ea0e7935..31827bdb 100644 --- a/addon/-private/fields/key.ts +++ b/addon/-private/fields/key.ts @@ -1,14 +1,16 @@ import { computed } from '@ember/object'; import { Dict } from '@orbit/utils'; +import Model from '../model'; + export default function(options: Dict = {}) { options.type = 'string'; return computed({ - get(name) { + get(this: Model, name) { return this.getKey(name); }, - set(name, value) { + set(this: Model, name, value: string) { const oldValue = this.getKey(name); if (value !== oldValue) { diff --git a/addon/-private/model.ts b/addon/-private/model.ts index f4d82815..04ea4256 100644 --- a/addon/-private/model.ts +++ b/addon/-private/model.ts @@ -1,4 +1,5 @@ import EmberObject from '@ember/object'; +import Orbit from '@orbit/core'; import { Dict } from '@orbit/utils'; import { Record, @@ -8,22 +9,21 @@ import { RelationshipDefinition } from '@orbit/data'; -import HasMany from './relationships/has-many'; +const { deprecate } = Orbit; + +import { HasOneRelation, HasManyRelation } from './relations'; import Store from './store'; export interface ModelSettings { identity: RecordIdentity; } -interface HasManyContract { - invalidate(): void; -} - export default class Model extends EmberObject { identity!: RecordIdentity; private _store?: Store; - private _relatedRecords: Dict = {}; + private _hasManyRelations: Dict = {}; + private _hasOneRelations: Dict = {}; get id(): string { return this.identity.id; @@ -56,6 +56,28 @@ export default class Model extends EmberObject { ); } + hasMany(name: string): HasManyRelation { + let relationship = this._hasManyRelations[name]; + if (!relationship) { + this._hasManyRelations[name] = relationship = new HasManyRelation( + this, + name + ); + } + return relationship; + } + + hasOne(name: string): HasOneRelation { + let relationship = this._hasOneRelations[name]; + if (!relationship) { + this._hasOneRelations[name] = relationship = new HasOneRelation( + this, + name + ); + } + return relationship; + } + getAttribute(field: string): any { return this.store.cache.peekAttribute(this.identity, field); } @@ -71,87 +93,69 @@ export default class Model extends EmberObject { ); } - getRelatedRecord(relationship: string): Record | null | undefined { - return this.store.cache.peekRelatedRecord(this.identity, relationship); + /** + * @deprecated + */ + getRelatedRecord(relationship: string): Record | null { + deprecate( + '`Model#getRelatedRecord(relationship)` is deprecated, use `Model#hasOne(relationship).value`.' + ); + return this.hasOne(relationship).value; } + /** + * @deprecated + */ async replaceRelatedRecord( relationship: string, - relatedRecord: Model | null, + record: RecordIdentity | null, options?: object ): Promise { - await this.store.update( - t => - t.replaceRelatedRecord( - this.identity, - relationship, - relatedRecord ? relatedRecord.identity : null - ), - options + deprecate( + '`Model#replaceRelatedRecord(relationship, record)` is deprecated, use `Model#hasOne(relationship).replace(record)`.' ); + await this.hasOne(relationship).replace(record, options); } - getRelatedRecords(relationship: string) { - this._relatedRecords = this._relatedRecords || {}; - - if (!this._relatedRecords[relationship]) { - this._relatedRecords[relationship] = HasMany.create({ - getContent: () => - this.store.cache.peekRelatedRecords(this.identity, relationship), - addToContent: (record: Model): Promise => { - return this.addToRelatedRecords(relationship, record); - }, - removeFromContent: (record: Model): Promise => { - return this.removeFromRelatedRecords(relationship, record); - } - }); - } - this._relatedRecords[relationship].invalidate(); - - return this._relatedRecords[relationship]; - } - + /** + * @deprecated + */ async addToRelatedRecords( relationship: string, - record: Model, + record: RecordIdentity, options?: object ): Promise { - await this.store.update( - t => t.addToRelatedRecords(this.identity, relationship, record.identity), - options + deprecate( + '`Model#addToRelatedRecords(relationship, record)` is deprecated, use `Model#hasMany(relationship).add(record)`.' ); + await this.hasMany(relationship).add(record, options); } + /** + * @deprecated + */ async removeFromRelatedRecords( relationship: string, - record: Model, + record: RecordIdentity, options?: object ): Promise { - await this.store.update( - t => - t.removeFromRelatedRecords( - this.identity, - relationship, - record.identity - ), - options + deprecate( + '`Model#removeFromRelatedRecords(relationship, record)` is deprecated, use `Model#hasMany(relationship).remove(record)`.' ); + await this.hasMany(relationship).remove(record, options); } + /** + * @deprecated + */ async replaceAttributes( properties: Dict = {}, options?: object ): Promise { - const keys = Object.keys(properties); - await this.store - .update( - t => - keys.map(key => - t.replaceAttribute(this.identity, key, properties[key]) - ), - options - ) - .then(() => this); + deprecate( + '`Model#replaceAttributes(properties)` is deprecated, use `Model#update(properties)`.' + ); + await this.update(properties, options); } async update( @@ -176,7 +180,7 @@ export default class Model extends EmberObject { } } - private get store(): Store { + get store(): Store { if (!this._store) { throw new Error('record has been removed from Store'); } diff --git a/addon/-private/relations.ts b/addon/-private/relations.ts new file mode 100644 index 00000000..f95ab461 --- /dev/null +++ b/addon/-private/relations.ts @@ -0,0 +1,110 @@ +import { RecordIdentity, cloneRecordIdentity } from '@orbit/data'; +import { DEBUG } from '@glimmer/env'; + +import Model from './model'; + +export class Relation { + readonly name: string; + readonly owner: Model; + + constructor(owner: Model, name: string) { + this.name = name; + this.owner = owner; + } +} + +export class HasOneRelation extends Relation { + get value(): Model | null { + return ( + this.owner.store.cache.peekRelatedRecord( + this.owner.identity, + this.name + ) || null + ); + } + + query(options?: object): Promise { + return this.owner.store.query( + q => q.findRelatedRecord(this.owner.identity, this.name), + options + ); + } + + async replace( + record: RecordIdentity | null, + options?: object + ): Promise { + await this.owner.store.update( + t => + t.replaceRelatedRecord( + this.owner.identity, + this.name, + record ? cloneRecordIdentity(record) : null + ), + options + ); + } +} + +export class HasManyRelation extends Relation { + get value(): Model[] { + const records = + this.owner.store.cache.peekRelatedRecords( + this.owner.identity, + this.name + ) || []; + + if (DEBUG) { + Object.freeze(records); + } + + return records; + } + + get ids(): string[] { + return this.value.map((record: RecordIdentity) => record.id); + } + + query(options?: object): Promise { + return this.owner.store.query( + q => q.findRelatedRecords(this.owner.identity, this.name), + options + ); + } + + async add(record: RecordIdentity, options?: object): Promise { + await this.owner.store.update( + t => + t.addToRelatedRecords( + this.owner.identity, + this.name, + cloneRecordIdentity(record) + ), + options + ); + } + + async remove(record: RecordIdentity, options?: object): Promise { + await this.owner.store.update( + t => + t.removeFromRelatedRecords( + this.owner.identity, + this.name, + cloneRecordIdentity(record) + ), + options + ); + } + + async replace(records: RecordIdentity[], options?: object): Promise { + await this.owner.store.update( + t => + t.replaceRelatedRecords( + this.owner.identity, + this.name, + records.map(record => cloneRecordIdentity(record)) + ), + options + ); + } +} diff --git a/addon/-private/relationships/has-many.js b/addon/-private/relationships/has-many.js deleted file mode 100644 index b3caf79f..00000000 --- a/addon/-private/relationships/has-many.js +++ /dev/null @@ -1,11 +0,0 @@ -import ReadOnlyArrayProxy from '../system/read-only-array-proxy'; - -export default ReadOnlyArrayProxy.extend({ - pushObject(record) { - return this.addToContent(record); - }, - - removeObject(record) { - return this.removeFromContent(record); - } -}); diff --git a/addon/-private/store.ts b/addon/-private/store.ts index 3c0d5c9a..2eabd7ef 100644 --- a/addon/-private/store.ts +++ b/addon/-private/store.ts @@ -148,9 +148,12 @@ export default class Store { await this.update(t => t.removeRecord(identity), options); } + /** + * @deprecated + */ findAll(type: string, options?: object): Promise { deprecate( - '`Store.findAll(type)` is deprecated, use `Store.findRecords(type)`.' + '`Store#findAll(type)` is deprecated, use `Store#findRecords(type)`.' ); return this.findRecords(type, options); } diff --git a/package.json b/package.json index b2759cb0..767745f8 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "postpublish": "ember ts:clean" }, "dependencies": { + "@glimmer/env": "^0.1.7", "@orbit/coordinator": "^0.16.4", "@orbit/core": "^0.16.3", "@orbit/data": "^0.16.4", diff --git a/tests/integration/model-test.js b/tests/integration/model-test.js index 69a2c6b9..7cbd3aaf 100644 --- a/tests/integration/model-test.js +++ b/tests/integration/model-test.js @@ -104,7 +104,7 @@ module('Integration - Model', function(hooks) { const jupiter = await store.addRecord({ type: 'planet', name: 'Jupiter' }); const callisto = await store.addRecord({ type: 'moon', name: 'Callisto' }); - await jupiter.moons.pushObject(callisto); + await jupiter.hasMany('moons').add(callisto); assert.ok(jupiter.moons.includes(callisto), 'added record to hasMany'); assert.equal(callisto.planet, jupiter, 'updated inverse'); @@ -114,8 +114,8 @@ module('Integration - Model', function(hooks) { const jupiter = await store.addRecord({ type: 'planet', name: 'Jupiter' }); const callisto = await store.addRecord({ type: 'moon', name: 'Callisto' }); - await jupiter.moons.pushObject(callisto); - await jupiter.moons.removeObject(callisto); + await jupiter.hasMany('moons').add(callisto); + await jupiter.hasMany('moons').remove(callisto); assert.ok(!jupiter.moons.includes(callisto), 'removed record from hasMany'); assert.ok(!callisto.planet, 'updated inverse'); @@ -201,17 +201,6 @@ module('Integration - Model', function(hooks) { assert.equal(record.getAttribute('name'), 'Jupiter2'); }); - test('#replaceAttributes', async function(assert) { - const record = await store.addRecord({ type: 'planet', name: 'Jupiter' }); - await record.replaceAttributes({ - name: 'Jupiter2', - classification: 'gas giant2' - }); - - assert.equal(record.name, 'Jupiter2'); - assert.equal(record.classification, 'gas giant2'); - }); - test('#replaceKey', async function(assert) { const record = await store.addRecord({ type: 'planet', @@ -269,24 +258,25 @@ module('Integration - Model', function(hooks) { ); }); - test('#getRelatedRecord / #replaceRelatedRecord', async function(assert) { + test('#hasOne', async function(assert) { const jupiter = await store.addRecord({ type: 'planet', name: 'Jupiter' }); const sun = await store.addRecord({ type: 'star', name: 'Sun' }); - assert.strictEqual(jupiter.sun, undefined); - assert.strictEqual(jupiter.getRelatedRecord('sun'), undefined); + assert.strictEqual(jupiter.sun, null); + assert.strictEqual(jupiter.hasOne('sun').value, null); - await jupiter.replaceRelatedRecord('sun', sun); + await jupiter.hasOne('sun').replace(sun); assert.strictEqual(jupiter.sun, sun); - assert.strictEqual(jupiter.getRelatedRecord('sun'), sun); + assert.strictEqual(jupiter.hasOne('sun').value, sun); + assert.strictEqual(await jupiter.hasOne('sun').query(), sun); - await jupiter.replaceRelatedRecord('sun', null); + await jupiter.hasOne('sun').replace(null); assert.strictEqual(jupiter.sun, null); - assert.strictEqual(jupiter.getRelatedRecord('sun'), null); + assert.strictEqual(jupiter.hasOne('sun').value, null); }); - test('#getRelatedRecords always returns the same LiveQuery', async function(assert) { + test('#hasMany always returns the same relationship', async function(assert) { const callisto = await store.addRecord({ type: 'moon', name: 'Callisto' }); const sun = await store.addRecord({ type: 'star', name: 'Sun' }); const jupiter = await store.addRecord({ @@ -300,35 +290,40 @@ module('Integration - Model', function(hooks) { [callisto], 'moons relationship has been added' ); - assert.strictEqual( + assert.deepEqual( jupiter.moons, - jupiter.getRelatedRecords('moons'), - 'getRelatedRecords returns the expected LiveQuery' + jupiter.hasMany('moons').value, + 'hasMany().value returns the expected array' + ); + assert.deepEqual( + jupiter.moons, + await jupiter.hasMany('moons').query(), + 'hasMany().query() returns the expected array' ); assert.strictEqual( - jupiter.getRelatedRecords('moons'), - jupiter.getRelatedRecords('moons'), - 'getRelatedRecords does not create additional LiveQueries' + jupiter.moons, + jupiter.moons, + 'hasMany attribute does not create additional arrays' ); }); - test('#addToRelatedRecords', async function(assert) { + test('#hasMany().add()', async function(assert) { const jupiter = await store.addRecord({ type: 'planet', name: 'Jupiter' }); const europa = await store.addRecord({ type: 'moon', name: 'Europa' }); const io = await store.addRecord({ type: 'moon', name: 'Io' }); - assert.deepEqual(jupiter.getRelatedRecords('moons').content, undefined); + assert.deepEqual(jupiter.hasMany('moons').value, []); - await jupiter.addToRelatedRecords('moons', europa); + await jupiter.hasMany('moons').add(europa); - assert.deepEqual(jupiter.getRelatedRecords('moons').content, [europa]); + assert.deepEqual(jupiter.hasMany('moons').value, [europa]); - await jupiter.addToRelatedRecords('moons', io); + await jupiter.hasMany('moons').add(io); - assert.deepEqual(jupiter.getRelatedRecords('moons').content, [europa, io]); + assert.deepEqual(jupiter.hasMany('moons').value, [europa, io]); }); - test('#removeFromRelatedRecords', async function(assert) { + test('#hasMany().remove()', async function(assert) { const europa = await store.addRecord({ type: 'moon', name: 'Europa' }); const io = await store.addRecord({ type: 'moon', name: 'Io' }); const jupiter = await store.addRecord({ @@ -337,15 +332,15 @@ module('Integration - Model', function(hooks) { moons: [europa, io] }); - assert.deepEqual(jupiter.getRelatedRecords('moons').content, [europa, io]); + assert.deepEqual(jupiter.hasMany('moons').value, [europa, io]); - await jupiter.removeFromRelatedRecords('moons', europa); + await jupiter.hasMany('moons').remove(europa); - assert.deepEqual(jupiter.getRelatedRecords('moons').content, [io]); + assert.deepEqual(jupiter.hasMany('moons').value, [io]); - await jupiter.removeFromRelatedRecords('moons', io); + await jupiter.hasMany('moons').remove(io); - assert.deepEqual(jupiter.getRelatedRecords('moons').content, []); + assert.deepEqual(jupiter.hasMany('moons').value, []); }); test('#update - updates attribute and relationships (with records)', async function(assert) { diff --git a/yarn.lock b/yarn.lock index 553301b0..79515f10 100644 --- a/yarn.lock +++ b/yarn.lock @@ -861,6 +861,11 @@ resolve "^1.8.1" semver "^5.6.0" +"@glimmer/env@^0.1.7": + version "0.1.7" + resolved "https://registry.yarnpkg.com/@glimmer/env/-/env-0.1.7.tgz#fd2d2b55a9029c6b37a6c935e8c8871ae70dfa07" + integrity sha1-/S0rVakCnGs3psk16MiHGucN+gc= + "@glimmer/interfaces@^0.44.0": version "0.44.0" resolved "https://registry.yarnpkg.com/@glimmer/interfaces/-/interfaces-0.44.0.tgz#0896204815f05fd8907b5703cbaee9d1b9edf5d3"