You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@archiva.apache.org by ma...@apache.org on 2020/12/29 00:11:49 UTC

[archiva] 01/02: Improving error handling

This is an automated email from the ASF dual-hosted git repository.

martin_s pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/archiva.git

commit cb0e4d19d78956b536c62772ae042c281d07ad9f
Author: Martin Stockhammer <ma...@apache.org>
AuthorDate: Thu Dec 24 10:37:55 2020 +0100

    Improving error handling
---
 .../archiva-web/src/app/model/app-notification.ts  |  3 +-
 .../manage-roles-edit.component.ts                 | 35 +++++++++------
 .../manage-users-add.component.html                |  3 --
 .../manage-users-roles.component.ts                |  8 ++--
 .../paginated-entities.component.ts                | 27 +++---------
 .../app/modules/shared/toast/toast.component.ts    |  2 +-
 .../src/app/modules/shared/with-loading.pipe.ts    |  8 ++--
 .../archiva-web/src/app/services/role.service.ts   | 50 +++++++++++++++++-----
 .../archiva-web/src/app/services/toast.service.ts  | 42 +++++++++++++++---
 .../src/main/archiva-web/src/assets/i18n/en.json   | 12 +++++-
 10 files changed, 127 insertions(+), 63 deletions(-)

diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/model/app-notification.ts b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/model/app-notification.ts
index 787762b..ae1b0a4 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/model/app-notification.ts
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/model/app-notification.ts
@@ -33,7 +33,6 @@ export class AppNotification {
         this.header = header;
         this.body = body;
         this.timestamp = timestamp;
-        console.log("Options " + JSON.stringify(options));
         if (options.classname) {
             this.classname = options.classname;
         }
@@ -49,7 +48,7 @@ export class AppNotification {
     }
 
     public toString(): string {
-        return this.origin + ',classname:' + this.classname + ", delay:" + this.delay +", context: "+JSON.stringify(this.contextData);
+        return this.origin + ',classname:' + this.classname + ", delay:" + this.delay ;
     }
 
 }
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/roles/manage-roles-edit/manage-roles-edit.component.ts b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/roles/manage-roles-edit/manage-roles-edit.component.ts
index 321557b..7654ee9 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/roles/manage-roles-edit/manage-roles-edit.component.ts
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/roles/manage-roles-edit/manage-roles-edit.component.ts
@@ -16,16 +16,7 @@
  * under the License.
  */
 
-import {
-    AfterContentInit,
-    ChangeDetectorRef,
-    Component,
-    EventEmitter,
-    OnInit,
-    Output,
-    TemplateRef,
-    ViewChild
-} from '@angular/core';
+import {AfterContentInit, Component, EventEmitter, OnInit, Output, ViewChild} from '@angular/core';
 import {ActivatedRoute} from "@angular/router";
 import {FormBuilder, Validators} from "@angular/forms";
 import {RoleService} from "@app/services/role.service";
@@ -42,6 +33,7 @@ import {UserService} from "@app/services/user.service";
 import {UserInfo} from '@app/model/user-info';
 import {HttpResponse} from "@angular/common/http";
 import {PaginatedEntitiesComponent} from "@app/modules/shared/paginated-entities/paginated-entities.component";
+import {ToastService} from "@app/services/toast.service";
 
 @Component({
     selector: 'app-manage-roles-edit',
@@ -73,7 +65,7 @@ export class ManageRolesEditComponent extends EditBaseComponent<Role> implements
     roleIdEvent: EventEmitter<string> = new EventEmitter<string>(true);
 
     constructor(private route: ActivatedRoute, public roleService: RoleService, private userService: UserService,
-                public fb: FormBuilder, private changeDetect : ChangeDetectorRef) {
+                public fb: FormBuilder, private toastService: ToastService) {
         super(fb);
         super.init(fb.group({
             id: [''],
@@ -151,7 +143,9 @@ export class ManageRolesEditComponent extends EditBaseComponent<Role> implements
                 } else {
                     return this.roleService.getRole(myId).pipe(tap(role => {
                         this.roleCache.set(role.id, role);
-                    }),catchError(() => of(this.createRole(id))));
+                    }),catchError((error : ErrorResult) => {
+                        this.showError(error, "roles.edit.errors.retrieveFailed")
+                        return of(this.createRole(id)); }));
                 }
             }));
     }
