Skip to content

Commit

Permalink
add ember-data require async inverse relationship rule
Browse files Browse the repository at this point in the history
  • Loading branch information
wozny1989 committed Jul 8, 2024
1 parent 55cd064 commit 3221491
Show file tree
Hide file tree
Showing 6 changed files with 331 additions and 1 deletion.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,7 @@ rules in templates can be disabled with eslint directives with mustache or html
| :----------------------------------------------------------------------------- | :------------------------------------------------------------------- | :- | :- | :- |
| [no-empty-attrs](docs/rules/no-empty-attrs.md) | disallow usage of empty attributes in Ember Data models | | | |
| [use-ember-data-rfc-395-imports](docs/rules/use-ember-data-rfc-395-imports.md) | enforce usage of `@ember-data/` package imports instead `ember-data` || 🔧 | |
| [require-async-inverse-relationship](docs/rules/require-async-inverse-relationship.md) | require `async` and `inverse` properties in `@belongsTo`/`@hasMany` relationships || | |

### Ember Object

Expand Down
43 changes: 43 additions & 0 deletions docs/rules/require-async-inverse-relationship.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# ember/require-async-inverse-relationship

💼 This rule is enabled in the ✅ `recommended` [config](https://github.com/ember-cli/eslint-plugin-ember#-configurations).

<!-- end auto-generated rule header -->

This rule ensures that the `async` and `inverse` properties are specified in `@belongsTo` and `@hasMany` decorators in Ember Data models.

## Rule Details

This rule disallows:

- Using `@belongsTo` without specifying the `async` and `inverse` properties.
- Using `@hasMany` without specifying the `async` and `inverse` properties.

## Examples

Examples of **incorrect** code for this rule:

```js
import Model, { belongsTo, hasMany } from '@ember-data/model';

export default class FolderModel extends Model {
@hasMany('folder', { inverse: 'parent' }) children;
@belongsTo('folder', { inverse: 'children' }) parent;
}
```

Examples of **correct** code for this rule:

```js
import Model, { belongsTo, hasMany } from '@ember-data/model';

export default class FolderModel extends Model {
@hasMany('folder', { async: true, inverse: 'parent' }) children;
@belongsTo('folder', { async: true, inverse: 'children' }) parent;
}
```

## References

- [Deprecate Non Strict Relationships](https://deprecations.emberjs.com/id/ember-data-deprecate-non-strict-relationships)
- [Ember Data Relationships](https://guides.emberjs.com/release/models/relationships)
3 changes: 2 additions & 1 deletion lib/recommended-rules.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ module.exports = {
"ember/no-unnecessary-route-path-option": "error",
"ember/no-volatile-computed-properties": "error",
"ember/prefer-ember-test-helpers": "error",
"ember/require-async-inverse-relationship": "error",
"ember/require-computed-macros": "error",
"ember/require-computed-property-dependencies": "error",
"ember/require-return-from-computed": "error",
Expand All @@ -75,4 +76,4 @@ module.exports = {
"ember/routes-segments-snake-case": "error",
"ember/use-brace-expansion": "error",
"ember/use-ember-data-rfc-395-imports": "error"
}
}
75 changes: 75 additions & 0 deletions lib/rules/require-async-inverse-relationship.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'Require inverse to be specified in @belongsTo and @hasMany decorators',

Check failure on line 6 in lib/rules/require-async-inverse-relationship.js

View workflow job for this annotation

GitHub Actions / build (ubuntu, 18.x)

`meta.docs.description` must match the regexp /^(enforce|require|disallow)/

Check failure on line 6 in lib/rules/require-async-inverse-relationship.js

View workflow job for this annotation

GitHub Actions / build (ubuntu, 20.x)

`meta.docs.description` must match the regexp /^(enforce|require|disallow)/

Check failure on line 6 in lib/rules/require-async-inverse-relationship.js

View workflow job for this annotation

GitHub Actions / build (ubuntu, 21.x)

`meta.docs.description` must match the regexp /^(enforce|require|disallow)/
category: 'Ember Data',
recommended: true,
url: 'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/require-async-inverse-relationship.md',
},
schema: [],
},

create(context) {
return {
CallExpression(node) {
const decorator =
node.parent.type === 'Decorator' &&
['belongsTo', 'hasMany'].includes(node.callee.name) &&
node;

if (decorator) {
const args = decorator.arguments;
const hasAsync = args.some(
(arg) =>
arg.type === 'ObjectExpression' &&
arg.properties.some((prop) => prop.key.name === 'async')
);
const hasBooleanAsync = args.some(
(arg) =>
arg.type === 'ObjectExpression' &&
arg.properties.some(
(prop) => prop.key.name === 'async' && typeof prop.value.value === 'boolean'
)
);
const hasInverse = args.some(
(arg) =>
arg.type === 'ObjectExpression' &&
arg.properties.some((prop) => prop.key.name === 'inverse')
);

if (!hasAsync) {
context.report({
node,
message: 'The @{{decorator}} decorator requires an `async` property to be specified.',
data: {
decorator: decorator.callee.name,
},
});
} else if (!hasBooleanAsync) {
context.report({
node,
message:
'The @{{decorator}} decorator requires an `async` property to be specified as a boolean.',
data: {
decorator: decorator.callee.name,
},
});
}

if (!hasInverse) {
context.report({
node,
message:
'The @{{decorator}} decorator requires an `inverse` property to be specified.',
data: {
decorator: decorator.callee.name,
},
});
}
}
},
};
},
};
1 change: 1 addition & 0 deletions tests/__snapshots__/recommended.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ exports[`recommended rules has the right list 1`] = `
"no-unnecessary-route-path-option",
"no-volatile-computed-properties",
"prefer-ember-test-helpers",
"require-async-inverse-relationship",
"require-computed-macros",
"require-computed-property-dependencies",
"require-return-from-computed",
Expand Down
209 changes: 209 additions & 0 deletions tests/lib/rules/require-async-inverse-relationship.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
'use strict';

//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------

const rule = require('../../../lib/rules/require-async-inverse-relationship');
const RuleTester = require('eslint').RuleTester;

const parserOptions = { ecmaVersion: 2022, sourceType: 'module' };

const ruleTester = new RuleTester({
parserOptions,
parser: require.resolve('@babel/eslint-parser'),
});

//------------------------------------------------------------------------------
// Tests
//------------------------------------------------------------------------------

ruleTester.run('require-async-inverse-relationship', rule, {
valid: [
`import Model, { belongsTo } from '@ember-data/model';
export default class extends Model {
@belongsTo('post', { async: true, inverse: 'comments' }) post;
}`,
`import Model, { hasMany } from '@ember-data/model';
export default class extends Model {
@hasMany('comment', { async: true, inverse: 'post' }) comments;
}`,
`import Model, { belongsTo, hasMany } from '@ember-data/model';
export default class extends Model {
@belongsTo('post', { async: false, inverse: 'comments' }) post;
@hasMany('user', { async: true, inverse: null }) owner;
}`,
],

invalid: [
{
code: `import Model, { belongsTo } from '@ember-data/model';
export default class extends Model {
@belongsTo('post') post;
}`,
output: null,
errors: [
{
message: 'The @belongsTo decorator requires an `async` property to be specified.',
type: 'CallExpression',
},
{
message: 'The @belongsTo decorator requires an `inverse` property to be specified.',
type: 'CallExpression',
},
],
},
{
code: `import Model, { belongsTo } from '@ember-data/model';
export default class extends Model {
@belongsTo('post', {}) post;
}`,
output: null,
errors: [
{
message: 'The @belongsTo decorator requires an `async` property to be specified.',
type: 'CallExpression',
},
{
message: 'The @belongsTo decorator requires an `inverse` property to be specified.',
type: 'CallExpression',
},
],
},
{
code: `import Model, { belongsTo } from '@ember-data/model';
export default class extends Model {
@belongsTo('post', { async: 'comments'}) post;
}`,
output: null,
errors: [
{
message:
'The @belongsTo decorator requires an `async` property to be specified as a boolean.',
type: 'CallExpression',
},
{
message: 'The @belongsTo decorator requires an `inverse` property to be specified.',
type: 'CallExpression',
},
],
},
{
code: `import Model, { belongsTo } from '@ember-data/model';
export default class extends Model {
@belongsTo('post', { async: true }) post;
}`,
output: null,
errors: [
{
message: 'The @belongsTo decorator requires an `inverse` property to be specified.',
type: 'CallExpression',
},
],
},
{
code: `import Model, { belongsTo } from '@ember-data/model';
export default class extends Model {
@belongsTo('post', { inverse: 'comments' }) post;
}`,
output: null,
errors: [
{
message: 'The @belongsTo decorator requires an `async` property to be specified.',
type: 'CallExpression',
},
],
},
{
code: `import Model, { hasMany } from '@ember-data/model';
export default class extends Model {
@hasMany('comment') comments;
}`,
output: null,
errors: [
{
message: 'The @hasMany decorator requires an `async` property to be specified.',
type: 'CallExpression',
},
{
message: 'The @hasMany decorator requires an `inverse` property to be specified.',
type: 'CallExpression',
},
],
},
{
code: `import Model, { hasMany } from '@ember-data/model';
export default class extends Model {
@hasMany('comment', {}) comments;
}`,
output: null,
errors: [
{
message: 'The @hasMany decorator requires an `async` property to be specified.',
type: 'CallExpression',
},
{
message: 'The @hasMany decorator requires an `inverse` property to be specified.',
type: 'CallExpression',
},
],
},
{
code: `import Model, { hasMany } from '@ember-data/model';
export default class extends Model {
@hasMany('comment', { async: 'comments'}) comments;
}`,
output: null,
errors: [
{
message:
'The @hasMany decorator requires an `async` property to be specified as a boolean.',
type: 'CallExpression',
},
{
message: 'The @hasMany decorator requires an `inverse` property to be specified.',
type: 'CallExpression',
},
],
},
{
code: `import Model, { hasMany } from '@ember-data/model';
export default class extends Model {
@hasMany('comment', { async: true }) comments;
}`,
output: null,
errors: [
{
message: 'The @hasMany decorator requires an `inverse` property to be specified.',
type: 'CallExpression',
},
],
},
{
code: `import Model, { hasMany } from '@ember-data/model';
export default class extends Model {
@hasMany('comment', { inverse: 'post' }) comments;
}`,
output: null,
errors: [
{
message: 'The @hasMany decorator requires an `async` property to be specified.',
type: 'CallExpression',
},
],
},
],
});

0 comments on commit 3221491

Please sign in to comment.