0.1.1 • Published 2 years ago

todo-manager v0.1.1

Weekly downloads
-
License
MIT
Repository
github
Last release
2 years ago

To Do Manager

Description

The aim of this library is, given a custom service, to provide a set of models and services to manage "to do" tasks. The library modeling is based in four simple concepts:

  • Task: A "to do" task. Example: "To buy milk"" is a task.
  • A FlowStep or state of a task: In which step or state the task is. As example: a task can be pending, work in progress or done.
  • A Flow is a collection of FlowSteps. That is: a collection of the possible states a task will "flow" through. Example: {pending, WIP, done}.
  • A Board is an instance of a flow. For example, we can have a shoping list, reading list or preparatives for the wedding as different boards with the same associated flow {pending, in progress, done}.

Index

  1. Models
  2. Operators
  3. Errors

Models

  1. Introduction
  2. Description
  3. Extending models in your application
  4. Other definitions

From design, models used are plain objects.

The library use four main models: ITask, IBoard, IFlow, IFlowStep. A fifth model IEntity is implemented as a base model from which all previous four inherit properties. The guards used internally infer the type of an entity (ITask, IBoard,...) from the property type: EntityType, which mark the type of the entity.

From design, models are always plain objects, and can be extended in your application by using custom declaration files. Also, the uniqueness of the relations task-board (R1) and flowStep-flow (R2) are configurable.

The following diagram illustrates the relation between the main models.

Models diagram

Description

IEntity
PropertiesTypeDescription
id?undefined | IdThe identification of the object, provided by the input service. Notice that when an object is created, before being saved, it has an undefined id. In that case, if some process require the id to be executed, a SavingRequiredError will be raised.
typeEntityTypeThis field is used by the guards to identify which model is.
ITask

Directly extends from IEntity and does not add any property.

IBoard
PropertiesTypeDescription
flowIFlowThe flow this board is associated to.
tasksEntityCollection<ITask & ISaved>An entity collection with the tasks contained in this board.
taskStepsMap<Id, Id>A map representing the state of a task in this board. The key Id represents the id of a board's task and the second Id represents the id of one of flowStep of the associated flow.
IFlow
PropertiesTypeDescription
stepsEntityCollection<IFlowStep & ISaved>An entity collection with the steps of the flow.
defaultStepId?undefined | IdIf provided, when a task is added to a board with this associated flow, the task will be assigned to that flowStep.
IFlowStep

Directly extends from IEntity and does not add any property.

Extending models in your application

Models have been design to be minimal, so in most of the cases your application will need for an extension. Node provides a way make your own type definition declaration files so you can expand the typing of third party libraries. Usually you have to be careful when doing that, because despite you are modifying the type definition, you are not changing the implementation.

This library has been implemented in a way that any included extra field is behaving as expected.

Example 1

Suppose the case for your application a name and optionally a description are needed for main models. Furthermore, suppose each flowStep is optionally associated to a color.

Then your @types/todo-manager/index.d.ts could look like the following and the functionality of the library would work as expected.

// src/@types/todo-manager/index.d.ts
declare module 'todo-manager' {
  export type TFlowStepColor = 'red' | 'blue' | 'green';

  export interface ISaved {
    id: Id;
    createdAt: Date;
  }

  // entity
  export interface IEntity {
    id: Id;
    type: EntityType;
    createdAt?: Date;
    updatedAt?: Date;
    name: string;
    description?: string;
  }
  export interface IEntityCreationProps extends Omit<IEntity, 'id' | 'type' | 'createdAt'> {}
  export interface IEntityUpdateProps extends Partial<IEntityCreationProps> {}

  // task
  export interface ITask extends IEntity {
    type: EntityType.Task;
  }
  export interface ITaskCreationProps extends Omit<ITask, 'id' | 'type' | 'createdAt'> {}
  export interface ITaskUpdateProps extends Partial<ITaskCreationProps> {}