@@ -192,6 +186,7 @@ export class ManageRolesEditComponent extends EditBaseComponent<Role> implements
                 this.error = true;
                 this.success = false;
                 this.errorResult = err;
+                this.showError(err, 'roles.edit.errors.updateFailed',{'role_id':role.id})
                 return [];
             })
         ).subscribe(roleInfo => {
@@ -199,6 +194,7 @@ export class ManageRolesEditComponent extends EditBaseComponent<Role> implements
             this.success = true;
             this.errorResult = null;
             this.result = roleInfo;
+            this.showSuccess('roles.edit.success.updated',{'role_id':role.id})
             this.editMode = false;
         });
 
@@ -233,6 +229,17 @@ export class ManageRolesEditComponent extends EditBaseComponent<Role> implements
         return item.user_id;
     }
 
+    showError(err: ErrorResult, errorKey:string, params:any={}) : void {
+        let message = err.error_messages.length>0?err.error_messages[0]:''
+        params['message']=message
+        this.toastService.showErrorByKey('manage-roles-edit',errorKey,params)
+    }
+
+    showSuccess(successKey:string, params:any={}) : void  {
+        console.log("Success " + successKey + " - " + JSON.stringify(params));
+        this.toastService.showSuccessByKey('manage-roles-edit',successKey,params)
+    }
+
     assignUserRole() {
         let userId;
         if (typeof(this.userSearchModel)=='string') {
@@ -248,6 +255,7 @@ export class ManageRolesEditComponent extends EditBaseComponent<Role> implements
                     this.error = true;
                     this.success = false;
                     this.errorResult = err;
+                    this.showError(err, 'roles.edit.errors.assignFailed', {'role_id':this.editRole.id,'user_id':userId})
                     return [];
                 })
             ).subscribe((response : HttpResponse<Role>)  => {
@@ -256,6 +264,7 @@ export class ManageRolesEditComponent extends EditBaseComponent<Role> implements
                 this.errorResult = null;
                 this.result = response.body;
                 this.roleUserComponent.changePage(1);
+                this.showSuccess('roles.edit.success.assign',{'role_id':this.editRole.id,'user_id':userId})
                 this.userSearchModel=''
             });
         }
@@ -269,6 +278,7 @@ export class ManageRolesEditComponent extends EditBaseComponent<Role> implements
                     this.error = true;
                     this.success = false;
                     this.errorResult = err;
+                    this.showError(err, 'roles.edit.errors.unassignFailed',{'role_id':this.editRole.id,'user_id':user_id})
                     return [];
                 })
             ).subscribe((response : HttpResponse<Role>)  => {
@@ -278,6 +288,7 @@ export class ManageRolesEditComponent extends EditBaseComponent<Role> implements
                     this.errorResult = null;
                     this.result = response.body;
                     this.roleUserComponent.changePage(1);
+                    this.showSuccess('roles.edit.success.unassign',{'role_id':this.editRole.id,'user_id':user_id})
                 }
             );
         }
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/users/manage-users-add/manage-users-add.component.html b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/users/manage-users-add/manage-users-add.component.html
index 915c812..c04ee50 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/users/manage-users-add/manage-users-add.component.html
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/users/manage-users-add/manage-users-add.component.html
@@ -90,9 +90,6 @@
         <button class="btn btn-primary" type="submit"
                 [attr.disabled]="userForm.valid?null:true">{{'users.add.submit'|translate}}</button>
     </div>
-    <div class="form-group col-md-8">
-        <button class="btn btn-primary" (click)="showMessage()">Show Message</button>
-    </div>
 
     <ng-template #successTmpl let-userId="user_id">
         User <a [routerLink]="['/security','users','edit',userId]">{{userId}}</a> was added to the list.
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/users/manage-users-roles/manage-users-roles.component.ts b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/users/manage-users-roles/manage-users-roles.component.ts
index 1beb1f0..9608bba 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/users/manage-users-roles/manage-users-roles.component.ts
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/users/manage-users-roles/manage-users-roles.component.ts
@@ -17,16 +17,16 @@
  */
 
 import {AfterViewInit, Component, EventEmitter, OnInit, Output} from '@angular/core';
-import { Role } from '@app/model/role';
+import {Role} from '@app/model/role';
 import {UserService} from "@app/services/user.service";
 import {ActivatedRoute} from "@angular/router";
