Skip to content

Instantly share code, notes, and snippets.

@gossi
Created September 21, 2022 16:14
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save gossi/fccff6ccfe477bf989eb1f189ad97656 to your computer and use it in GitHub Desktop.
Save gossi/fccff6ccfe477bf989eb1f189ad97656 to your computer and use it in GitHub Desktop.

Plain Business Logic w/ Ember

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)

Option 1: Pure JS

Definition

Ability in pure js:

// plain-js/blog/aggregates/post/abilities.ts

export function canEdit(post: Post, user: User) {
  return post.author === user || user.admin;
}

Usage

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>
}

Advantages

  • It works 🎉
  • Broad usage of pure business logic
  • Clear purpose
  • Composable
  • Reasonable
  • Framework agnostic
  • Ember: As long as @post and currentUser.user are tracked, this is re-evaluating

Disadvantages

  • Couples components to services

Option 2: Currying

Definition

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

Usage from within ember (polaris):

import { canEdit } from '../the/location/above';

<template>
  {{#if (canEdit @post)}}
    edit functionality here
  {{/if}}
</template>

Advantages

  • Clear purpose
  • Composable
  • Reasonable
  • Framework agnostic (for the pure js part)
  • Decouples component from services

Disadvantages

  • Wrapper feels like "Duplication"

Option 3: Plain JS w/ Ember Abilities

Definition

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

Usage from within ember (polaris)

import { canEdit } from '../the/location/above';

<template>
  {{#if (canEdit @post)}}
    edit functionality here
  {{/if}}
</template>

Advantages

  • Only declare it once

Disadvantages

  • Coupled to DI container
  • Dependencies in "plain-js-package" to ember deps
  • Coupled to these ember deps
  • Barely composable

Option 4: Abilities within Ember

Definition

// 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

Usage from within ember (polaris)

import { canEdit } from '../the/location/above';

<template>
  {{#if (canEdit @post)}}
    edit functionality here
  {{/if}}
</template>

Advantages

  • Reasonable
  • Decouples component from services
  • Less code to write

Disadvantages

  • Coupled to Ember
  • Unlikely be used outside ember
  • Barely composable
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment