bpmnServer / bpmn-server

BPMN 2.0 server for Node.js , providing modeling, execution, persistence and monitoring for Workflow. along with sample UI. Intended to be developers workbench for BPMN 2.0
MIT License
186 stars 48 forks source link

Difficulty of multiple "user type" in real world application #190

Closed kstan79 closed 8 months ago

kstan79 commented 8 months ago

I have plenty of scenario look common in real world but quite challenging to implement viauser task, wish to find solution.

1 example as below: assume I have multiple group of users such as:

  1. cashier
  2. teacher
  3. students

I have multiple mongodb collection such as users (real user database who can login the application, cashers, teachers, and branch managers store in this collection) teachers (master list of teachers) student (master list of students) schedules (class schedule, it store student name list, the teacherid)

Assume when class date/time change we wish to notify teachers and students.

samplebpmn

schedule

refer to above bpmn and image, you can see that schedule record consist of teacher. However the teacher is master data, not real user. The only thing can match user and teacher is email, but we won't save email into schedule. So, during prepare bpmn we can't define correct user identity.

There is many scenario have similar case such as

  1. students
  2. parents
  3. salesagent they stored in different collection but consder human, play role in business processes. It is impossible to obtain suitable user info from data cause it too dynamic. Seems I need special user resolver as below strategy:
assignee: $system.usersolver('teacher', $data.teacher._id)

then I can create suitable methods outside to resolve the real user for that teacher, and send the notification/pusher message.

Any advise to implement this?

kstan79 commented 8 months ago

I figure out there is 'hack' way like:

@teacher[$data.teacher._id]
@student[$data.student._id]
@user[$data.createdBy]

seems bpmn-server won't override data with@, I can manually apply string replace or regex outside. However it seems not a long term solution. It is still better we have some official way which is endorse by you.

bpmn-ts commented 8 months ago

Please send your Bpmn file will investigate Sent from my iPhoneOn Mar 21, 2024, at 6:26 AM, Ks Tan @.***> wrote: I figure out there is 'hack' way like: @Techer[data.teacher._id] Then at bpmn won't override it, I'm manually apply string replace or regex outside. However it seems not a long term solution

—Reply to this email directly, view it on GitHub, or unsubscribe.You are receiving this because you are subscribed to this thread.Message ID: @.***>

kstan79 commented 8 months ago

Untitled

I'm not file a bug report, just wish to ask any "official" way to resolve user from the bpmn user task and that updated information can save into database permanently.

Base on above sample, i created my own way methods to convert teacher (@teacher[$data.teacher._id]). it willl run custom code to find user who have same email with the teacher, then I wish to save the teacher's userid into items. below i gave example which i can replace the run time assignee/candidateusers/groups but cant persist the value into mongodb

this.bpmnServer.listener.on(
      'all',
      async (event) => await this.applyEventListener(event),
    );

async applyEventListener(event){

.. ... many codes
//if is user task event under wait status
         const assignee = await this.userResolver(appuser,  event.context.item.assignee,data,);
          const candidateUsers = await this.userResolver( appuser, event.context.item.candidateUsers, data,);
          const candidateGroups = await this.userResolver( appuser,vent.context.item.candidateGroups,data,);
       //i cant override this 3 properties and let bpmnserver save into database
          event.context.item.assignee = assignee
          event.context.item.candidateUsers = candidateUsers
          event.context.item.candidateGroups =candidateGroups
       //send email notification
       //run others codes
}

// sample userids  = ['@teacher[$data.teacher._id]','@user[$data.createdBy], ]
  async userResolver( appuser: UserContext, userids: string | string[] | undefined, data: any,) {
    if (!userids) return undefined;
    if (typeof userids == 'string') userids = [userids];
    if (!Array.isArray(userids)) return undefined;
    const newids: string[] = [];
    for (let i = 0; i < userids.length; i++) {
      let uid = userids[i].trim();

      // require convert teacher/student to real user
      if (uid.substring(0, 1) == '@' && uid.includes('[') && uid.includes(']')) {
        const regextype = /(?<=\@)(.*?)(?=\[)/;    
        const regexvalue = /(?<=\[)(.*?)(?=\])/;     

        const usertype = uid.match(regextype)[0].trim();  //obtain teacher/user
        const typevalue = uid.match(regexvalue)[0].trim();  // obtain $data.teacher._id , $data.createdBy
        let idvalue = '';

          // read $data.teacher._id , $data.createdBy from "data"
        if (typevalue.substring(0, 6) == '$data.') {
          const fieldpath = typevalue.replace('$data.', '');
          idvalue = this.readFieldFromData(fieldpath, data);
        }
        //convert become real uid using external resolver (such as teacher/student execute different resolver)
        uid = await this.userResolverService.resolve(
          appuser,
          usertype,
          idvalue,
          data,
        );
      }

     //either convert become user's uuid, or remove it when the teacher cannot resolve
      if (uid) newids.push(uid); 
    }

    return newids;  //after return, wish to save into mongodb instance's usertask item
  }

readFieldFromData(path: string, data: any) {
    return path.split('.').reduce((o, i) => o[i], data);
  }

here is the xml

<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:camunda="http://camunda.org/schema/1.0/bpmn" id="Definitions_1" targetNamespace="http://bpmn.io/schema/bpmn">
  <bpmn:process id="errohandle" name="errors handler" isExecutable="false">
    <bpmn:startEvent id="StartEvent_1" name="update schedule">
      <bpmn:outgoing>Flow_1pg0fnn</bpmn:outgoing>
    </bpmn:startEvent>
    <bpmn:sequenceFlow id="Flow_1pg0fnn" sourceRef="StartEvent_1" targetRef="tryerror" />
    <bpmn:endEvent id="Event_1o6o4k2">
      <bpmn:incoming>Flow_170a071</bpmn:incoming>
    </bpmn:endEvent>
    <bpmn:sequenceFlow id="Flow_170a071" sourceRef="teacheracknowledge" targetRef="Event_1o6o4k2" />
    <bpmn:userTask id="teacheracknowledge" name="teacher acknowledge" camunda:assignee="@teacher[$data.teacher._id]">
      <bpmn:incoming>Flow_0i8h61u</bpmn:incoming>
      <bpmn:outgoing>Flow_170a071</bpmn:outgoing>
    </bpmn:userTask>
    <bpmn:sequenceFlow id="Flow_0i8h61u" sourceRef="tryerror" targetRef="teacheracknowledge" />
    <bpmn:serviceTask id="tryerror" name="try error">
      <bpmn:incoming>Flow_1pg0fnn</bpmn:incoming>
      <bpmn:outgoing>Flow_0i8h61u</bpmn:outgoing>
    </bpmn:serviceTask>
    <bpmn:boundaryEvent id="Event_1x43azn" attachedToRef="tryerror">
      <bpmn:outgoing>Flow_0ujgdj5</bpmn:outgoing>
      <bpmn:errorEventDefinition id="ErrorEventDefinition_1jljctq" />
    </bpmn:boundaryEvent>
    <bpmn:sequenceFlow id="Flow_0ujgdj5" sourceRef="Event_1x43azn" targetRef="errorhandler" />
    <bpmn:serviceTask id="errorhandler" name="error handler">
      <bpmn:incoming>Flow_0ujgdj5</bpmn:incoming>
      <bpmn:outgoing>Flow_0xn22s3</bpmn:outgoing>
    </bpmn:serviceTask>
    <bpmn:endEvent id="Event_1lr7684">
      <bpmn:incoming>Flow_0xn22s3</bpmn:incoming>
    </bpmn:endEvent>
    <bpmn:sequenceFlow id="Flow_0xn22s3" sourceRef="errorhandler" targetRef="Event_1lr7684" />
  </bpmn:process>
  <bpmndi:BPMNDiagram id="BPMNDiagram_1">
    <bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="errohandle">
      <bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
        <dc:Bounds x="122" y="102" width="36" height="36" />
        <bpmndi:BPMNLabel>
          <dc:Bounds x="100" y="145" width="81" height="14" />
        </bpmndi:BPMNLabel>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape id="Event_1o6o4k2_di" bpmnElement="Event_1o6o4k2">
        <dc:Bounds x="632" y="102" width="36" height="36" />
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape id="Activity_1vu7axv_di" bpmnElement="teacheracknowledge">
        <dc:Bounds x="400" y="80" width="100" height="80" />
        <bpmndi:BPMNLabel />
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape id="Activity_0vrmgin_di" bpmnElement="tryerror">
        <dc:Bounds x="200" y="80" width="100" height="80" />
        <bpmndi:BPMNLabel />
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape id="Activity_12q3140_di" bpmnElement="errorhandler">
        <dc:Bounds x="320" y="200" width="100" height="80" />
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape id="Event_1lr7684_di" bpmnElement="Event_1lr7684">
        <dc:Bounds x="472" y="222" width="36" height="36" />
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape id="Event_0nov24y_di" bpmnElement="Event_1x43azn">
        <dc:Bounds x="232" y="142" width="36" height="36" />
      </bpmndi:BPMNShape>
      <bpmndi:BPMNEdge id="Flow_1pg0fnn_di" bpmnElement="Flow_1pg0fnn">
        <di:waypoint x="158" y="120" />
        <di:waypoint x="200" y="120" />
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge id="Flow_170a071_di" bpmnElement="Flow_170a071">
        <di:waypoint x="500" y="120" />
        <di:waypoint x="632" y="120" />
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge id="Flow_0i8h61u_di" bpmnElement="Flow_0i8h61u">
        <di:waypoint x="300" y="120" />
        <di:waypoint x="400" y="120" />
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge id="Flow_0ujgdj5_di" bpmnElement="Flow_0ujgdj5">
        <di:waypoint x="250" y="178" />
        <di:waypoint x="250" y="240" />
        <di:waypoint x="320" y="240" />
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge id="Flow_0xn22s3_di" bpmnElement="Flow_0xn22s3">
        <di:waypoint x="420" y="240" />
        <di:waypoint x="472" y="240" />
      </bpmndi:BPMNEdge>
    </bpmndi:BPMNPlane>
  </bpmndi:BPMNDiagram>
</bpmn:definitions>
kstan79 commented 8 months ago

cause above code happen during doEvent start (or others which before save), so bpmn.engine.assign() seems is not a solution. Hope where is better interceptor or middleware available, either deligates or etc.

ralphhanna commented 8 months ago

I don't understand why is your start event handler able to calculate the assignment and set it directly:

item.assignee='user1';

call api from inside the event does not work Sorry about lack of documentation about this

kstan79 commented 8 months ago

[samples]

  1. there is an user name "John" with uid=xxxxx-yyyyyy-zzzz
  2. there is a teacher data name "Teacher John" with employee id : E001
  3. in schedule collection, it bind to teacher record E001

[workflow]

  1. there is many bpmn created, such as change schedule, new student enroll, student drops, ...
  2. no matter from which bpmn, as long as user task 'start', we want to convert E001 become xxxxx-yyyy-zzzz, then immdiately send pending approval notification (such as email) to respective user.
  3. When user "John" login to web app, it will auto query all UserTask with assignee or candidateUsers = xxxxx-yyyyyy-zzzz

I saw example #(services.getSupervisorUser(this.data.requester)) at below link, no luck too. ** I assume getSupervisorUser is the method under delegates.serviceProvider https://github.com/bpmnServer/bpmn-server/blob/0c001fc61518da9d4d92692acecd0f88800a909b/docs/userAssignment.md

kstan79 commented 8 months ago

I think i can conclude my observation as this:

  1. $(appDelegate.syncdata('sss')) => AppDelegate syncronize method ok, the value stored assignee at mongodb
  2. (appDelegate.getasyncdata('sss')) => AppDelegate async can't capture , the assignee = null

"$" working fine, and "#" doesn't.

since we need async call to read database, thats the reason i keep failing.

ralphhanna commented 8 months ago

sorry, you are having problems with this: as a quick work around you have two options:

  1. Use a previous Service Node to calculate the user info from mongoDB , store the infor in item.data.users={....}
  2. Or use an event listener at the appDelegate this can have an async call

I am on the road right now, but will investigate this further in couple of days Thanks

kstan79 commented 8 months ago

seems during evaluate at ScriptHandler have error, below is the console error:

error in script execution 
            var item=this;
            var data=this.data;
            var input=this.input;
            var output=this.output;
            var appDelegate=this.token.execution.appDelegate;
            var appServices=this.token.execution.servicesProvider;
            var appUtils=appDelegate.appUtils;

                  #(appDelegate.getasyncdata('sss'));
SyntaxError: Invalid or unexpected token

result = Function(js).bind(scope)(); have issue with #(appDelegate.getasyncdata('sss'));?

kstan79 commented 8 months ago

ok, seems it is bugs during process #(...), i'd send PR #191 to fix it.

Just, I have a few comment:

  1. it seems use kind of eval which create security vulnerability, no doubt it is high efficient but there is some drawback as I mentioned below. Future we may use more restricted but reliable evaluation instead. It also release from javascript pattern
  2. eval require long definition like object/variables: #(appDelegate.getUser('teacher',this.data.teacher._id)). We may have shorter alias in future:
    
    [$this.data.teacher._id]
    can shorten become $F{teacher._id} (mean $ for access data only, most of time developer use it)
    ** We can have multiple kind of prefix like 
    $F => Field in this.data, dynamic depends on data
    $R => static runtimes parameter, like server name, server time, instance properties $R{servertime}
    $P => extra input parameter we obtain from somewhere (in your case like something from `vars`,`inputs`)
    $V => static variables regarding this process/item such as process owner, how long it wait, when overdue, how many assignees and etc [$V{owner}, $V{expired}]

[#(appDelegate.getUser('teacher',this.data.teacher._id))] can shortern become @.getUser('teacher',$F{teacher._id}), in this case @=access appdeligate (or serviceProvider). I have additional feed back which is:

  1. no more declare it s async/sync cause bpmn server valuate is it promise instance
  2. it is much better standardize a rules either all go into deligates, or all go to serviceProvider. that will restrict the scope, easier for developer follow standard rules at the same time reduce complexity

[$this.instance.context...] Im not recommend to allow bpmn access dangerous runtime. BPMN is advisable run within isolated environment, the bpmn designer not interested in this scope too.



Hope above comment helpful
ralphhanna commented 8 months ago

Thanks for this feedback.

  1. Currently, no need to say this.data.var just `data.var'
  2. Yes there is No need for # instead of $ since we check out the results any, will modify the docs
  3. We should all go to appServices , no more appDelegate
ralphhanna commented 8 months ago

I have to point out that bpmn-server is not using eval but using Function() https://stackoverflow.com/questions/4599857/are-eval-and-new-function-the-same-thing

kstan79 commented 8 months ago

Thanks of clarification. I don't have computer to prove now but I would like to say don't create room for execute script at every places. Just allow it on script task. I worry user task able to execute some of below code

$(throw 'error') $(fetch(...send-out-private-data))

The field of bpmn like user task assigned shall only allow access to specific data, or method similar like "getter", give them room to insert js freely maybe harmful

ralphhanna commented 8 months ago

But keep in mind that is code must be in either the bpmn model or appServices and both cases is protected. In other words the end-user has no access to this code nor can modify it