-import {catchError, filter, map, multicast, share, switchMap, tap} from "rxjs/operators";
+import {catchError, filter, map, share, switchMap, tap} from "rxjs/operators";
 import {RoleTree} from "@app/model/role-tree";
 import {RoleService} from "@app/services/role.service";
 import {RoleTemplate} from "@app/model/role-template";
-import {Observable, of} from "rxjs";
+import {Observable} from "rxjs";
 import {Util} from "@app/modules/shared/shared.module";
-import { RoleResult } from './role-result';
+import {RoleResult} from './role-result';
 import {fromArray} from "rxjs/internal/observable/fromArray";
 import {ErrorResult} from "@app/model/error-result";
 import {HttpResponse} from "@angular/common/http";
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/paginated-entities/paginated-entities.component.ts b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/paginated-entities/paginated-entities.component.ts
index 8e2e166..f7d2188 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/paginated-entities/paginated-entities.component.ts
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/paginated-entities/paginated-entities.component.ts
@@ -16,26 +16,13 @@
  * under the License.
  */
 
-import {AfterViewInit, ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output} from '@angular/core';
-import {concat, merge, Observable, of, pipe, Subject} from "rxjs";
-import {
-    concatAll,
-    debounceTime,
-    delay,
-    distinctUntilChanged,
-    filter,
-    map,
-    mergeMap,
-    pluck,
-    share,
-    startWith,
-    switchMap,
-    tap
-} from "rxjs/operators";
-import {EntityService} from "../../../model/entity-service";
-import {FieldToggle} from "../../../model/field-toggle";
-import {PageQuery} from "@app/modules/shared/model/page-query";
-import { LoadingValue } from '../shared.module';
+import {AfterViewInit, Component, EventEmitter, Input, OnInit, Output} from '@angular/core';
+import {concat, merge, Observable, of, Subject} from "rxjs";
+import {debounceTime, distinctUntilChanged, filter, map, pluck, startWith, switchMap} from "rxjs/operators";
+import {EntityService} from "@app/model/entity-service";
+import {FieldToggle} from "@app/model/field-toggle";
+import {PageQuery} from "../model/page-query";
+import {LoadingValue} from '../model/loading-value';
 import {PagedResult} from "@app/model/paged-result";
 
 
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/toast/toast.component.ts b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/toast/toast.component.ts
index 0813f6c..ff6a033 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/toast/toast.component.ts
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/toast/toast.component.ts
@@ -41,7 +41,7 @@ import {AppNotification} from "@app/model/app-notification";
       <ng-template #text>{{ toast.body }}</ng-template>
     </ngb-toast>
   `,
-  styles: [".ngb-toasts{margin:.5em;padding:0.5em;position:fixed;right:2px;top:2px;z-index:1200}"
+  styles: [".ngb-toasts{margin:.5em;padding:0.5em;position:fixed;right:2px;top:20px;z-index:1200}"
   ],
   host: {'[class.ngb-toasts]': 'true'}
 })
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/with-loading.pipe.ts b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/with-loading.pipe.ts
index 8de45ed..dbb9cd0 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/with-loading.pipe.ts
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/with-loading.pipe.ts
@@ -16,10 +16,10 @@
  * under the License.
  */
 
-import { Pipe, PipeTransform } from '@angular/core';
-import {concat, isObservable, of } from 'rxjs';
-import {catchError, map, startWith, tap } from 'rxjs/operators';
-import {LoadingValue} from "@app/modules/shared/shared.module";
+import {Pipe, PipeTransform} from '@angular/core';
+import {isObservable, of} from 'rxjs';
+import {catchError, map, startWith} from 'rxjs/operators';
+import {LoadingValue} from "@app/modules/shared/model/loading-value";
 
 @Pipe({
   name: 'withLoading'
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/services/role.service.ts b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/services/role.service.ts
index e5b7e39..5886c8a 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/services/role.service.ts
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/services/role.service.ts
@@ -19,13 +19,14 @@
 import { Injectable } from '@angular/core';
 import {ArchivaRequestService} from "@app/services/archiva-request.service";
 import {RoleTemplate} from "@app/model/role-template";
-import { Observable } from 'rxjs';
+import {Observable, throwError} from 'rxjs';
 import { Role } from '@app/model/role';
-import {HttpResponse} from "@angular/common/http";
+import {HttpErrorResponse, HttpResponse} from "@angular/common/http";
 import {PagedResult} from "@app/model/paged-result";
 import {UserInfo} from "@app/model/user-info";
 import {RoleUpdate} from "@app/model/role-update";
 import { User } from '@app/model/user';
+import {catchError} from "rxjs/operators";
 
 @Injectable({
   providedIn: 'root'
@@ -35,15 +36,24 @@ export class RoleService {
   constructor(private rest: ArchivaRequestService) { }
 
   public getTemplates() : Observable<RoleTemplate[]> {
-    return this.rest.executeRestCall("get", "redback", "roles/templates", null);
+    return this.rest.executeRestCall<RoleTemplate[]>("get", "redback", "roles/templates", null).pipe(
+        catchError((error: HttpErrorResponse) => {
+          return throwError(this.rest.getTranslatedErrorResult(error));
+        }));
   }
 
   public assignRole(roleId, userId) : Observable<HttpResponse<Role>> {
-    return this.rest.executeResponseCall<Role>("put", "redback", "roles/" + roleId + "/user/" + userId, null);
+    return this.rest.executeResponseCall<Role>("put", "redback", "roles/" + roleId + "/user/" + userId, null).pipe(
+        catchError((error: HttpErrorResponse) => {
+          return throwError(this.rest.getTranslatedErrorResult(error));
+        }));
   }
 
   public unAssignRole(roleId, userId) : Observable<HttpResponse<Role>> {
-    return this.rest.executeResponseCall<Role>("delete", "redback", "roles/" + roleId + "/user/" + userId, null);
+    return this.rest.executeResponseCall<Role>("delete", "redback", "roles/" + roleId + "/user/" + userId, null).pipe(
+        catchError((error: HttpErrorResponse) => {
+          return throwError(this.rest.getTranslatedErrorResult(error));
+        }));
   }
 
   public query(searchTerm: string, offset: number = 0, limit: number = 10, orderBy: string[] = ['id'], order: string = 'asc'): Observable<PagedResult<Role>> {
@@ -59,7 +69,10 @@ export class RoleService {
       'limit': limit,
       'orderBy': orderBy,
       'order': order
-    });
+    }).pipe(
+        catchError((error: HttpErrorResponse) => {
+          return throwError(this.rest.getTranslatedErrorResult(error));
+        }));
   }
 
   public queryAssignedUsers(roleId: string,
@@ -77,7 +90,10 @@ export class RoleService {
       'limit': limit,
       'orderBy': orderBy,
       'order': order
-    });
+    }).pipe(
+        catchError((error: HttpErrorResponse) => {
+          return throwError(this.rest.getTranslatedErrorResult(error));
+        }));
   }
 
   /**
@@ -108,7 +124,10 @@ export class RoleService {
       'limit': limit,
       'orderBy': orderBy,
       'order': order
-    });
+    }).pipe(
+        catchError((error: HttpErrorResponse) => {
+          return throwError(this.rest.getTranslatedErrorResult(error));
+        }));
   }
 
   public queryUnAssignedUsers(roleId: string,
@@ -126,16 +145,25 @@ export class RoleService {
       'limit': limit,
       'orderBy': orderBy,
       'order': order
-    });
+    }).pipe(
+        catchError((error: HttpErrorResponse) => {
+          return throwError(this.rest.getTranslatedErrorResult(error));
+        }));
   }
 
 
   public getRole(roleId:string) : Observable<Role> {
-    return this.rest.executeRestCall("get", "redback", "roles/" + roleId, null);
+    return this.rest.executeRestCall<Role>("get", "redback", "roles/" + roleId, null).pipe(
+        catchError((error: HttpErrorResponse) => {
+          return throwError(this.rest.getTranslatedErrorResult(error));
+        }));
   }
 
   public updateRole(role:RoleUpdate) : Observable<Role> {
-    return this.rest.executeRestCall("patch", "redback", "roles/" + role.id, role);
+    return this.rest.executeRestCall<Role>("patch", "redback", "roles/" + role.id, role).pipe(
+        catchError((error: HttpErrorResponse) => {
+          return throwError(this.rest.getTranslatedErrorResult(error));
+        }));
   }
 
 }
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/services/toast.service.ts b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/services/toast.service.ts
index b168a0f..1b1a934 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/services/toast.service.ts
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/services/toast.service.ts
@@ -19,6 +19,7 @@
 import { Injectable, TemplateRef } from '@angular/core';
 import {AppNotification} from "@app/model/app-notification";
 import {not} from "rxjs/internal-compatibility";
+import {TranslateService} from "@ngx-translate/core";
 
 @Injectable({
   providedIn: 'root'
@@ -30,9 +31,9 @@ export class ToastService {
   toasts:AppNotification[]=[]
   toastHistory:AppNotification[]=[]
 
-  constructor() { }
+  constructor(private translator: TranslateService) { }
 
-  show(origin:string, textOrTpl: string | TemplateRef<any>, options: any = {}) {
+  public show(origin:string, textOrTpl: string | TemplateRef<any>, options: any = {}): void {
     let notification = new AppNotification(origin, textOrTpl, "", options);
     this.toasts.push(notification);
     this.toastHistory.push(notification);
@@ -45,7 +46,7 @@ export class ToastService {
     console.log("Notification " + notification);
   }
 
-  showStandard(origin:string,textOrTpl:string|TemplateRef<any>, options:any={}) {
+  public showStandard(origin:string,textOrTpl:string|TemplateRef<any>, options:any={}) : void {
     options.classname='bg-primary'
     if (!options.delay) {
       options.delay=8000
@@ -53,7 +54,17 @@ export class ToastService {
     this.show(origin,textOrTpl,options)
   }
 
-  showError(origin:string,textOrTpl:string|TemplateRef<any>, options:any={}) {
+  public showStandardByKey(origin:string,translationKey:string, params:any={}, options:any={} ) : void {
+    let message:string;
+    if (params) {
+      message = this.translator.instant(translationKey, params);
+    } else {
+      message = this.translator.instant(translationKey);
+    }
+    this.showStandard(origin, message, options);
+  }
+
+  public showError(origin:string,textOrTpl:string|TemplateRef<any>, options:any={}) : void {
     options.classname='bg-warning'
     options.type='error'
     if (!options.delay) {
@@ -62,7 +73,17 @@ export class ToastService {
     this.show(origin,textOrTpl,options)
   }
 
-  showSuccess(origin:string,textOrTpl:string|TemplateRef<any>, options:any={}) {
+  public showErrorByKey(origin:string,translationKey:string, params:any={}, options:any={} ) : void {
+    let message:string;
+    if (params) {
+      message = this.translator.instant(translationKey, params);
+    } else {
+      message = this.translator.instant(translationKey);
+    }
+    this.showError(origin, message, options);
+  }
+
+  public showSuccess(origin:string,textOrTpl:string|TemplateRef<any>, options:any={}) : void {
     options.classname='bg-info'
     options.type='success'
     if (!options.delay) {
@@ -71,6 +92,17 @@ export class ToastService {
     this.show(origin,textOrTpl,options)
   }
 
+  public showSuccessByKey(origin:string,translationKey:string, params:any={}, options:any={} ) : void {
+    let message:string;
+    if (params) {
+      message = this.translator.instant(translationKey, params);
+    } else {
+      message = this.translator.instant(translationKey);
+    }
+    this.showSuccess(origin, message, options);
+  }
+
+
   remove(toast) {
     this.toasts = this.toasts.filter(t => t != toast);
   }
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/assets/i18n/en.json b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/assets/i18n/en.json
index 0be67d3..e39cf21 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/assets/i18n/en.json
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/assets/i18n/en.json
@@ -143,7 +143,17 @@
       "noUsersAssigned": "There are no users assigned to this role",
       "assignUserSearch": "Search and assign users to this role",
       "userSearchFailed": "Sorry, could not load users",
-      "assignButton": "Assign"
+      "assignButton": "Assign",
+      "errors": {
+        "updateFailed": "Could not update role {role_id}: {message}",
+        "assignFailed": "Could not assign role {role_id} to user {user_id}: {message}",
+        "retrieveFailed": "Could not retrieve role information: {message}"
+      },
+      "success": {
+        "updated": "Role {role_id} was updated",
+        "assign": "Role {role_id} was assigned to user {user_id}",
+        "unassign": "Removed assignment of {role_id} to {user_id}"
+      }
     },
     "attributes": {
       "id": "Identifier",