@fickou/adonis-workflow
v1.0.5
Published
Generic workflow engine for AdonisJS 5 + Lucid ORM
Maintainers
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
- Installation
- Migrations
- Enregistrement du provider
- Configuration
- Implémenter l'EntityResolver
- Utiliser le WorkflowEngine
- ActionHooks — logique métier transactionnelle
- Écouter les événements
- RBAC — rôles et permissions
- Délégations
- RuleEngineService — conditions dynamiques
- WebSocket temps réel
- Commandes Ace
- Référence des routes API
- Codes d'erreur
1. Installation
npm install @fickou/adonis-workflowVé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.ts — pas 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:runLes migrations sont numérotées
001→011. 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 workflowConfig5. 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_typen'est pas enregistré,WorkflowEnginelèveWorkflowException('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:seedsi 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:seedworkflow: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>&1workflow:migrate
Affiche le chemin des migrations du package (à référencer dans config/database.ts).
node ace workflow:migrate14. 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:install2. 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:seed3. Lancer le serveur de développement
Depuis la racine du package packages/adonis-workflow :
npm run example:devLe serveur sera accessible sur http://localhost:3333.
