npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@fickou/adonis-workflow

v1.0.5

Published

Generic workflow engine for AdonisJS 5 + Lucid ORM

Readme

@fickou/adonis-workflow

Moteur de workflow générique pour AdonisJS 5 + Lucid ORM.

S'attache à n'importe quelle entité métier (invoice, purchase_order, leave_request…) sans importer de modèles métier — uniquement via entity_type (string) + entity_id (string).


Table des matières

  1. Installation
  2. Migrations
  3. Enregistrement du provider
  4. Configuration
  5. Implémenter l'EntityResolver
  6. Utiliser le WorkflowEngine
  7. ActionHooks — logique métier transactionnelle
  8. Écouter les événements
  9. RBAC — rôles et permissions
  10. Délégations
  11. RuleEngineService — conditions dynamiques
  12. WebSocket temps réel
  13. Commandes Ace
  14. Référence des routes API
  15. Codes d'erreur

1. Installation

npm install @fickou/adonis-workflow

Vérifier que les peer dependencies sont présentes dans votre projet hôte :

"@adonisjs/core": "^5.8.0",
"@adonisjs/lucid": "^18.0.0",
"luxon": "^3.0.0"

2. Migrations

Le package embarque ses propres migrations. Ajoutez leur chemin dans config/database.tspas besoin de les copier :

// config/database.ts
import { join } from 'path'
import Application from '@ioc:Adonis/Core/Application'

const databaseConfig: DatabaseConfig = {
  connection: 'sqlite',
  connections: {
    sqlite: {
      client: 'sqlite3',
      connection: {
        filename: Application.tmpPath('db.sqlite3'),
      },
      useNullAsDefault: true,
      migrations: {
        paths: [
          Application.makePath('database/migrations'),
          join(__dirname, '../node_modules/@fickou/adonis-workflow/build/migrations'),
        ],
      },
    },
  },
}

Lancer les migrations :

node ace migration:run

Les migrations sont numérotées 001011. Tables créées : workflow_definitions, workflow_steps, workflow_transitions, workflow_step_validators, workflow_step_actions, workflow_step_conditions, workflow_instances, workflow_instance_steps, workflow_action_logs, workflow_delegations, workflow_definition_snapshots.


3. Enregistrement du provider

Dans .adonisrc.json :

{
  "providers": [
    "@fickou/adonis-workflow/build/providers/WorkflowProvider"
  ],
  "commands": [
    "@fickou/adonis-workflow/build/commands"
  ],
  "preloads": [
    "./start/workflow"
  ]
}

4. Configuration

Créer config/workflow.ts :

// config/workflow.ts
import { WorkflowProviderConfig } from '@fickou/adonis-workflow/build/providers/WorkflowProvider'

const workflowConfig: WorkflowProviderConfig = {
  // Préfixe des routes (défaut : '/workflow')
  routePrefix: '/workflow',

  // Résolveur de rôle RBAC — retourner le rôle workflow de l'utilisateur
  // Rôles valides : 'workflow_admin' | 'workflow_manager' | 'workflow_user'
  roleResolver: async (userId: string) => {
    const { default: User } = await import('App/Models/User')
    const user = await User.find(userId)
    if (user?.isAdmin) return 'workflow_admin'
    if (user?.isManager) return 'workflow_manager'
    return 'workflow_user'
  },
}

export default workflowConfig

5. Implémenter l'EntityResolver

L'EntityResolver permet au moteur d'accéder aux données de vos entités métier sans les importer directement.

Créer start/workflow.ts et le déclarer dans preloads (voir section 3) :

// start/workflow.ts
import Application from '@ioc:Adonis/Core/Application'

const registry = Application.container.use('Workflow/EntityResolverRegistry')

// Resolver pour les factures
registry.register('invoice', {
  async resolve(id: string) {
    // Import dynamique obligatoire : évite les dépendances circulaires
    const { default: Invoice } = await import('App/Models/Invoice')
    const invoice = await Invoice.find(id)
    return invoice?.serialize() ?? null
  },
  async exists(id: string) {
    const { default: Invoice } = await import('App/Models/Invoice')
    return !!(await Invoice.find(id))
  },
})

// Ajouter autant de resolvers que d'entity_type dans votre projet
// registry.register('purchase_order', { ... })
// registry.register('leave_request', { ... })

Si un entity_type n'est pas enregistré, WorkflowEngine lève WorkflowException('ENTITY_NOT_FOUND').


6. Utiliser le WorkflowEngine

