diff --git a/packages/-ember-data/package.json b/packages/-ember-data/package.json index 8fbe38295cb..778fc56d413 100644 --- a/packages/-ember-data/package.json +++ b/packages/-ember-data/package.json @@ -50,6 +50,7 @@ "broccoli-string-replace": "^0.1.2", "broccoli-test-helper": "^2.0.0", "broccoli-uglify-sourcemap": "^3.0.0", + "ember-compatibility-helpers": "^1.2.0", "ember-cli": "^3.12.0", "ember-cli-app-version": "^3.2.0", "ember-cli-dependency-checker": "^3.2.0", diff --git a/packages/-ember-data/tests/unit/model-test.js b/packages/-ember-data/tests/unit/model-test.js index ebfc7ee9600..c38a1aaa8b9 100644 --- a/packages/-ember-data/tests/unit/model-test.js +++ b/packages/-ember-data/tests/unit/model-test.js @@ -14,6 +14,7 @@ import JSONSerializer from '@ember-data/serializer/json'; import { attr as DSattr } from '@ember-data/model'; import { recordDataFor } from 'ember-data/-private'; import { attr, hasMany, belongsTo } from '@ember-data/model'; +import { gte } from 'ember-compatibility-helpers'; module('unit/model - Model', function(hooks) { setupTest(hooks); @@ -1442,6 +1443,42 @@ module('unit/model - Model', function(hooks) { await cat.save(); }); + + if (gte('3.10.0')) { + test('@attr decorator works without parens', async function(assert) { + assert.expect(1); + let cat; + + class Mascot extends Model { + @attr name; + } + + this.owner.register('model:mascot', Mascot); + adapter.updateRecord = function() { + assert.deepEqual(cat.changedAttributes(), { + name: ['Argon', 'Helia'], + }); + + return { data: { id: '1', type: 'mascot' } }; + }; + + cat = store.push({ + data: { + type: 'mascot', + id: '1', + attributes: { + name: 'Argon', + }, + }, + }); + + cat.setProperties({ + name: 'Helia', + }); + + await cat.save(); + }); + } }); module('Misc', function() { diff --git a/packages/-ember-data/tests/unit/model/relationships-test.js b/packages/-ember-data/tests/unit/model/relationships-test.js index 62496064ac1..860c1f18d03 100644 --- a/packages/-ember-data/tests/unit/model/relationships-test.js +++ b/packages/-ember-data/tests/unit/model/relationships-test.js @@ -3,6 +3,7 @@ import { module, test } from 'qunit'; import Model from '@ember-data/model'; import { hasMany, belongsTo } from '@ember-data/model'; import { get } from '@ember/object'; +import { gte } from 'ember-compatibility-helpers'; class Person extends Model { @hasMany('occupation', { async: false }) occupations; @@ -125,4 +126,35 @@ module('[@ember-data/model] unit - relationships', function(hooks) { assert.equal(relationship.meta.name, 'streamItems', 'relationship name has not been changed'); }); + + if (gte('3.10.0')) { + test('decorators works without parens', function(assert) { + let store; + let { owner } = this; + + class StreamItem extends Model { + @belongsTo user; + } + + class User extends Model { + @hasMany streamItems; + } + + owner.unregister('model:user'); + owner.register('model:stream-item', StreamItem); + owner.register('model:user', User); + + store = owner.lookup('service:store'); + + let user = store.modelFor('user'); + + const relationships = get(user, 'relationships'); + + assert.ok(relationships.has('stream-item'), 'relationship key has been normalized'); + + const relationship = relationships.get('stream-item')[0]; + + assert.equal(relationship.meta.name, 'streamItems', 'relationship name has not been changed'); + }); + } }); diff --git a/packages/model/addon/-private/attr.js b/packages/model/addon/-private/attr.js index 3598d0a165c..0330f2b49a2 100644 --- a/packages/model/addon/-private/attr.js +++ b/packages/model/addon/-private/attr.js @@ -3,6 +3,7 @@ import { assert } from '@ember/debug'; import { DEBUG } from '@glimmer/env'; import { recordDataFor } from '@ember-data/store/-private'; import { RECORD_DATA_ERRORS } from '@ember-data/canary-features'; +import { computedMacroWithOptionalParams } from './util'; /** @module @ember-data/model @@ -107,7 +108,7 @@ function hasValue(internalModel, key) { @param {Object} options a hash of options @return {Attribute} */ -export default function attr(type, options) { +function attr(type, options) { if (typeof type === 'object') { options = type; type = undefined; @@ -160,3 +161,5 @@ export default function attr(type, options) { }, }).meta(meta); } + +export default computedMacroWithOptionalParams(attr); diff --git a/packages/model/addon/-private/belongs-to.js b/packages/model/addon/-private/belongs-to.js index 24b965a0f86..f696031626d 100644 --- a/packages/model/addon/-private/belongs-to.js +++ b/packages/model/addon/-private/belongs-to.js @@ -2,6 +2,7 @@ import { computed } from '@ember/object'; import { assert, warn, inspect } from '@ember/debug'; import { normalizeModelName } from '@ember-data/store'; import { DEBUG } from '@glimmer/env'; +import { computedMacroWithOptionalParams } from './util'; /** @module @ember-data/model @@ -103,7 +104,7 @@ import { DEBUG } from '@glimmer/env'; @param {Object} options (optional) a hash of options @return {Ember.computed} relationship */ -export default function belongsTo(modelName, options) { +function belongsTo(modelName, options) { let opts, userEnteredModelName; if (typeof modelName === 'object') { opts = modelName; @@ -180,3 +181,5 @@ export default function belongsTo(modelName, options) { }, }).meta(meta); } + +export default computedMacroWithOptionalParams(belongsTo); diff --git a/packages/model/addon/-private/has-many.js b/packages/model/addon/-private/has-many.js index 9d7439b2ba2..671eec0b742 100644 --- a/packages/model/addon/-private/has-many.js +++ b/packages/model/addon/-private/has-many.js @@ -5,6 +5,7 @@ import { computed } from '@ember/object'; import { assert, inspect } from '@ember/debug'; import { normalizeModelName } from '@ember-data/store'; import { DEBUG } from '@glimmer/env'; +import { computedMacroWithOptionalParams } from './util'; /** `hasMany` is used to define One-To-Many and Many-To-Many @@ -141,7 +142,7 @@ import { DEBUG } from '@glimmer/env'; @param {Object} options (optional) a hash of options @return {Ember.computed} relationship */ -export default function hasMany(type, options) { +function hasMany(type, options) { if (typeof type === 'object') { options = type; type = undefined; @@ -199,3 +200,5 @@ export default function hasMany(type, options) { }, }).meta(meta); } + +export default computedMacroWithOptionalParams(hasMany); diff --git a/packages/model/addon/-private/util.ts b/packages/model/addon/-private/util.ts new file mode 100644 index 00000000000..d6cc8f1a512 --- /dev/null +++ b/packages/model/addon/-private/util.ts @@ -0,0 +1,31 @@ +import { gte } from 'ember-compatibility-helpers'; + +export type DecoratorPropertyDescriptor = PropertyDescriptor & { initializer?: any } | undefined; + +export function isElementDescriptor(args: any[]): args is [object, string, DecoratorPropertyDescriptor] { + let [maybeTarget, maybeKey, maybeDesc] = args; + + return ( + // Ensure we have the right number of args + args.length === 3 && + // Make sure the target is a class or object (prototype) + (typeof maybeTarget === 'function' || (typeof maybeTarget === 'object' && maybeTarget !== null)) && + // Make sure the key is a string + typeof maybeKey === 'string' && + // Make sure the descriptor is the right shape + ((typeof maybeDesc === 'object' && + maybeDesc !== null && + 'enumerable' in maybeDesc && + 'configurable' in maybeDesc) || + // TS compatibility + maybeDesc === undefined) + ); +} + +export function computedMacroWithOptionalParams(fn) { + if (gte('3.10.0')) { + return (...maybeDesc: any[]) => (isElementDescriptor(maybeDesc) ? fn()(...maybeDesc) : fn(...maybeDesc)); + } else { + return fn; + } +} diff --git a/packages/model/package.json b/packages/model/package.json index e441ceb0a01..6fe5820fae4 100644 --- a/packages/model/package.json +++ b/packages/model/package.json @@ -24,6 +24,7 @@ "@ember-data/-build-infra": "3.14.0-alpha.1", "@ember-data/canary-features": "3.14.0-alpha.1", "@ember-data/store": "3.14.0-alpha.1", + "ember-compatibility-helpers": "^1.2.0", "ember-cli-babel": "^7.10.0", "ember-cli-string-utils": "^1.1.0", "ember-cli-test-info": "^1.0.0",