  // flow step
  export interface IFlowStep extends IEntity {
    type: EntityType.FlowStep;
    color?: TFlowStepColor;
  }
  export interface IFlowStepCreationProps extends Omit<IFlowStep, 'id' | 'type' | 'createdAt'> {}
  export interface IFlowStepUpdateProps extends Partial<IFlowStepCreationProps> {}

  // flow
  export interface IFlow extends IEntity {
    type: EntityType.Flow;
    steps: EntityCollection<IFlowStep>;
    defaultStepId?: Id;
    order: Id[];
  }
  export interface IFlowCreationProps extends Omit<IFlow, 'id' | 'type' | 'createdAt'> {}
  export interface IFlowUpdateProps extends Partial<IFlowCreationProps> {}

  // board
  export interface IBoard extends IEntity {
    type: EntityType.Board;
    flow: IFlow;
    tasks: EntityCollection<ITask>;
    taskSteps: Map<Id, Id>;
  }
  export interface IBoardCreationProps extends Omit<IBoard, 'id' | 'type' | 'createdAt'> {}
  export interface IBoardUpdateProps extends Partial<IBoardCreationProps> {}
}

Other definitions

Entity type
export enum EntityType {
  Task,
  Board,
  Flow,
  FlowStep
}
Identificator
export type Id = string | number | symbol;
Entity collection
export type EntityCollection<Entity> = Map<Id, Entity>;
Any Entity
export type IAnyEntity = ITask | IFlowStep | IBoard | IFlow;

Operators

  1. Introduction
  2. How to get the operators
  3. Source operators
  4. Operators description
  5. Extending provided operators

The main goal of the library is to provide operators to perform transformation to the objects. To do so, this library uses inversify as dependency to provide a container with the operators. Concretely, the provided container has injected five class instances with the operations as methods. So, in this way, operators are classified in five groups.

Since this library pretends to be a functional programming library, all the methods have the object this properly injected, so you can use them as a independent functions freely. That also means that apply .bind, .apply or .call to the methods will not affect the this object used in the operators.

Also, to provide the container, a set of input operators to access the entities. Notice that by defining more or less operations, some output operators and functionalities will be available or not.

How to get the operators

Two ways: If your application uses inversify, the library provides a getContainer which returns directly the container.

// src/services/inversify.config.ts

import { Container } from 'inversify';
import { getContainer } from 'todo-manager';
import { sourceProvider } from './storage.provider';

const appContainerBase: Container = new Container();
/* ... Inject whatever ... */

export const appContainer = Container.merge(
  appContainerBase,
  getContainer({providers: {source: sourceProvider}})
);
// other file
import { Identifiers, ITaskOperators } from 'todo-manager';
import { appContainer } from '~/services';

const taskOperators: container.get<ITaskOperators>(Identifiers.Task);
const task = await taskOperators.create({name: 'My task'});

However, if you do not want to use the inversify way to obtain the operators, the exported function getOperators will perform the extraction for you:

// src/services/index.ts

import { getOperators } from 'todo-manager';
import { sourceProvider } from './storage.provider';

const { task: taskOperators } = getOperators({providers: {source: sourceProvider}});
export taskOperators;
// other file
import { taskOperators } from '~/services';

const task = await taskOperators.create({name: 'My task'});

Important: Observe that when calling any of the two methods a new container is generated, meaning every one has injected different class instances. So, in a standard application, getOperators or getContainer are called once and its result is exported and used application-wide.

Source operators

In order to manage objects, the library needs access to them. Thus some basic operations should be provided. The minimum needed from input operations are to get, set and delete an entity from a storage, or REST API or any source. Additionally, other operations can be given in exchange of more output functionalities.

The way to provide that input operations is through a source provider, which is a function that returns an object containing the operations. That object will be injected to the container in singleton scope using toDynamicValue and an identifier stored in the object Identifiers.Source.

More concretely, that provider should match:

import { interfaces } from 'inversify';

type MaybePromise<T> = Promise<T> | T;

