Skip to content

Commit cea2299

Browse files
committed
feat: improve error handling and lifecycle management in Backends and LifecycleHooks services
- Enhanced error handling in BackendsService by utilizing a new function to format worker result error messages, improving clarity in BadRequestException responses. - Updated IdentitiesCrudController to retrieve the updated identity after lifecycle changes, ensuring accurate data representation. - Modified LifecycleHooksService to implement rollback logic on backend failures during lifecycle updates, enhancing reliability and consistency in identity state management. - Added a new method in the web UI to extract and display lifecycle error messages, improving user feedback during lifecycle transitions.
1 parent 5d948b3 commit cea2299

6 files changed

Lines changed: 130 additions & 16 deletions

File tree

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { WorkerResultInterface } from '../_interfaces/worker-result.interface';
2+
3+
export function formatWorkerResultErrorMessage(workerResult?: WorkerResultInterface): string {
4+
if (!workerResult?.data) {
5+
return 'Échec de synchronisation backend';
6+
}
7+
8+
const messages: string[] = [];
9+
for (const [backendName, result] of Object.entries(workerResult.data)) {
10+
const message = result?.error?.message || result?.output?.message;
11+
if (message) {
12+
messages.push(`${backendName}: ${message}`);
13+
continue;
14+
}
15+
16+
if (result?.status) {
17+
messages.push(`${backendName}: erreur (code ${result.status})`);
18+
}
19+
}
20+
21+
return messages.length ? messages.join(' · ') : 'Échec de synchronisation backend';
22+
}

apps/api/src/core/backends/backends.service.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { TasksService } from '../tasks/tasks.service';
2020
import { ActionType } from './_enum/action-type.enum';
2121
import { ExecuteJobOptions } from './_interfaces/execute-job-options.interface';
2222
import { WorkerResultInterface } from '~/core/backends/_interfaces/worker-result.interface';
23+
import { formatWorkerResultErrorMessage } from '~/core/backends/_functions/format-worker-result-error-message.function';
2324
import { DataStatusEnum } from '~/management/identities/_enums/data-status';
2425

2526
const DEFAULT_SYNC_TIMEOUT = 30_000;
@@ -672,10 +673,13 @@ export class BackendsService extends AbstractQueueProcessor {
672673
});
673674
}
674675

676+
const workerResult = (error as any).response as WorkerResultInterface | undefined;
677+
675678
throw new BadRequestException({
676679
status: HttpStatus.BAD_REQUEST,
680+
message: formatWorkerResultErrorMessage(workerResult),
677681
error,
678-
job: (error as any).response,
682+
job: workerResult,
679683
});
680684
}
681685

apps/api/src/management/identities/identities-crud.controller.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -369,7 +369,9 @@ export class IdentitiesCrudController extends AbstractController {
369369
}
370370
}
371371

