@pallad/cascade
TypeScript icon, indicating that this package has built-in type declarations

1.1.0 • Public • Published

Cascade 💣 ➡️ 💥 ➡️ 💥

Run cascade actions from your code instead database


CircleCI npm version License: MIT

@pallad/cascade is a library to run cascade actions on entities.

For example action delete on an Article entity containing images triggers removal of Image entities. If Image entity has anything else to remove as well they might trigger deletion of other entities, and so on and so forth.

Use cases

  • cascade deletion on databases not supporting relations or cascade actions like DynamoDB, MongoDB, Redis etc.
  • cascade updates of cached views
  • running extra logic on creation of new entries
  • achieving cascade logic between multiple persistence models (key/value database <-> filesystem <-> RDBMS etc)
  • great for apps that tries to leverage polyglot persistence

Features

  • 👷 Built with Typescript
  • 📏 Simple but powerful 🔥
  • 🧑‍🤝‍🧑 Context friendly for forwarding extra information (like transaction handle)
  • ❤️ Integration with @pallad/modules

Installation

npm install @pallad/cascade

Modules

If you already use alpha-dic and would like to integrate with @pallad/modules install @pallad/cascade-module

npm install @pallad/cascade-module

Why should I use it?

If you can (and want to) handle all cascade actions within a database then great and you probably don't need @pallad/cascade at all :)

However it is still a great tool for

  • databases that does not support cascade actions like DynamoDB, MongoDB, Redis etc.
  • cascade updates of cached views
  • running extra logic on creation of new entries
  • handling cascade multiple persistence modesl (key/value database <-> filesystem <-> RDBMS etc)
  • apps that tries to leverage polyglot persistence

Concepts

The basic conceptions are very simple but it very important to understand them to get in speed with handling cascade actions.

Rule

Rule performs an action on a target. It decides whether is able to handle given target, if not then Action will not ask it to handle it.

import {Rule} from '@pallad/cascade';

const deleteImageRule: Rule = {
	supports(target) {
		return target instanceof Image;
	},
	run(target: Image): Rule.Result | Promise<Rule.Result> {
		// perform image deletion
		// at this stage we're sure that target is an instanceof Image
		// since otherwise it would not be called
	}
}

Action

Aggregates rules and controls the flow of execution. Once action starts the following steps are being performed:

  1. iterates over the rules one by one
  2. check if target is supported by the rule (via supports). If not then moved to another rule, otherwise calls run on it.
  3. If rule returns a promise then awaits for its resolution.
  4. If the final result of rule handling returns anything iterable then action is ran on every target.

Manager

Just aggregates all actions in one place.

Examples

Basic example

import {Rule} from '@pallad/cascade';

class Article {
	id!: string;
	images!: Image[];
}

class Image {
	id!: string;
}

const deleteArticleRule: Rule = {
	supports(target) {
		return target instanceof Article;
	},
	run(target: Article) {
		console.log('Removing article', target.id);

		return target.images;
	}
}
const deleteImageRule: Rule = {
	supports(target) {
		return target instanceof Image;
	},
	run(target: Image) {
		console.log('Removing image', target.id);
	}
}
const deleteAction = new Action([
	deleteArticleRule,
	deleteImageRule
]);

const article = Object.assign(new Article, {
	id: 'a1',
	images: [
		Object.assign(new Image(), {id: 'i1'}),
		Object.assign(new Image(), {id: 'i2'}),
	]
});
deleteAction.run(article);

// Removing article a1
// Removing image i1
// Removing image i2

More entities + counter invalidation

A little bit more advanced with more entity types and articles counter invalidation. Note the order of execution:

  1. First Stash is being deleted
  2. then Article with id: a1
  3. then images for that article
  4. then Article with id: a2
  5. and so on and so forth
import {Rule, Action} from '@pallad/cascade';

class Article {
	id!: string;
	images!: Image[];
}

class Image {
	id!: string;
}

class Stash {
	id!: string;
	entries!: Article[];
}

const deleteArticleRule: Rule = {
	supports(target) {
		return target instanceof Article;
	},
	run(target: Article) {
		console.log('Removing article', target.id);
		return target.images;
	}
}
const deleteImageRule: Rule = {
	supports(target) {
		return target instanceof Image;
	},
	run(target: Image) {
		console.log('Removing image', target.id);
	}
}

const deleteStashRule: Rule = {
	supports(target) {
		return target instanceof Stash;
	},
	run(target: Stash) {
		console.log('Removing stash', target.id);
		return target.entries;
	}
}

const updateArticlesCounterRule: Rule = {
	supports(target) {
		return target instanceof Article;
	},
	run(target: Article) {
		console.log('Updating articles counter of', target.id);
	}
}
const deleteAction = new Action([
	deleteArticleRule,
	deleteImageRule,
	deleteStashRule,
	updateArticlesCounterRule,
]);

const article1 = Object.assign(new Article, {
	id: 'a1',
	images: [
		Object.assign(new Image(), {id: 'i1'}),
		Object.assign(new Image(), {id: 'i2'}),
	]
});

const article2 = Object.assign(new Article, {
	id: 'a1',
	images: [
		Object.assign(new Image(), {id: 'i3'}),
		Object.assign(new Image(), {id: 'i4'}),
	]
});

const stash = Object.assign(new Stash, {
	id: 's1',
	entries: [article1, article2]
});

deleteAction.run(stash);

// Removing stash s1
// Removing article a1
// Removing image i1
// Removing image i2
// Updating articles counter of a1
// Removing article a1
// Removing image i3
// Removing image i4
// Updating articles counter of a1

Wrapping in transactions

import {Rule, Action} from '@pallad/cascade';
import {Knex} from 'knex';

interface DeleteActionContext {
	transaction: Knex.Transaction;
}

class Article {
	id!: string;
	images!: Image[];
}

class Image {
	id!: string;
}

const deleteArticleRule: Rule<DeleteActionContext> = {
	supports(target) {
		return target instanceof Article;
	},
	run(target: Article, context) {
		console.log('Removing article', target.id);
		// now you 
		context.transaction('articles').delete().where('id', target.id);
		return target.images;
	}
}
const deleteImageRule: Rule<DeleteActionContext> = {
	supports(target) {
		return target instanceof Image;
	},
	run(target: Image, context) {
		console.log('Removing image', target.id);
		context.transaction('images').delete().where('id', target.id);
	}
}

const deleteAction = new Action<DeleteActionContext>([
	deleteArticleRule,
	deleteImageRule,
]);

const article = Object.assign(new Article, {
	id: 'a1',
	images: [
		Object.assign(new Image(), {id: 'i1'}),
		Object.assign(new Image(), {id: 'i2'}),
	]
});

const transaction = knex.transaction()

knex.transaction((trx) => {
	return deleteAction.run(article, {
		transaction: trx
	});
});

// Removing article a1
// Removing image i1
// Removing image i2

Tips

How to distinguish targets if my entities are just pure javascript objects (POJO)?

You have to wrap them within extra object to indicate its type or use @pallad/entity-ref

Readme

Keywords

none

Package Sidebar

Install

npm i @pallad/cascade

Weekly Downloads

6

Version

1.1.0

License

MIT

Unpacked Size

36.6 kB

Total Files

14

Last publish

Collaborators

  • wookieb