Récupérer le moteur depuis le container IoC dans vos controllers :

// app/Controllers/Http/InvoiceController.ts
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
import Application from '@ioc:Adonis/Core/Application'

export default class InvoiceController {
  public async submit({ request, response }: HttpContextContract) {
    const engine = Application.container.use('Workflow/WorkflowEngine')
    const userId = request.header('X-User-Id')!

    // Démarrer un workflow — signature : (entityType, entityId, initiatorId)
    const instance = await engine.startWorkflow('invoice', params.id, userId)

    return response.created({ instance })
  }

  public async action({ params, request, response }: HttpContextContract) {
    const engine = Application.container.use('Workflow/WorkflowEngine')
    const userId = request.header('X-User-Id')!

    // Exécuter une action — signature : (instanceId, action, actorId, payload)
    await engine.executeAction(
      params.instanceId,
      request.input('action'),   // 'approve' | 'reject_cancel' | ...
      userId,
      {
        version: request.input('version'),  // verrouillage optimiste — OBLIGATOIRE
        reason: request.input('reason'),
      }
    )

    return response.ok({ message: 'Action exécutée.' })
  }
}

Actions disponibles

| Action | Description | reason obligatoire | |---|---|---| | approve | Valide l'étape courante | Non | | reject_previous | Renvoie à l'étape précédente | Non | | reject_start | Renvoie au début du workflow | Non | | reject_cancel | Annule définitivement le workflow | Oui | | request_correction | Renvoie au créateur pour correction | Oui | | transfer | Transfère l'étape à un autre validateur (delegate_id requis) | Oui | | suspend | Met l'instance en pause | Oui |


7. ActionHooks — logique métier transactionnelle

Les ActionHooks s'exécutent à l'intérieur de la transaction DB d'une action. Un retour { allowed: false } déclenche un rollback complet.

// start/workflow.ts (suite)
import Application from '@ioc:Adonis/Core/Application'

const hookRegistry = Application.container.use('Workflow/ActionHookRegistry')

// Hook exécuté avant chaque 'approve' sur les purchase_order
hookRegistry.register('purchase_order', 'approve', {
  async execute(context) {
    const { entityData, trx } = context

    // TOUTES les opérations DB dans un hook DOIVENT utiliser `trx`
    const { default: Budget } = await import('App/Models/Budget')
    const budget = await Budget.query({ client: trx })
      .where('department_id', entityData.department_id as string)
      .firstOrFail()

    if (budget.available < (entityData.amount as number)) {
      return {
        allowed: false,
        reason: `Budget insuffisant. Disponible : ${budget.available} €`,
      }
    }

    budget.available -= entityData.amount as number
    await budget.useTransaction(trx).save()

    return { allowed: true }
  },
})

Les hooks sont optionnels. Si aucun hook n'est enregistré pour (entityType, action), l'action passe directement.


8. Écouter les événements

Le moteur n'envoie jamais de notifications directement. Le projet hôte écoute les événements préfixés workflow: dans start/events.ts :

// start/events.ts
import Event from '@ioc:Adonis/Core/Event'
import { WORKFLOW_EVENTS } from '@fickou/adonis-workflow/build/events/WorkflowEvents'

// Workflow démarré
Event.on(WORKFLOW_EVENTS.STARTED, ({ instance, initiatorId }) => {
  // Notifier le créateur...
})

// Action exécutée (approve, reject, etc.)
Event.on(WORKFLOW_EVENTS.ACTION_EXECUTED, ({ instance, action, actorId }) => {
  // Logger, notifier les parties concernées...
})

// Workflow terminé
Event.on(WORKFLOW_EVENTS.COMPLETED, ({ instance }) => {
  // Notifier le créateur de l'approbation finale...
})

// Délai dépassé (émis par workflow:check-deadlines)
Event.on(WORKFLOW_EVENTS.DEADLINE_EXCEEDED, ({ instanceId, stepId, exceededSince }) => {
  // Envoyer une relance au validateur...
})

// Hook bloquant (action refusée par la logique métier)
Event.on(WORKFLOW_EVENTS.ACTION_HOOK_REJECTED, ({ instanceId, action, reason }) => {
  // Notifier l'acteur via WebSocket...
})

Tableau complet des événements