export interface ISourceOperators {
  get: <E extends IEntity>(type: E['type']) => (id: Id) => MaybePromise<E & ISaved | undefined>;
  set: (entity: IEntity) => MaybePromise<IEntity | ISaved>;
  delete: (type: EntityType) => (id: Id) => MaybePromise<void>;
  // optional
  list?: <E extends IEntity>(type: E['type']) => MaybePromise<Iterable<E & ISaved>>;
  getTaskBoard?: (id: Id) => MaybePromise<IBoard & ISaved | undefined>;
  getStepFlow?: (id: Id) => MaybePromise<IFlow & ISaved>;
  getTasksWithStep?: (id: Id) => MaybePromise<Iterable<ITask & ISaved>>;
}

type TSourceProvider = (context: interfaces.Context) => ISourceOperators;

Let us explain each member:

ISourceOperator.get

Required. Given a type and id, it should return an entity of that type and id, or undefined if it does not exists.

ISourceOperator.set

Required. Called to "save" an IEntity. This process may or may not modify the actual entity data. In any case, the entity should be returned.

ISourceOperator.delete

Required. Called to "delete" an IEntity.

Option (A1): ISourceOperator.list

Should return an iterable with all the entities of a given type. If provided, operation list will be available. Otherwise, if the list operator is called, a NotImplementedError will be raised.

Option (R1): ISourceOperator.getTaskBoard

Should return the board at which the task belongs, if any.

Provide to make your tasks to belong to at much at a unique board. Otherwise, one task belonging to several boards is allowed. If provided, operations getBoard, getTaskStep and setTaskStep are available. Otherwise, if called, a NotImplementedError will be raised.

Option (R2): ISourceOperator.getStepFlow

Should return the flow at which the flowStep belongs.

Provide to make your flowSteps to belong exactly to a unique flow. Otherwise, one flowStep belonging to several flows is allowed. If provided, operation getFlow is available. Otherwise, if called, a NotImplementedError will be raised.

Option (ST1): ISourceOperator.getTasksWithStep

Should return an iterable with the tasks whose state is that flowStep.

If provided, operation getTasks and removeStep are available. Otherwise, if called, a NotImplementedError will be raised.

Operators description

The library provides the operators as methods classified in five classes. Actually, they are not exactly javascript methods but getters returning an arrow function. This has the effect of them having the object this properly injected no matter how you call the method.

Also, all operators are pure functions. So they are suitable for functional programming.

Entity Operators

This is designed to perform general operations of entities.

import { Identifiers, IEntityOperators } from 'todo-manager';

const entityOperators = getContainer({providers: {source: sourceProvider}}).get<IEntityOperators>(Identifiers.Entity);
// or
const { entity: entityOperators } = getOperators({providers: {source: sourceProvider}});
IEntityOperators.get

get: (type: EntityType) => (id: Id) => Promise<IEntity & ISaved | undefined>

Returns an entity by id from the source. undefined will be also returned if the type of the obtained entity does not match the expected type.

IEntityOperators.getOrFail

getOrFail: (type: EntityType) => (id: Id) => Promise<IEntity & ISaved>

Returns an entity by id from the source or EntityNotFoundError is raised. The error is also raised if the type of the received entity does not match the expected type.

IEntityOperators.list

Requires A1

list: (type: EntityType) => Promise<EntityCollection<IEntity & ISaved>>

Returns an entity collection with all the entities of a certain type.

IEntityOperators.save

save: (entity: IEntity) => Promise<IEntity & ISaved>

Save an entity. Notice that the returned entity could have updated the id, createAt or updateAt fields.

IEntityOperators.create

create: (type: EntityType) => (props: IEntityCreationProps) => Promise<IEntity & ISaved>

Creates an entity.

IEntityOperators.update

update: (data: IEntityUpdateProps) => (entity: Entity) => IEntity

Updates the current entity with new data.

IEntityOperators.delete

delete: <E extends IEntity>(entity: E) => Promise<E>

If entity is saved, request deletion to the source. Then returns a copy of the entity.

IEntityOperators.clone

clone: <E extends IEntity>(entity: E) => E

The identity function. Effectively, creates a copy of the entity.

IEntityOperators.refresh

refresh: (entity: IEntity) => Promise<IEntity & ISaved | undefined>

