npm package: @qualsoft/workflow-core
Description: Core workflow engine and type definitions used by Qualsoft workflow-enabled applications. This package provides the workflow engine, state store interface, and helper utilities for driving step-based workflows.
Latest version: 0.1.18

# @qualsoft/workflow-core

Core workflow engine and type definitions used by Qualsoft workflow-enabled applications. This package provides the workflow engine, state store interface, and helper utilities for driving step-based workflows.

## Installation

```bash
npm install @qualsoft/workflow-core
```

## Vue d'ensemble de l'interface

### Exports

```ts
import {
  WorkflowEngine,
  InMemoryActionRegistry,
  resolveSteps,
  findAction,
  findStep,
  type WorkflowStateStore,
  type StepWorkflowDefinition,
  type WorkflowRecord,
  type WorkflowContext,
  type WorkflowRunInput,
  type WorkflowRunResult
} from '@qualsoft/workflow-core';
```

### Contrat de persistance (state store)

Implémentez `WorkflowStateStore` pour connecter le moteur à votre couche de stockage :

```ts
const stateStore: WorkflowStateStore = {
  async getWorkflowDefinition(formId, workflowId) {
    // Load definition
  },
  async getRecord(formId, recordId) {
    // Load record
  },
  async updateRecord(formId, recordId, payload) {
    // Persist updated record fields
  }
};
```

### Exécuter une action de workflow

```ts
const engine = new WorkflowEngine({
  stateStore,
  actionRegistry: new InMemoryActionRegistry()
});

const result = await engine.run({
  formId: 'forms-123',
  workflowId: 'workflow-abc',
  recordId: 'record-789',
  actionId: 'approve-action',
  context: {
    user: { email: 'approver@example.com' },
    timestamp: new Date().toISOString()
  }
});

console.log(result.updatedRecord.statut);
```

### Action runners à l'entrée d'une étape

Les steps de workflow peuvent définir des `actionRunners` exécutés à l'entrée d'une étape ou après un délai. Chaque runner précise un `type` (ex: `assignUser`, `assignRole`, `updateRecordFields`, `notifyUser`, `slaTimer`) et un déclencheur :

```ts
steps: [
  {
    id: 'review',
    label: 'Revue',
    actor: 'role',
    role: 'quality',
    fields: [],
    actions: [],
    actionRunners: [
      {
        id: 'review-notify',
        label: 'Notifier le responsable',
        type: 'notifyUser',
        trigger: {
          type: 'afterDelay',
          delay: { value: 2, unit: 'days', source: 'stepEntry' }
        },
        payload: {
          subject: 'Relance revue',
          message: 'Une revue est en attente.'
        }
      }
    ]
  }
]
```

Le moteur exécute les runners immédiats via le registre d'actions et planifie les runners différés via l'action `scheduleTask` (si enregistrée).

## Registre d'actions unitaires

Le moteur s'appuie sur un registre d'actions qui associe un type d'action à une fonction unique. Chaque action est enregistrée séparément (ex: `sendEmail`) et tout type non enregistré est ignoré (avec un avertissement).

### Exemple d'enregistrement

```ts
const registry = new InMemoryActionRegistry();

registry.register({
  type: 'sendEmail',
  async run(payload, context) {
    console.log('payload', payload, 'context', context);
  }
});
```

## Structures de données

### Entrée principale (`WorkflowRunInput`)

La méthode `WorkflowEngine.run` attend :

```ts
type WorkflowRunInput = {
  formId: string;
  workflowId: string;
  recordId: string;
  actionId: string;
  context?: {
    formId: string;
    recordId: string;
    workflowId: string;
    user?: {
      id?: string;
      email?: string;
      displayName?: string;
      roles?: string[];
    };
    timestamp?: string;
    actions?: Array<{ type: string; payload: unknown }>;
    [key: string]: unknown;
  };
};
```

### Sortie (`WorkflowRunResult`)