| Constante | Chaîne émise | Déclencheur | |---|---|---| | STARTED | workflow:started | startWorkflow() | | COMPLETED | workflow:completed | Dernière étape approuvée | | REJECTED | workflow:rejected | reject_cancel | | ACTION_EXECUTED | workflow:action:executed | Toute action réussie | | ACTION_HOOK_REJECTED | workflow:action:hook_rejected | Hook retourne allowed: false | | STEP_COMPLETED | workflow:step:completed | Étape validée, transition | | STEP_SUSPENDED | workflow:step:suspended | suspend | | STEP_TRANSFERRED | workflow:step:transferred | transfer | | CORRECTION_REQUESTED | workflow:correction:requested | request_correction | | CORRECTION_SUBMITTED | workflow:correction:submitted | resubmitAfterCorrection() | | DELEGATION_CREATED | workflow:delegation:created | Création délégation | | DELEGATION_REVOKED | workflow:delegation:revoked | Révocation délégation | | DEADLINE_EXCEEDED | workflow:deadline:exceeded | handleDeadline() via cron | | CONCURRENT_CONFLICT | workflow:concurrent:conflict | Conflit de version optimiste |


9. RBAC — rôles et permissions

Hiérarchie des rôles

| Rôle | Accès | |---|---| | workflow_admin | Tout : CRUD définitions, monitoring, statistiques, forcer des actions | | workflow_manager | Lecture toutes instances + statistiques | | workflow_user | Exécuter des actions sur ses étapes assignées |

Vérification manuelle dans un controller

import Application from '@ioc:Adonis/Core/Application'

const permService = Application.container.use('Workflow/WorkflowPermissionService')

// Vérifier un rôle minimum (lève WorkflowException 403 si insuffisant)
await permService.requireRole(userId, 'workflow_manager')

10. Délégations

Un utilisateur peut déléguer ses droits de validation pour une période donnée.

import Application from '@ioc:Adonis/Core/Application'
import { DateTime } from 'luxon'

const delegationService = Application.container.use('Workflow/WorkflowDelegationService')

// Créer une délégation
// Signature : (delegatorId, delegateId, scope, validFrom, validUntil, type, workflowDefinitionId?, stepId?)
await delegationService.createDelegation(
  'user-123',           // délégant
  'user-456',           // délégué
  'workflow',           // 'global' | 'workflow' | 'step'
  DateTime.now(),
  DateTime.now().plus({ days: 7 }),
  'delegation',         // 'delegation' | 'substitution'
  42,                   // workflowDefinitionId (requis si scope = 'workflow' ou 'step')
)

// Révoquer
await delegationService.revokeDelegation(delegationId, revokedByUserId)

Priorité de résolution : step > workflow > global


11. RuleEngineService — conditions dynamiques

Les conditions sur les étapes et transitions sont définies en JSON et évaluées par le RuleEngineService.

Format d'un RuleSet

{
  "operator": "AND",
  "conditions": [
    { "field": "amount", "op": ">", "value": 10000 },
    { "field": "status", "op": "==", "value": "draft" }
  ]
}

Opérateurs disponibles

==, !=, <, >, <=, >=, in, not_in, between, contains, starts_with, is_empty, is_null, is_not_null, custom_query

Handler personnalisé (custom_query)

import Application from '@ioc:Adonis/Core/Application'

const ruleEngine = Application.container.use('Workflow/RuleEngineService')

ruleEngine.registerCustomHandler('check_budget', async (context, params) => {
  const { default: Budget } = await import('App/Models/Budget')
  const budget = await Budget.findBy('department_id', context.department_id as string)
  return (budget?.available ?? 0) >= (params.minAmount as number)
})

Utilisation dans une condition :

{ "field": "custom", "op": "custom_query", "value": { "handler": "check_budget", "minAmount": 5000 } }

Un handler non trouvé lève WorkflowException('UNKNOWN_CUSTOM_HANDLER').


12. WebSocket temps réel

Si @adonisjs/websocket est installé, le moteur pousse des notifications sur les channels workflow:instance:{id}.

Le WorkflowProvider enregistre automatiquement le channel si le binding Adonis/Addons/Ws est présent. WebSocket est optionnel — si absent, les actions fonctionnent normalement.

Messages envoyés automatiquement

| Événement WS | Déclencheur | |---|---| | action_executed | Toute action réussie | | step_changed | Transition vers une nouvelle étape | | status_changed | Changement de statut de l'instance |


13. Commandes Ace

workflow:seed

Crée un workflow d'exemple invoice (2 étapes avec condition sur le montant) pour le développement.

node ace workflow:seed

À lancer avant db:seed si vos seeders applicatifs démarrent des workflows.

Séquence complète pour initialiser un environnement de dev :

node ace migration:fresh && node ace workflow:seed && node ace db:seed