Update the entity from the source. Effectively, concatenates getId and get.

IEntityOperators.refreshOrFail

refreshOrFail: (entity: IEntity) => Promise<IEntity & ISaved>

Update the entity from the source. Effectively, concatenates getId and getOrFail.

IEntityOperators.getId

getId: (entity: IEntity | Id) => Id

If an id is passed, it just returns it. If an is entity passed, extracts the id from the entity. If entity is not saved a SavingRequiredError is thrown.

IEntityOperators.getProp

getProp: <K extends keyof IEntity>(prop: K) => (entity: IEntity) => IEntity[K]

Extracts a property from the entity.

IEntityOperators.toCollection

toCollection: <E extends IEntity>(entities: Iterable<E & ISaved>) => EntityCollection<E & ISaved>

Creates a collection from an iterable of saved entities.

IEntityOperators.mergeCollections

mergeCollections: <E extends IEntity>(collections: Iterable<EntityCollection<E & ISaved>>) => EntityCollection<E & ISaved>

Merges several collections into a new single one.

IEntityOperators.requireSavedEntity

requireSavedEntity: <E extends IEntity, RT extends any | undefined | null>(fn: (entity: E & ISaved) => RT) => (entity: E) => RT

Requires the entity to have an id. Otherwise a SavingRequiredError is thrown.

Task Operators

Operators performed over ITask.

import { Identifiers, ITaskOperators } from 'todo-manager';

const taskOperators = getContainer({providers: {source: sourceProvider}}).get<ITaskOperators>(Identifiers.Entity);
// or
const { task: taskOperators } = getOperators({providers: {source: sourceProvider}});

Have operators save, update, delete, clone, refresh, getId and getProp similarly to entity operators but with the corresponding task models. In addition:

ITaskOperators.get

get: (id: ITask & ISaved | Id) => Promise<ITask & ISaved | undefined>

Returns a task by id from the source. If a task is provided, the entity is refreshed.

ITaskOperators.getOrFail

getOrFail: (id: ITask & ISaved | Id) => Promise<ITask & ISaved>

Returns a task by id from the source or EntityNotFoundError is raised. If a task is provided, the entity is refreshed.

ITaskOperators.list

list: () => Promise<EntityCollection<ITask & ISaved>>

Required A1

Returns an entity collection with all the tasks.

ITaskOperators.create

create: (props: ITaskCreationProps) => Promise<ITask & ISaved>

Creates a task.

ITaskOperators.getBoard

getBoard: (task: ITask | Id) => Promise<(IBoard & ISaved) | undefined>

Required R1

Returns the board at which task belongs, if any.

ITaskOperators.getTaskStep

getTaskStep: (task: (ITask & ISaved) | Id) => Promise<(IFlowStep & ISaved) | undefined>

Required R1

Returns the associated flowStep in its board, if any.

ITaskOperators.setTaskStep

setTaskStep: (step: (IFlowStep & ISaved) | Id) => (task: (ITask & ISaved) | Id) => Promise<ITask & ISaved>

Required R1

Assigns a flowStep to the task in its board. The operation on the board will be saved.

If task has no board as parent, a InvalidBoardAssociationError will be raised.

If the flowStep is not valid, a InvalidFlowStepError will be raised.

Flow Step Operators

Operators performed over IFlowStep.

import { Identifiers, IFlowStepOperators } from 'todo-manager';

const flowStepOperators = getContainer({providers: {source: sourceProvider}}).get<IFlowStepOperators>(Identifiers.Entity);
// or
const { flowStep: flowStepOperators } = getOperators({providers: {source: sourceProvider}});

Have operators save, update, delete, clone, refresh, getId and getProp similarly to entity operators but with the corresponding flowStep models. In addition:

IFlowStepOperators.get

get: (id: IFlowStep | Id) => Promise<IFlowStep | undefined>

Returns a flow step by id from the source. If a flowStep is provided the entity is refreshed.

IFlowStepOperators.getOrFail

getOrFail: (id: IFlowStep | Id) => Promise<IFlowStep>