Le résultat contient l'enregistrement mis à jour et la définition de workflow résolue :

```ts
type WorkflowRunResult = {
  updatedRecord: WorkflowRecord;
  definition: StepWorkflowDefinition;
};
```

### Définition d'un workflow (`StepWorkflowDefinition`)

Une définition moderne s'appuie sur `steps`. Elle peut aussi exposer `states/transitions` (format legacy) et sera convertie automatiquement via `resolveSteps`.

```ts
type StepWorkflowDefinition = {
  id: string;
  name?: string;
  initialState?: string;
  steps?: WorkflowStep[];
  states?: Array<{ id: string; label: string; isFinal?: boolean }>;
  transitions?: Array<{ from: string; to: string; actor: WorkflowActor; role?: string }>;
};
```

### Enregistrement (`WorkflowRecord`)

```ts
type WorkflowRecord = {
  id?: string;
  statut?: string;
  customFields?: Record<string, unknown>;
  workflowHistory?: WorkflowStepHistoryEntry[];
  [key: string]: unknown;
};
```

## Exemple complet

```ts
import {
  WorkflowEngine,
  InMemoryActionRegistry,
  type WorkflowStateStore,
  type StepWorkflowDefinition,
  type WorkflowRecord
} from '@qualsoft/workflow-core';

const definition: StepWorkflowDefinition = {
  id: 'workflow-qa',
  name: 'Validation QA',
  initialState: 'draft',
  steps: [
    {
      id: 'draft',
      label: 'Brouillon',
      actor: 'creator',
      fields: [],
      actions: [
        {
          id: 'submit',
          label: 'Soumettre',
          defaultNextStepId: 'review',
          conditionalRoutes: []
        }
      ]
    },
    {
      id: 'review',
      label: 'Revue',
      actor: 'role',
      role: 'quality',
      fields: [],
      actions: [
        {
          id: 'approve',
          label: 'Approuver',
          defaultNextStepId: 'done',
          conditionalRoutes: []
        }
      ]
    },
    {
      id: 'done',
      label: 'Terminé',
      actor: 'none',
      isFinal: true,
      fields: [],
      actions: []
    }
  ]
};

const record: WorkflowRecord = {
  id: 'record-42',
  statut: 'draft',
  workflowHistory: []
};

const stateStore: WorkflowStateStore = {
  async getWorkflowDefinition() {
    return definition;
  },
  async getRecord() {
    return record;
  },
  async updateRecord(_formId, _recordId, payload) {
    Object.assign(record, payload);
  }
};

const actionRegistry = new InMemoryActionRegistry();

const engine = new WorkflowEngine({
  stateStore,
  actionRegistry
});

const result = await engine.run({
  formId: 'form-1',
  workflowId: 'workflow-qa',
  recordId: 'record-42',
  actionId: 'submit',
  context: {
    formId: 'form-1',
    workflowId: 'workflow-qa',
    recordId: 'record-42',
    user: { id: 'user-1', email: 'creator@example.com' }
  }
});

console.log(result.updatedRecord.statut); // => "review"
```

## Migration strategy: `states`/`transitions` → `steps`

Workflow definitions can still expose legacy `states`/`transitions`. Use `resolveSteps` to centralize conversion to `steps` and keep the rest of the application working with the modern structure:

```ts
const steps = resolveSteps(definition);
```

`resolveSteps` converts transitions into step actions and logs a clear error when transitions reference missing states (or when transitions are provided without any states). Use these logs to identify definitions that need to be updated. The recommended migration path is:

1. Load definitions and run them through `resolveSteps`.
2. Fix any logged inconsistencies in legacy `states`/`transitions`.
3. Persist definitions back with `steps` populated so future reads can skip conversion.

## Build

```bash
npm run build
```

The compiled JavaScript and type declarations are output to `lib/`.

## Publish

```bash
npm version patch && npm publish --access public
```

> The `prepare` script runs the build automatically during publish.