workflow:check-deadlines

Vérifie toutes les étapes actives et émet workflow:deadline:exceeded pour celles dépassées.

node ace workflow:check-deadlines

À planifier en cron toutes les 5 à 15 minutes :

*/10 * * * * cd /chemin/vers/projet && node ace workflow:check-deadlines >> storage/logs/deadlines.log 2>&1

workflow:migrate

Affiche le chemin des migrations du package (à référencer dans config/database.ts).

node ace workflow:migrate

14. Référence des routes API

Toutes les routes sont montées sous le préfixe configuré (défaut : /workflow).

Instances

| Méthode | Route | Description | |---|---|---| | POST | /instances | Démarrer un workflow — body: { entity_type, entity_id } | | GET | /instances/:id | Récupérer une instance par ID | | GET | /instances/by-entity | Instance par entité — query: entity_type, entity_id | | POST | /instances/:id/actions | Exécuter une action — body: { action, version, reason? } | | POST | /instances/:id/resubmit | Resoumettre après correction | | GET | /instances/:id/history | Journal des actions |

Définitions (workflow_admin)

| Méthode | Route | Description | |---|---|---| | GET | /definitions | Lister toutes les définitions | | POST | /definitions | Créer une définition | | GET | /definitions/by-type/:entityType | Définition active par entity_type | | GET | /definitions/:id | Détail d'une définition | | PUT | /definitions/:id | Mettre à jour | | DELETE | /definitions/:id | Supprimer | | PATCH | /definitions/:id/activate | Activer | | PATCH | /definitions/:id/deactivate | Désactiver | | GET/POST/PUT/DELETE | /definitions/:id/steps/... | CRUD des étapes | | GET/POST/PUT/DELETE | /definitions/:id/transitions/... | CRUD des transitions | | GET/POST/DELETE | /definitions/:id/steps/:stepId/validators/... | CRUD des validateurs | | GET/PUT | /definitions/:id/steps/:stepId/actions/... | Actions d'une étape |

Délégations

| Méthode | Route | Description | |---|---|---| | GET | /delegations | Mes délégations | | POST | /delegations | Créer une délégation | | DELETE | /delegations/:id | Révoquer |

Statistiques (workflow_manager)

| Méthode | Route | Description | |---|---|---| | GET | /stats/delays | Délais moyens par étape | | GET | /stats/rejection-rates | Taux de rejet par étape | | GET | /stats/volumes | Volumes par période | | GET | /stats/pending | Documents en attente |


15. Codes d'erreur

Toutes les erreurs métier sont des instances de WorkflowException avec un code string et un status HTTP.

| Code | Status | Description | |---|---|---| | ENTITY_NOT_FOUND | 404 | EntityResolver non enregistré ou entité inexistante | | NO_ACTIVE_DEFINITION | 404 | Pas de définition active pour cet entity_type | | INSTANCE_NOT_FOUND | 404 | Instance introuvable | | INSTANCE_ALREADY_ACTIVE | 409 | Instance déjà en cours pour cette entité | | NO_ACTIVE_STEP | 404 | Étape active introuvable | | ACTION_FORBIDDEN | 403 | Acteur non autorisé à agir sur cette étape | | ACTION_DISABLED | 422 | Action désactivée pour cette étape | | REASON_REQUIRED | 422 | Raison obligatoire pour cette action | | CONCURRENT_MODIFICATION | 409 | Conflit de version optimiste | | HOOK_REJECTED | 422 | ActionHook a bloqué l'action | | HOOK_EXCEPTION | 500 | Exception non gérée dans un hook | | INSUFFICIENT_ROLE | 403 | Rôle insuffisant | | UNKNOWN_CUSTOM_HANDLER | 500 | Handler custom_query non enregistré | | TRANSFER_MISSING_TARGET | 422 | delegate_id manquant pour l'action transfer | | EMPTY_DEFINITION | 422 | La définition ne contient aucune étape |


Développement et Exemple

Le package contient un exemple complet d'intégration dans examples/adonis-example.

1. Installation de l'exemple

Depuis la racine du package packages/adonis-workflow :

npm run example:install

2. Initialisation de la base de données (SQLite)

cd examples/adonis-example

# Création des tables (métier + workflow)
node ace migration:run

# Peuplement des données (utilisateurs, définitions, factures, instances)
node ace db:seed

3. Lancer le serveur de développement

Depuis la racine du package packages/adonis-workflow :

npm run example:dev

Le serveur sera accessible sur http://localhost:3333.