Returns a flowStep by id from the source or EntityNotFoundError is raised. If a flowStep is provided the entity is refreshed.

IFlowStepOperators.list

list: () => Promise<EntityCollection<IFlowStep & ISaved>>

Required A1

Returns an entity collection with all the flowSteps.

IFlowStepOperators.create

create: (props: IFlowStepCreationProps) => Promise<IFlowStep & ISaved>

Creates a flowStep.

IFlowStepOperators.getFlow

getFlow: (flowStep: IFlowStep | Id) => Promise<IFlow & ISaved>

Required R2

Returns the flow at which flow step belongs.

IFlowStepOperators.getTasks

getTasks: (flowStep: IFlowStep | Id) => Promise<EntityCollection<ITask & ISaved>>

Required ST1

Returns the associated tasks.

Flow Operators

Operators performed over IFlow.

import { Identifiers, IFlowOperators } from 'todo-manager';

const flowOperators = getContainer({providers: {source: sourceProvider}}).get<IFlowOperators>(Identifiers.Entity);
// or
const { flow: flowOperators } = getOperators({providers: {source: sourceProvider}});

Have operators save, update, delete, clone, refresh, getId and getProp similarly to entity operators but with the corresponding flow models. In addition:

IFlowOperators.get

get: (id: IFlow | Id) => Promise<IFlow | undefined>

Returns a flow by id from the source. If a flow is provided the entity is refreshed.

IFlowOperators.getOrFail

getOrFail: (id: IFlow | Id) => Promise<IFlow>

Returns a flow by id from the source or EntityNotFoundError is raised. If a flow is provided the entity is refreshed.

IFlowOperators.list

list: () => Promise<EntityCollection<IFlow & ISaved>>

Required A1

Returns an entity collection with all the flows.

IFlowOperators.create

create: (props: IFlowCreationProps) => Promise<IFlow & ISaved>

Creates a flow.

IFlowOperators.getSteps

getSteps: (flow: IFlow | Id) => EntityCollection<IFlowStep & ISaved>

Returns the flowSteps belonging to the flow.

IFlowOperators.addStep

addStep: (flowStep: IFlowStep | Id) => (flow: IFlow | Id) => Promise<IFlow>

Required either not R2 or ST1

Adds a flowStep to the flow. If option R2 is selected, the flowStep will be first removed from the current parent, and the operation will be saved. If the current flowStep have associated tasks, an error FlowStepInUseError will be raised.

IFlowOperators.removeStep

removeStep: (flowStep: IFlowStep | Id) => (flow: IFlow | Id) => Promise<IFlow>

Required ST1

Removes a flowStep from the flow. If the current flowStep have associated tasks, an error FlowStepInUseError will be raised.

Board Operators

Operators performed over IBoard.

import { Identifiers, IBoardOperators } from 'todo-manager';

const boardOperators = getContainer({providers: {source: sourceProvider}}).get<IBoardOperators>(Identifiers.Entity);
// or
const { board: boardOperators } = getOperators({providers: {source: sourceProvider}});

Have operators save, update, delete, clone, refresh, getId and getProp similarly to entity operators but with the corresponding board models. In addition:

IBoardOperators.get

get: (id: IBoard | Id) => Promise<IBoard | undefined>

Returns a board by id from the source. If a board is provided the entity is refreshed.

IBoardOperators.getOrFail

getOrFail: (id: IBoard | Id) => Promise<IBoard>

Returns a board by id from the source or EntityNotFoundError is raised. If a board is provided the entity is refreshed.

IBoardOperators.list

list: () => Promise<EntityCollection<IBoard & ISaved>>

Required A1

Returns an entity collection with all the boards.

IBoardOperators.create

create: (props: IBoardCreationProps) => Promise<IBoard & ISaved>

Creates a board.

IBoardOperators.addTask

addTask: (task: (ITask & ISaved) | Id) => (board: IBoard | Id) => Promise<IBoard>

Adds a task to the board. If R1 is set, first the task will be removed from its current board. That operation will be saved.

