Business logic as in:
- Commands/Actions: Mutate things
- Queries/Abilities: Questions that retrieve facts
Let's focus on abilities here (would be very similar for commands, too):
Keep business logic in plain js, ok that's easy. Hard part:
- How to inject dependencies?
- How to create an API that can also be used from somewhere else, e.g. statecharts (guards or actions)
Ability in pure js:
// plain-js/blog/aggregates/post/abilities.ts
export function canEdit(post: Post, user: User) {
return post.author === user || user.admin;
}
In a component:
import { canEdit } from 'plain-js/blog/aggregates/post/abilities';
export default class Post extends Component {
@service declare currentUser: Services['currentUser'];
<template>
{{#if (canEdit @post this.currentUser.user)}}
edit functionality here
{{/if}}
</template>
}
- It works 🎉
- Broad usage of pure business logic
- Clear purpose
- Composable
- Reasonable
- Framework agnostic
- Ember: As long as
@post
andcurrentUser.user
are tracked, this is re-evaluating
- Couples components to services
Curry the plain js ability from above with ember-ability
:
Ref: https://github.com/gossi/ember-ability/blob/main/ember-ability/src/index.ts
import { ability } from 'ember-ability';
import { canEdit as upstreamCanEdit } from 'plain-js/blog/aggregates/post/abilities';
export const canEdit = ability((post: Post, { services }) => {
return upstreamCanEdit(post, services.user.currentUser);
})
Usage from within ember (polaris):
import { canEdit } from '../the/location/above';
<template>
{{#if (canEdit @post)}}
edit functionality here
{{/if}}
</template>
- Clear purpose
- Composable
- Reasonable
- Framework agnostic (for the pure js part)
- Decouples component from services
- Wrapper feels like "Duplication"
Create the ability in plain ts directly curried
// plain-js/blog/aggregates/post/abilities.ts
import { ability } from 'ember-ability';
export const canEdit = ability((post: Post, { services })) {
const { currentUser: user } = services.user;
return post.author === user || user.admin;
}
Usage from within ember (polaris)
import { canEdit } from '../the/location/above';
<template>
{{#if (canEdit @post)}}
edit functionality here
{{/if}}
</template>
- Only declare it once
- Coupled to DI container
- Dependencies in "plain-js-package" to ember deps
- Coupled to these ember deps
- Barely composable
// somewhere-in-ember/blog/aggregates/post/abilities.ts
import { ability } from 'ember-ability';
export const canEdit = ability((post: Post, { services })) {
const { currentUser: user } = services.user;
return post.author === user || user.admin;
}
Usage from within ember (polaris)
import { canEdit } from '../the/location/above';
<template>
{{#if (canEdit @post)}}
edit functionality here
{{/if}}
</template>
- Reasonable
- Decouples component from services
- Less code to write
- Coupled to Ember
- Unlikely be used outside ember
- Barely composable