
Security News
Attackers Are Hunting High-Impact Node.js Maintainers in a Coordinated Social Engineering Campaign
Multiple high-impact npm maintainers confirm they have been targeted in the same social engineering campaign that compromised Axios.
todo-manager
Advanced tools
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:
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.

IEntity| Properties | Type | Description |
|---|---|---|
id? | `undefined | Id` |
type | EntityType | This field is used by the guards to identify which model is. |
ITaskDirectly extends from IEntity and does not add any property.
IBoard| Properties | Type | Description |
|---|---|---|
flow | IFlow | The flow this board is associated to. |
tasks | EntityCollection<ITask & ISaved> | An entity collection with the tasks contained in this board. |
taskSteps | Map<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| Properties | Type | Description |
|---|---|---|
steps | EntityCollection<IFlowStep & ISaved> | An entity collection with the steps of the flow. |
defaultStepId? | `undefined | Id` |
IFlowStepDirectly extends from IEntity and does not add any property.
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.
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> {}
}
export enum EntityType {
Task,
Board,
Flow,
FlowStep
}
export type Id = string | number | symbol;
export type EntityCollection<Entity> = Map<Id, Entity>;
export type IAnyEntity = ITask | IFlowStep | IBoard | IFlow;
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.
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.
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.getRequired. Given a type and id, it should return an entity of that type and id, or undefined if it does not exists.
ISourceOperator.setRequired. 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.deleteRequired. Called to "delete" an IEntity.
ISourceOperator.listShould 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.
ISourceOperator.getTaskBoardShould 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.
ISourceOperator.getStepFlowShould 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.
ISourceOperator.getTasksWithStepShould 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.
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.
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}});
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.
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.
Requires A1
list: (type: EntityType) => Promise<EntityCollection<IEntity & ISaved>>
Returns an entity collection with all the entities of a certain type.
save: (entity: IEntity) => Promise<IEntity & ISaved>
Save an entity. Notice that the returned entity could have updated the id, createAt or updateAt fields.
create: (type: EntityType) => (props: IEntityCreationProps) => Promise<IEntity & ISaved>
Creates an entity.
update: (data: IEntityUpdateProps) => (entity: Entity) => IEntity
Updates the current entity with new data.
delete: <E extends IEntity>(entity: E) => Promise<E>
If entity is saved, request deletion to the source. Then returns a copy of the entity.
clone: <E extends IEntity>(entity: E) => E
The identity function. Effectively, creates a copy of the entity.
refresh: (entity: IEntity) => Promise<IEntity & ISaved | undefined>
Update the entity from the source. Effectively, concatenates getId and get.
refreshOrFail: (entity: IEntity) => Promise<IEntity & ISaved>
Update the entity from the source. Effectively, concatenates getId and getOrFail.
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.
getProp: <K extends keyof IEntity>(prop: K) => (entity: IEntity) => IEntity[K]
Extracts a property from the entity.
toCollection: <E extends IEntity>(entities: Iterable<E & ISaved>) => EntityCollection<E & ISaved>
Creates a collection from an iterable of saved entities.
mergeCollections: <E extends IEntity>(collections: Iterable<EntityCollection<E & ISaved>>) => EntityCollection<E & ISaved>
Merges several collections into a new single one.
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.
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:
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.
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.
list: () => Promise<EntityCollection<ITask & ISaved>>
Required A1
Returns an entity collection with all the tasks.
create: (props: ITaskCreationProps) => Promise<ITask & ISaved>
Creates a task.
getBoard: (task: ITask | Id) => Promise<(IBoard & ISaved) | undefined>
Required R1
Returns the board at which task belongs, if any.
getTaskStep: (task: (ITask & ISaved) | Id) => Promise<(IFlowStep & ISaved) | undefined>
Required R1
Returns the associated flowStep in its board, if any.
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.
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:
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.
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.
list: () => Promise<EntityCollection<IFlowStep & ISaved>>
Required A1
Returns an entity collection with all the flowSteps.
create: (props: IFlowStepCreationProps) => Promise<IFlowStep & ISaved>
Creates a flowStep.
getFlow: (flowStep: IFlowStep | Id) => Promise<IFlow & ISaved>
Required R2
Returns the flow at which flow step belongs.
getTasks: (flowStep: IFlowStep | Id) => Promise<EntityCollection<ITask & ISaved>>
Required ST1
Returns the associated tasks.
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:
get: (id: IFlow | Id) => Promise<IFlow | undefined>
Returns a flow by id from the source. If a flow is provided the entity is refreshed.
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.
list: () => Promise<EntityCollection<IFlow & ISaved>>
Required A1
Returns an entity collection with all the flows.
create: (props: IFlowCreationProps) => Promise<IFlow & ISaved>
Creates a flow.
getSteps: (flow: IFlow | Id) => EntityCollection<IFlowStep & ISaved>
Returns the flowSteps belonging to the flow.
addStep: (flowStep: IFlowStep | Id) => (flow: IFlow | Id) => Promise<IFlow>
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.
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.
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:
get: (id: IBoard | Id) => Promise<IBoard | undefined>
Returns a board by id from the source. If a board is provided the entity is refreshed.
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.
list: () => Promise<EntityCollection<IBoard & ISaved>>
Required A1
Returns an entity collection with all the boards.
create: (props: IBoardCreationProps) => Promise<IBoard & ISaved>
Creates a board.
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.
removeTask: (task: (ITask & ISaved) | Id) => (board: IBoard | Id) => Promise<IBoard>
Removes a task from the board.
hasTask: (task: (ITask & ISaved)| Id) => (board: IBoard | Id) => Promise<boolean>
Returns true if the board has the task, false otherwise.
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.
getTaskStep: (task: (ITask & ISaved) | Id) => (board: IBoard | Id) => Promise<IFlowStep & ISaved>
Returns the flowStep associated to the task in the context of the board.
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.
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),
}
});
);
The library provides errors with the objective of catching and handling invalid actions or wrong parameters and distinguish them from unexpected errors.
The following errors are implemented:
TodoManagerErrorNotImplementedErrorEntityNotFoundErrorSavingRequiredErrorBoardTaskWithoutStepErrorInvalidFlowStepErrorFlowStepInUseErrorInvalidBoardAssociationErrorTodoManagerErrorExtends from Error.
Description: Base error for extending all library errors as well as the application ones.
NotImplementedErrorExtends 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.
EntityNotFoundErrorExtends 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.
SavingRequiredErrorExtends 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.
BoardTaskWithoutStepErrorExtends from TodoManagerError.
Description: Tried to attach a task without indicating the flowStep to a board without defaultStepId
InvalidFlowStepErrorExtends 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.
FlowStepInUseErrorExtends 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.
InvalidBoardAssociationErrorExtends 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.
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);
}
}
FAQs
A simple library to manage tasks
We found that todo-manager demonstrated a not healthy version release cadence and project activity because the last version was released a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
Multiple high-impact npm maintainers confirm they have been targeted in the same social engineering campaign that compromised Axios.

Security News
Axios compromise traced to social engineering, showing how attacks on maintainers can bypass controls and expose the broader software supply chain.

Security News
Node.js has paused its bug bounty program after funding ended, removing payouts for vulnerability reports but keeping its security process unchanged.