IBoardOperators.removeTask

removeTask: (task: (ITask & ISaved) | Id) => (board: IBoard | Id) => Promise<IBoard>

Removes a task from the board.

IBoardOperators.hasTask

hasTask: (task: (ITask & ISaved)| Id) => (board: IBoard | Id) => Promise<boolean>

Returns true if the board has the task, false otherwise.

IBoardOperators.getTaskStepId

getTaskStepId: (task: (ITask & ISaved) | Id) => (board: IBoard | Id) => Proimse<Id>

Returns the flowStep's id associated to the task in the context of the board.

IBoardOperators.getTaskStep

getTaskStep: (task: (ITask & ISaved) | Id) => (board: IBoard | Id) => Promise<IFlowStep & ISaved>

Returns the flowStep associated to the task in the context of the board.

IBoardOperators.setTaskStep

setTaskStep: (step: (IFlowStep & ISaved) | Id) => (task: (ITask & ISaved) | Id) => (board: IBoard) => Promise<IBoard>

Set a task's state to a flowStep in the context of the board.

Extending operators

All five entity, task, flowStep, flow and board operators can be extended or override by providing a new class. The following example illustrates how it can be done:

// src/services/inversify.config.ts

import { Container } from 'inversify';
import { getContainer } from 'todo-manager';
import { SourceOperators } from './storage';
import { TaskOperators } from './task';
import { FlowStepOperators } from './flow-step';
import { FlowOperators } from './flow';
import { BoardOperators } from './board';

const appContainerBase: Container = new Container();
/* ... Inject whatever ... */


export const appContainer = Container.merge(
  appContainerBase,
  getContainer({
    providers: {
      source: (context) => new SourceOperators(),
      task: (context) => new TaskOperators(context),
      flowStep: (context) => new FlowStepOperators(context),
      flow: (context) => new FlowOperators(context),
      board: (context) => new BoardOperators(context),
    }
  });
);

Errors

  1. Introduction
  2. Descriptions
  3. Extending errors

The library provides errors with the objective of catching and handling invalid actions or wrong parameters and distinguish them from unexpected errors.

Descriptions

The following errors are implemented:

TodoManagerError

Extends from Error.

Description: Base error for extending all library errors as well as the application ones.

NotImplementedError

Extends from TodoManagerError.

Description: Tried to access some property or perform an action not available with the current configuration.

Example To try to call getBoard on a task when configuration does not assert uniqueness of boards on tasks.

EntityNotFoundError

Extends from TodoManagerError.

Description: Operation expected an entity but no entity is not obtained, or the obtained entity has not the expected type.

Example To call getOrFail expecting a task and receiving a board.

SavingRequiredError

Extends from TodoManagerError.

Description: Tried to access some property or perform an action over an instance without id that requires that object to be saved.

Example To try to attach a recently created task to a board will raise that error.

BoardTaskWithoutStepError

Extends from TodoManagerError.

Description: Tried to attach a task without indicating the flowStep to a board without defaultStepId

InvalidFlowStepError

Extends from TodoManagerError.

Description: Tried to change a task from current flowStep to an invalid flowStep

Example In a context of a board, when associated flow have defined allowedNextIds and try to change task's flowStep to another flowStep which is not in the first's allowedNextIds set.

FlowStepInUseError

Extends from TodoManagerError.

Description: Tried to perform some action over a flowStep that required to not to have any task associated.

Example: To try to delete a flowStep with some associated task.

InvalidBoardAssociationError

Extends from TodoManagerError.

Description: Tried to perform some action over or with an entity not associated to a correct board.

Example: To try assign a flowStep to a task with a board which a flow which has not the flowStep.

Extending errors

The provided errors are designed to cover a general purpose cases of logically invalid actions. But it may happen that for your application logic other cases of invalid actions happen. If so, it is a good idea to create an error based of TodoManagerError as follows.

export class MyCustomInvalidActionError extends TodoManagerError {
  constructor(m: string) {
    super(m);
    Object.setPrototypeOf(this, MyCustomInvalidActionError.prototype);
  }
}