372-
const data = await this._service.updateLifecycle(_id, body.lifecycle);
372+
await this._service.updateLifecycle(_id, body.lifecycle);
373+
const data = await this._service.findById<Identities>(_id);
374+
373375
return res.status(HttpStatus.OK).json({
374376
statusCode: HttpStatus.OK,
375377
data,

apps/api/src/management/lifecycle/lifecycle-hooks.service.ts

Lines changed: 60 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -419,7 +419,7 @@ export class LifecycleHooksService extends AbstractLifecycleService {
419419
* // Déclenché automatiquement lors de :
420420
* // await identitiesService.update(id, { lifecycle: 'MANUAL' })
421421
*/
422-
@OnEvent('management.identities.service.afterUpdate')
422+
@OnEvent('management.identities.service.afterUpdate', { suppressErrors: false })
423423
public async handle(event: { updated: Identities; before?: Identities }): Promise<void> {
424424
this.logger.verbose(`Handling identity update event for identity <${event.updated._id}>`);
425425

@@ -452,7 +452,7 @@ export class LifecycleHooksService extends AbstractLifecycleService {
452452
* // Déclenché automatiquement lors de :
453453
* // await identitiesService.upsert(filter, data)
454454
*/
455-
@OnEvent('management.identities.service.afterUpsert')
455+
@OnEvent('management.identities.service.afterUpsert', { suppressErrors: false })
456456
public async handleOrderCreatedEvent(event: { result: Identities; before?: Identities }): Promise<void> {
457457
this.logger.verbose(`Handling identity upsert event for identity <${event.result._id}>`);
458458

@@ -515,12 +515,35 @@ export class LifecycleHooksService extends AbstractLifecycleService {
515515
return before.lifecycle === after.lifecycle && before.state !== after.state;
516516
}
517517

518+
private async rollbackLifecycleOnBackendFailure(
519+
identityId: Types.ObjectId,
520+
lifecycle: string,
521+
lastLifecycleUpdate: Date | undefined,
522+
historyIds: Array<Types.ObjectId | string>,
523+
): Promise<void> {
524+
await this.identitiesService.model.findByIdAndUpdate(identityId, {
525+
$set: {
526+
lifecycle,
527+
...(lastLifecycleUpdate ? { lastLifecycleUpdate } : {}),
528+
},
529+
});
530+
531+
for (const historyId of historyIds) {
532+
try {
533+
await this.delete(historyId);
534+
} catch (error) {
535+
this.logger.warn(`Unable to delete lifecycle history <${historyId}>: ${error?.message}`);
536+
}
537+
}
538+
}
539+
518540
private async fireLifecycleEvent(
519541
before: Identities,
520542
after: Identities,
521543
options?: { force?: boolean },
522544
): Promise<void> {
523545
const lifecycleChanged = options?.force || (!!before && before.lifecycle !== after.lifecycle);
546+
const shouldRollbackLifecycle = lifecycleChanged && !options?.force && !!before;
524547

525548
if (!options?.force && this.isOperationalStateOnlyChange(before, after)) {
526549
this.logger.debug(
@@ -529,14 +552,15 @@ export class LifecycleHooksService extends AbstractLifecycleService {
529552
return;
530553
}
531554

555+
let initialHistoryId: Types.ObjectId | string | undefined;
532556
if (lifecycleChanged) {
533-
await this.create({
557+
const history = await this.create({
534558
refId: after._id,
535559
lifecycle: after.lifecycle,
536560
date: new Date(),
537561
});
562+
initialHistoryId = history?._id ? String(history._id) : undefined;
538563
this.logger.debug(`Lifecycle event manualy recorded for identity <${after._id}>: ${after.lifecycle}`);
539-
// If the lifecycle has changed, we need to process the new lifecycle
540564
}
541565

542566
if (this.lifecycleSources[after.lifecycle]) {
@@ -551,7 +575,7 @@ export class LifecycleHooksService extends AbstractLifecycleService {
551575

552576
if (lcs.trigger) {
553577
this.logger.debug(`Skipping lifecycle source <${after.lifecycle}> with trigger: ${lcs.trigger}`);
554-
continue; // Skip processing if it's a trigger-based rule
578+
continue;
555579
}
556580

557581
const hasMutation = hasLifecycleMutation(resolvedMutation);
@@ -570,8 +594,8 @@ export class LifecycleHooksService extends AbstractLifecycleService {
570594
},
571595
},
572596
{
573-
new: true, // Return the updated document
574-
upsert: false, // Do not create a new document if no match is found
597+
new: true,
598+
upsert: false,
575599
},
576600
);
577601

@@ -580,16 +604,27 @@ export class LifecycleHooksService extends AbstractLifecycleService {
580604
continue;
581605
}
582606

583-
await this.create({
607+
const ruleHistory = await this.create({
584608
refId: after._id,
585609
lifecycle: lcs.target,
586610
date: new Date(),
587611
});
588612

589613
const identities = res._id ? [{ id: res._id.toString(), before: after, after: res }] : [];
590-
await this.backendsService.lifecycleChangedIdentities(identities, {
591-
...(hasMutation ? { targetState: IdentityState.TO_SYNC } : {}),
592-
});
614+
615+
try {
616+
await this.backendsService.lifecycleChangedIdentities(identities, {
617+
...(hasMutation ? { targetState: IdentityState.TO_SYNC } : {}),
618+
});
619+
} catch (error) {
620+
await this.rollbackLifecycleOnBackendFailure(
621+
after._id,
622+
after.lifecycle,
623+
after.lastLifecycleUpdate,
624+
[ruleHistory?._id ? String(ruleHistory._id) : undefined].filter(Boolean) as Array<Types.ObjectId | string>,
625+
);
626+
throw error;
627+
}
593628

594629
this.logger.log(
595630
hasMutation
@@ -602,7 +637,20 @@ export class LifecycleHooksService extends AbstractLifecycleService {
602637

603638
if (lifecycleChanged) {
604639
const identities = after._id ? [{ id: after._id.toString(), before, after }] : [];
605-
await this.backendsService.lifecycleChangedIdentities(identities);
640+
641+
try {
642+
await this.backendsService.lifecycleChangedIdentities(identities);
643+
} catch (error) {
644+
if (shouldRollbackLifecycle) {
645+
await this.rollbackLifecycleOnBackendFailure(
646+
after._id,
647+
before.lifecycle,
648+
before.lastLifecycleUpdate,
649+
[initialHistoryId].filter(Boolean) as Array<Types.ObjectId | string>,
650+
);
651+
}
652+
throw error;
653+
}
606654
}
607655
}
608656
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { formatWorkerResultErrorMessage } from '~/core/backends/_functions/format-worker-result-error-message.function';
2+
3+
describe('formatWorkerResultErrorMessage', () => {
4+
it('should format backend error messages', () => {
5+
const message = formatWorkerResultErrorMessage({
6+
jobId: '1',
7+
status: 1,
8+
data: {
9+
dummy: {
10+
backend: 'dummy',
11+
status: 1,
12+
error: { status: 1, message: 'Transition P vers A refusée par dummy2' },
13+
},
14+
},
15+
});
16+
17+
expect(message).toBe('dummy: Transition P vers A refusée par dummy2');
18+
});
19+
});

apps/web/src/pages/identities/table/[_id]/index.vue

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,24 @@ export default defineNuxtComponent({
326326
}
327327
return this.isManualLifecycleTargetAllowed(this.identity.lifecycle, targetLifecycle)
328328
},
329+
extractLifecycleErrorMessage(error: any): string {
330+
const data = error?.response?._data
331+
if (typeof data?.message === 'string' && data.message.trim()) {
332+
return data.message
333+
}
334+
335+
const job = data?.job
336+
if (job?.data) {
337+
for (const result of Object.values(job.data) as Array<{ error?: { message?: string }; output?: { message?: string } }>) {
338+
const message = result?.error?.message || result?.output?.message
339+
if (message) {
340+
return message
341+
}
342+
}
343+
}
344+
345+
return data?.error?.message || error?.message || 'erreur inconnue'
346+
},
329347
async switchLifecycle(lifecycle: string) {
330348
if (!this.isManualLifecycleChangeAllowed(lifecycle)) {
331349
this.$q.notify({
@@ -345,14 +363,15 @@ export default defineNuxtComponent({
345363
position: 'top-right',
346364
icon: 'mdi-check-circle-outline',
347365
})
348-
;(this as any).refresh()
349366
} catch (error: any) {
350367
this.$q.notify({
351-
message: 'Impossible de modifier le cycle de vie : ' + error.response._data.message,
368+
message: 'Impossible de modifier le cycle de vie : ' + this.extractLifecycleErrorMessage(error),
352369
color: 'negative',
353370
position: 'top-right',
354371
icon: 'mdi-alert-circle-outline',
355372
})
373+
} finally {
374+
;(this as any).refresh()
356375
}
357376
},
358377
async forceLifecycleExecution() {

0 commit comments

Comments
 (0)