@things-factory/worklist 10.1.202 → 10.1.203

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/dist-client/pages/activity/activity-list-page.d.ts +3 -0
  2. package/dist-client/pages/activity/activity-list-page.js +25 -13
  3. package/dist-client/pages/activity/activity-list-page.js.map +1 -1
  4. package/dist-client/pages/activity/activity-partial-view.js +1 -1
  5. package/dist-client/pages/activity/activity-partial-view.js.map +1 -1
  6. package/dist-client/pages/activity/assignee-roles-cell.d.ts +32 -0
  7. package/dist-client/pages/activity/assignee-roles-cell.js +26 -0
  8. package/dist-client/pages/activity/assignee-roles-cell.js.map +1 -0
  9. package/dist-client/pages/activity-catalog/activity-catalog-page.js +1 -1
  10. package/dist-client/pages/activity-catalog/activity-catalog-page.js.map +1 -1
  11. package/dist-client/pages/work-door/door-sentence.d.ts +1 -1
  12. package/dist-client/pages/work-door/door-sentence.js +2 -2
  13. package/dist-client/pages/work-door/door-sentence.js.map +1 -1
  14. package/dist-client/tsconfig.tsbuildinfo +1 -1
  15. package/dist-client/types/activity-instance-type.d.ts +2 -2
  16. package/dist-client/types/activity-instance-type.js.map +1 -1
  17. package/dist-client/types/activity-instance.d.ts +1 -1
  18. package/dist-client/types/activity-instance.js.map +1 -1
  19. package/dist-client/types/activity.d.ts +1 -1
  20. package/dist-client/types/activity.js.map +1 -1
  21. package/dist-server/controllers/activity-approval/resolve-judgeable-seats.d.ts +2 -2
  22. package/dist-server/controllers/activity-approval/resolve-judgeable-seats.js +2 -2
  23. package/dist-server/controllers/activity-approval/resolve-judgeable-seats.js.map +1 -1
  24. package/dist-server/controllers/activity-instance/abort.js +1 -1
  25. package/dist-server/controllers/activity-instance/abort.js.map +1 -1
  26. package/dist-server/controllers/activity-instance/assign.d.ts +2 -1
  27. package/dist-server/controllers/activity-instance/assign.js +104 -2
  28. package/dist-server/controllers/activity-instance/assign.js.map +1 -1
  29. package/dist-server/controllers/activity-instance/assignee-input.d.ts +25 -0
  30. package/dist-server/controllers/activity-instance/assignee-input.js +28 -0
  31. package/dist-server/controllers/activity-instance/assignee-input.js.map +1 -0
  32. package/dist-server/controllers/activity-instance/draft.js +7 -2
  33. package/dist-server/controllers/activity-instance/draft.js.map +1 -1
  34. package/dist-server/controllers/activity-instance/end.js +1 -1
  35. package/dist-server/controllers/activity-instance/end.js.map +1 -1
  36. package/dist-server/controllers/activity-instance/entitlement.d.ts +1 -1
  37. package/dist-server/controllers/activity-instance/entitlement.js +7 -5
  38. package/dist-server/controllers/activity-instance/entitlement.js.map +1 -1
  39. package/dist-server/controllers/activity-instance/issue.js +28 -7
  40. package/dist-server/controllers/activity-instance/issue.js.map +1 -1
  41. package/dist-server/controllers/activity-instance/pick.js +22 -3
  42. package/dist-server/controllers/activity-instance/pick.js.map +1 -1
  43. package/dist-server/controllers/activity-read/assert-may-read.js +2 -1
  44. package/dist-server/controllers/activity-read/assert-may-read.js.map +1 -1
  45. package/dist-server/controllers/activity-read/participates.d.ts +2 -2
  46. package/dist-server/controllers/activity-read/participates.js +2 -2
  47. package/dist-server/controllers/activity-read/participates.js.map +1 -1
  48. package/dist-server/controllers/activity-thread/check-authority.js +22 -4
  49. package/dist-server/controllers/activity-thread/check-authority.js.map +1 -1
  50. package/dist-server/controllers/common.js +17 -12
  51. package/dist-server/controllers/common.js.map +1 -1
  52. package/dist-server/service/activity/activity-history.d.ts +1 -2
  53. package/dist-server/service/activity/activity-history.js +4 -8
  54. package/dist-server/service/activity/activity-history.js.map +1 -1
  55. package/dist-server/service/activity/activity-mutation.js +39 -8
  56. package/dist-server/service/activity/activity-mutation.js.map +1 -1
  57. package/dist-server/service/activity/activity-query.d.ts +1 -1
  58. package/dist-server/service/activity/activity-query.js +9 -9
  59. package/dist-server/service/activity/activity-query.js.map +1 -1
  60. package/dist-server/service/activity/activity-type.d.ts +2 -2
  61. package/dist-server/service/activity/activity-type.js +12 -6
  62. package/dist-server/service/activity/activity-type.js.map +1 -1
  63. package/dist-server/service/activity/activity.d.ts +2 -2
  64. package/dist-server/service/activity/activity.js +15 -7
  65. package/dist-server/service/activity/activity.js.map +1 -1
  66. package/dist-server/service/activity/event-subscriber.js.map +1 -1
  67. package/dist-server/service/activity-instance/activity-instance-history.d.ts +1 -2
  68. package/dist-server/service/activity-instance/activity-instance-history.js +4 -8
  69. package/dist-server/service/activity-instance/activity-instance-history.js.map +1 -1
  70. package/dist-server/service/activity-instance/activity-instance-mutation.d.ts +1 -1
  71. package/dist-server/service/activity-instance/activity-instance-mutation.js +10 -5
  72. package/dist-server/service/activity-instance/activity-instance-mutation.js.map +1 -1
  73. package/dist-server/service/activity-instance/activity-instance-query.d.ts +1 -2
  74. package/dist-server/service/activity-instance/activity-instance-query.js +16 -25
  75. package/dist-server/service/activity-instance/activity-instance-query.js.map +1 -1
  76. package/dist-server/service/activity-instance/activity-instance-type.d.ts +2 -2
  77. package/dist-server/service/activity-instance/activity-instance-type.js +12 -6
  78. package/dist-server/service/activity-instance/activity-instance-type.js.map +1 -1
  79. package/dist-server/service/activity-instance/activity-instance.d.ts +2 -2
  80. package/dist-server/service/activity-instance/activity-instance.js +15 -7
  81. package/dist-server/service/activity-instance/activity-instance.js.map +1 -1
  82. package/dist-server/service/activity-instance/assignee-pool.d.ts +59 -0
  83. package/dist-server/service/activity-instance/assignee-pool.js +99 -0
  84. package/dist-server/service/activity-instance/assignee-pool.js.map +1 -0
  85. package/dist-server/service/activity-instance/event-subscriber.d.ts +9 -1
  86. package/dist-server/service/activity-instance/event-subscriber.js +20 -4
  87. package/dist-server/service/activity-instance/event-subscriber.js.map +1 -1
  88. package/dist-server/service/activity-summary/activity-summary-query.js +3 -3
  89. package/dist-server/service/activity-summary/activity-summary-query.js.map +1 -1
  90. package/dist-server/service/activity-thread/my-open-threads.d.ts +1 -1
  91. package/dist-server/service/activity-thread/my-open-threads.js +4 -3
  92. package/dist-server/service/activity-thread/my-open-threads.js.map +1 -1
  93. package/dist-server/service/subscription/subscription-filters.d.ts +3 -2
  94. package/dist-server/service/subscription/subscription-filters.js +6 -3
  95. package/dist-server/service/subscription/subscription-filters.js.map +1 -1
  96. package/dist-server/service/work-door/work-door-service.js +9 -6
  97. package/dist-server/service/work-door/work-door-service.js.map +1 -1
  98. package/dist-server/service/work-door/work-door-type.d.ts +1 -1
  99. package/dist-server/service/work-door/work-door-type.js +4 -4
  100. package/dist-server/service/work-door/work-door-type.js.map +1 -1
  101. package/dist-server/service/work-door/work-door.d.ts +3 -3
  102. package/dist-server/service/work-door/work-door.js +2 -2
  103. package/dist-server/service/work-door/work-door.js.map +1 -1
  104. package/dist-server/tsconfig.tsbuildinfo +1 -1
  105. package/package.json +9 -9
  106. package/tests/assign-role-pool.test.ts +508 -0
  107. package/tests/assignee-pool-history.test.ts +81 -0
  108. package/tests/may-read-case.test.ts +3 -2
  109. package/tests/org-member-domain.test.ts +10 -4
  110. package/tests/pick-race.pg.test.ts +2 -2
  111. package/tests/published-shape.test.ts +2 -2
  112. package/tests/role-pool-race.pg.test.ts +182 -0
  113. package/tests/work-door.test.ts +3 -3
  114. package/translations/en.json +5 -0
  115. package/translations/es.json +5 -0
  116. package/translations/ja.json +5 -0
  117. package/translations/ko.json +5 -0
  118. package/translations/ms.json +5 -0
  119. package/translations/zh.json +5 -0
@@ -1 +1 @@
1
- {"version":3,"file":"entitlement.js","sourceRoot":"","sources":["../../../server/controllers/activity-instance/entitlement.ts"],"names":[],"mappings":";;AAoEA,oDAgBC;AAiBD,sCA0BC;AA/HD,yDAAuE;AAIvE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AAEH,qFAAqF;AACrF,KAAK,UAAU,OAAO,CAAC,OAAwB;IAC7C,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,oBAAoB,EAAE,GAAG,OAAO,CAAC,KAAK,CAAA;IAEtE,OAAO,MAAM,IAAA,2BAAe,EAC1B,EAAE,QAAQ,EAAE,mBAAmB,EAAE,SAAS,EAAE,UAAU,EAAE,KAAK,EAAE,IAAI,EAAE,EACrE,IAAI,EACJ,MAAM,EACN,QAAQ,EACR,oBAAoB,CACrB,CAAA;AACH,CAAC;AAED;;;;;;GAMG;AACI,KAAK,UAAU,oBAAoB,CAAC,gBAAkC,EAAE,OAAwB;IACrG,MAAM,EAAE,IAAI,EAAE,GAAG,OAAO,CAAC,KAAK,CAAA;IAE9B,IAAI,gBAAgB,CAAC,SAAS,KAAK,IAAI,CAAC,EAAE,EAAE,CAAC;QAC3C,OAAM;IACR,CAAC;IAED,IAAI,MAAM,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;QAC3B,OAAM;IACR,CAAC;IAED,MAAM,IAAI,KAAK,CACb,OAAO,CAAC,CAAC,CAAC,iDAAiD,EAAE;QAC3D,EAAE,EAAE,gBAAgB,CAAC,EAAE;KACxB,CAAC,CACH,CAAA;AACH,CAAC;AAED;;;;;;;;;;;;;;GAcG;AACI,KAAK,UAAU,aAAa,CAAC,gBAAkC,EAAE,OAAwB;IAC9F,MAAM,EAAE,IAAI,EAAE,EAAE,EAAE,GAAG,OAAO,CAAC,KAAK,CAAA;IAClC,MAAM,MAAM,GAAG,gBAAgB,CAAC,cAAc,CAAA;IAE9C,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,OAAM;IACR,CAAC;IAED,MAAM,MAAM,GAAG,MAAM,EAAE,CAAC,aAAa,CAAC,gBAAI,CAAC,CAAC,OAAO,CAAC;QAClD,KAAK,EAAE,EAAE,EAAE,EAAE,IAAI,CAAC,EAAE,EAAE;QACtB,SAAS,EAAE,CAAC,OAAO,CAAC;KACrB,CAAC,CAAA;IAEF,IAAI,CAAC,MAAM,EAAE,KAAK,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,IAAU,EAAE,EAAE,CAAC,IAAI,CAAC,EAAE,KAAK,MAAM,CAAC,EAAE,CAAC;QACnE,OAAM;IACR,CAAC;IAED,IAAI,MAAM,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;QAC3B,OAAM;IACR,CAAC;IAED,MAAM,IAAI,KAAK,CACb,OAAO,CAAC,CAAC,CAAC,sDAAsD,EAAE;QAChE,EAAE,EAAE,gBAAgB,CAAC,EAAE;KACxB,CAAC,CACH,CAAA;AACH,CAAC","sourcesContent":["import { checkPermission, Role, User } from '@things-factory/auth-base'\n\nimport { ActivityInstance } from '../../service/activity-instance/activity-instance.js'\n\n/**\n * Who may act on **this one** activity instance.\n *\n * ── Why three mutations carry no `@privilege` ────────────────────────────────\n * `draftActivityInstance`, `issueActivityInstance` and `pickActivityInstance` were given\n * `@privilege(category: \"activity-instance\", privilege: \"mutation\", domainOwnerGranted: true)`\n * commented out, in the same commit that put it live on the other six\n * (`74acc61f6d`, 2025-04-10). That was a choice, not an omission, and the choice was right:\n *\n * gated acting on someone else's work, or destructive\n * assign · end · abort · delete draft\n * not gated doing your own work\n * draft · issue · pick\n *\n * That privilege is the **management** one. Requiring it to pick up a task addressed to you\n * would mean handing every operator on the floor the right to reassign and abort everyone\n * else's work. So the category gate stays off here.\n *\n * The reason was never written down anywhere, which is why the next person to read the file\n * saw three commented-out lines and could not tell \"deliberate\" from \"forgotten\". It is\n * written here now.\n *\n * ── What was actually missing ────────────────────────────────────────────────\n * \"Your own work\" was the intent and **nothing enforced it.** Each of the three takes an\n * id and loads the record without asking whether this person has anything to do with it:\n *\n * draft(id) edits a draft — never checked who wrote it\n * issue(id) issues a draft, routing approvals — never checked who wrote it\n * pick(id) takes an unassigned task — loaded `assigneeRole` and never read it\n *\n * The category gate cannot fix that. Turning it on locks out the operator whose task it is;\n * leaving it off lets anyone in the domain take, edit or issue anyone else's. Those are two\n * different questions and only the second one is open:\n *\n * category privilege may this person manage activity instances at all\n * entitlement may this person act on **this** record\n *\n * The approval side already has the second one — `reject.ts` refuses a reject from someone\n * who is only a reference on the order. The assignment side had no equivalent.\n *\n * Starting something new is untouched: with no id there is no owner to check, and anyone in\n * the domain may begin a task.\n */\n\n/** Whoever holds the management privilege is entitled to every record, as before. */\nasync function manages(context: ResolverContext): Promise<boolean> {\n const { domain, user, unsafeIP, prohibitedPrivileges } = context.state\n\n return await checkPermission(\n { category: 'activity-instance', privilege: 'mutation', owner: true },\n user,\n domain,\n unsafeIP,\n prohibitedPrivileges\n )\n}\n\n/**\n * May this person edit or issue this draft?\n *\n * The person who wrote it, or someone who manages activity instances. A draft is a private\n * thing until it is issued — it carries an approval line and assignees that become other\n * people's obligations the moment it goes out.\n */\nexport async function assertMayWorkOnDraft(activityInstance: ActivityInstance, context: ResolverContext) {\n const { user } = context.state\n\n if (activityInstance.creatorId === user.id) {\n return\n }\n\n if (await manages(context)) {\n return\n }\n\n throw new Error(\n context.t('error.activity-instance belongs to someone else', {\n id: activityInstance.id\n })\n )\n}\n\n/**\n * May this person pick up this task?\n *\n * ── When a role is named, it is the answer ───────────────────────────────────\n * `assigneeRole` is how an issuer says \"this is for the quality inspectors\". Reading it is\n * the whole point of writing it, and until now it was loaded and ignored.\n *\n * ── When no role is named, anyone in the domain may take it ──────────────────\n * That is the current behaviour and it is deliberate rather than a fallback: an issuer who\n * names nobody is saying \"whoever is free\". Refusing instead would make every unaddressed\n * task unpickable, which is the opposite of what leaving the field blank means.\n *\n * `reference_absent_value_leans_which_way` asks which way an absent value should lean, and\n * here the lean is the issuer's stated intent — not a guess the code makes to be safe.\n */\nexport async function assertMayPick(activityInstance: ActivityInstance, context: ResolverContext) {\n const { user, tx } = context.state\n const roleId = activityInstance.assigneeRoleId\n\n if (!roleId) {\n return\n }\n\n const holder = await tx.getRepository(User).findOne({\n where: { id: user.id },\n relations: ['roles']\n })\n\n if ((holder?.roles || []).some((role: Role) => role.id === roleId)) {\n return\n }\n\n if (await manages(context)) {\n return\n }\n\n throw new Error(\n context.t('error.activity-instance is addressed to another role', {\n id: activityInstance.id\n })\n )\n}\n"]}
1
+ {"version":3,"file":"entitlement.js","sourceRoot":"","sources":["../../../server/controllers/activity-instance/entitlement.ts"],"names":[],"mappings":";;AAqEA,oDAgBC;AAiBD,sCA2BC;AAjID,yDAAuE;AAGvE,uFAAwF;AAExF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AAEH,qFAAqF;AACrF,KAAK,UAAU,OAAO,CAAC,OAAwB;IAC7C,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,oBAAoB,EAAE,GAAG,OAAO,CAAC,KAAK,CAAA;IAEtE,OAAO,MAAM,IAAA,2BAAe,EAC1B,EAAE,QAAQ,EAAE,mBAAmB,EAAE,SAAS,EAAE,UAAU,EAAE,KAAK,EAAE,IAAI,EAAE,EACrE,IAAI,EACJ,MAAM,EACN,QAAQ,EACR,oBAAoB,CACrB,CAAA;AACH,CAAC;AAED;;;;;;GAMG;AACI,KAAK,UAAU,oBAAoB,CAAC,gBAAkC,EAAE,OAAwB;IACrG,MAAM,EAAE,IAAI,EAAE,GAAG,OAAO,CAAC,KAAK,CAAA;IAE9B,IAAI,gBAAgB,CAAC,SAAS,KAAK,IAAI,CAAC,EAAE,EAAE,CAAC;QAC3C,OAAM;IACR,CAAC;IAED,IAAI,MAAM,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;QAC3B,OAAM;IACR,CAAC;IAED,MAAM,IAAI,KAAK,CACb,OAAO,CAAC,CAAC,CAAC,iDAAiD,EAAE;QAC3D,EAAE,EAAE,gBAAgB,CAAC,EAAE;KACxB,CAAC,CACH,CAAA;AACH,CAAC;AAED;;;;;;;;;;;;;;GAcG;AACI,KAAK,UAAU,aAAa,CAAC,gBAAkC,EAAE,OAAwB;IAC9F,MAAM,EAAE,IAAI,EAAE,EAAE,EAAE,GAAG,OAAO,CAAC,KAAK,CAAA;IAClC,MAAM,OAAO,GAAG,IAAA,4BAAS,EAAC,gBAAgB,CAAC,CAAA;IAE3C,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC;QACpB,OAAM;IACR,CAAC;IAED,MAAM,MAAM,GAAG,MAAM,EAAE,CAAC,aAAa,CAAC,gBAAI,CAAC,CAAC,OAAO,CAAC;QAClD,KAAK,EAAE,EAAE,EAAE,EAAE,IAAI,CAAC,EAAE,EAAE;QACtB,SAAS,EAAE,CAAC,OAAO,CAAC;KACrB,CAAC,CAAA;IAEF,kEAAkE;IAClE,IAAI,IAAA,6BAAU,EAAC,CAAC,MAAM,EAAE,KAAK,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,IAAU,EAAE,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,OAAO,CAAC,EAAE,CAAC;QAC5E,OAAM;IACR,CAAC;IAED,IAAI,MAAM,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;QAC3B,OAAM;IACR,CAAC;IAED,MAAM,IAAI,KAAK,CACb,OAAO,CAAC,CAAC,CAAC,sDAAsD,EAAE;QAChE,EAAE,EAAE,gBAAgB,CAAC,EAAE;KACxB,CAAC,CACH,CAAA;AACH,CAAC","sourcesContent":["import { checkPermission, Role, User } from '@things-factory/auth-base'\n\nimport { ActivityInstance } from '../../service/activity-instance/activity-instance.js'\nimport { holdsAnyOf, poolIdsOf } from '../../service/activity-instance/assignee-pool.js'\n\n/**\n * Who may act on **this one** activity instance.\n *\n * ── Why three mutations carry no `@privilege` ────────────────────────────────\n * `draftActivityInstance`, `issueActivityInstance` and `pickActivityInstance` were given\n * `@privilege(category: \"activity-instance\", privilege: \"mutation\", domainOwnerGranted: true)`\n * commented out, in the same commit that put it live on the other six\n * (`74acc61f6d`, 2025-04-10). That was a choice, not an omission, and the choice was right:\n *\n * gated acting on someone else's work, or destructive\n * assign · end · abort · delete draft\n * not gated doing your own work\n * draft · issue · pick\n *\n * That privilege is the **management** one. Requiring it to pick up a task addressed to you\n * would mean handing every operator on the floor the right to reassign and abort everyone\n * else's work. So the category gate stays off here.\n *\n * The reason was never written down anywhere, which is why the next person to read the file\n * saw three commented-out lines and could not tell \"deliberate\" from \"forgotten\". It is\n * written here now.\n *\n * ── What was actually missing ────────────────────────────────────────────────\n * \"Your own work\" was the intent and **nothing enforced it.** Each of the three takes an\n * id and loads the record without asking whether this person has anything to do with it:\n *\n * draft(id) edits a draft — never checked who wrote it\n * issue(id) issues a draft, routing approvals — never checked who wrote it\n * pick(id) takes an unassigned task — loaded the assignee role and never read it\n *\n * The category gate cannot fix that. Turning it on locks out the operator whose task it is;\n * leaving it off lets anyone in the domain take, edit or issue anyone else's. Those are two\n * different questions and only the second one is open:\n *\n * category privilege may this person manage activity instances at all\n * entitlement may this person act on **this** record\n *\n * The approval side already has the second one — `reject.ts` refuses a reject from someone\n * who is only a reference on the order. The assignment side had no equivalent.\n *\n * Starting something new is untouched: with no id there is no owner to check, and anyone in\n * the domain may begin a task.\n */\n\n/** Whoever holds the management privilege is entitled to every record, as before. */\nasync function manages(context: ResolverContext): Promise<boolean> {\n const { domain, user, unsafeIP, prohibitedPrivileges } = context.state\n\n return await checkPermission(\n { category: 'activity-instance', privilege: 'mutation', owner: true },\n user,\n domain,\n unsafeIP,\n prohibitedPrivileges\n )\n}\n\n/**\n * May this person edit or issue this draft?\n *\n * The person who wrote it, or someone who manages activity instances. A draft is a private\n * thing until it is issued — it carries an approval line and assignees that become other\n * people's obligations the moment it goes out.\n */\nexport async function assertMayWorkOnDraft(activityInstance: ActivityInstance, context: ResolverContext) {\n const { user } = context.state\n\n if (activityInstance.creatorId === user.id) {\n return\n }\n\n if (await manages(context)) {\n return\n }\n\n throw new Error(\n context.t('error.activity-instance belongs to someone else', {\n id: activityInstance.id\n })\n )\n}\n\n/**\n * May this person pick up this task?\n *\n * ── When a role is named, it is the answer ───────────────────────────────────\n * `assigneeRoles` is how an issuer says \"this is for the quality inspectors\". Reading it is\n * the whole point of writing it, and until now it was loaded and ignored.\n *\n * ── When no role is named, anyone in the domain may take it ──────────────────\n * That is the current behaviour and it is deliberate rather than a fallback: an issuer who\n * names nobody is saying \"whoever is free\". Refusing instead would make every unaddressed\n * task unpickable, which is the opposite of what leaving the field blank means.\n *\n * `reference_absent_value_leans_which_way` asks which way an absent value should lean, and\n * here the lean is the issuer's stated intent — not a guess the code makes to be safe.\n */\nexport async function assertMayPick(activityInstance: ActivityInstance, context: ResolverContext) {\n const { user, tx } = context.state\n const poolIds = poolIdsOf(activityInstance)\n\n if (!poolIds.length) {\n return\n }\n\n const holder = await tx.getRepository(User).findOne({\n where: { id: user.id },\n relations: ['roles']\n })\n\n /* Any one of the pool's roles is enough (ADR-0091 decision 9). */\n if (holdsAnyOf((holder?.roles || []).map((role: Role) => role.id), poolIds)) {\n return\n }\n\n if (await manages(context)) {\n return\n }\n\n throw new Error(\n context.t('error.activity-instance is addressed to another role', {\n id: activityInstance.id\n })\n )\n}\n"]}
@@ -7,6 +7,8 @@ const activity_instance_js_1 = require("../../service/activity-instance/activity
7
7
  const common_js_1 = require("../common.js");
8
8
  const submit_js_1 = require("../activity-thread/submit.js");
9
9
  const approval_line_input_js_1 = require("./approval-line-input.js");
10
+ const assignee_input_js_1 = require("./assignee-input.js");
11
+ const assignee_pool_js_1 = require("../../service/activity-instance/assignee-pool.js");
10
12
  const entitlement_js_1 = require("./entitlement.js");
11
13
  const claim_draft_js_1 = require("./claim-draft.js");
12
14
  const approval_expectation_js_1 = require("./approval-expectation.js");
@@ -15,14 +17,14 @@ const resolve_judgeable_seats_js_1 = require("../activity-approval/resolve-judge
15
17
  async function issue(activityInstance, context) {
16
18
  const { domain, user, tx } = context.state;
17
19
  const issuedAt = new Date();
18
- var { id, name, description, assignees, uiType, uiSource, viewType, viewSource, dueAt, priority, approvalLine, activityId, activityType, assigneeRole, supervisoryRole, input } = activityInstance;
20
+ var { id, name, description, assignees, uiType, uiSource, viewType, viewSource, dueAt, priority, approvalLine, activityId, activityType, assigneeRoles, supervisoryRole, input } = activityInstance;
19
21
  var origin = id
20
22
  ? await tx.getRepository(activity_instance_js_1.ActivityInstance).findOne({
21
23
  where: { domain: { id: domain.id }, id },
22
24
  relations: [
23
25
  'domain',
24
26
  'activity',
25
- 'assigneeRole',
27
+ 'assigneeRoles',
26
28
  'supervisoryRole',
27
29
  'updater',
28
30
  'creator',
@@ -65,7 +67,7 @@ async function issue(activityInstance, context) {
65
67
  var activity = origin?.activity ||
66
68
  (await repository.findOne({
67
69
  where: { domain: domain.parentId ? { id: (0, typeorm_1.In)([domain.id, domain.parentId]) } : { id: domain.id }, id: activityId },
68
- relations: ['assigneeRole', 'supervisoryRole']
70
+ relations: ['assigneeRoles', 'supervisoryRole']
69
71
  }));
70
72
  if (!activity) {
71
73
  throw new Error(context.t('error.activity not found', {
@@ -87,7 +89,6 @@ async function issue(activityInstance, context) {
87
89
  activityInstance.uiSource = uiSource || activity.uiSource;
88
90
  activityInstance.viewType = viewType || activity.viewType;
89
91
  activityInstance.viewSource = viewSource || activity.viewSource;
90
- activityInstance.assigneeRole = assigneeRole || activity.assigneeRole;
91
92
  activityInstance.supervisoryRole = supervisoryRole || activity.supervisoryRole;
92
93
  activityInstance.priority = priority ?? activity.priority;
93
94
  activityInstance.adhocType = 'standard';
@@ -113,6 +114,26 @@ async function issue(activityInstance, context) {
113
114
  }
114
115
  }
115
116
  }
117
+ /*
118
+ * The pool - handed in, else the activity's for a new case (a draft keeps the one it was written with). Every
119
+ * role is read in the case's domain, each once, or the case is refused by name: a role nobody here holds only
120
+ * looks like a pool. § assignee-pool.ts
121
+ */
122
+ if (assigneeRoles) {
123
+ activityInstance.assigneeRoles = await (0, assignee_pool_js_1.resolvePoolRoles)(assigneeRoles, context);
124
+ }
125
+ else if (!origin) {
126
+ activityInstance.assigneeRoles = await (0, assignee_pool_js_1.resolvePoolRoles)(activity.assigneeRoles, context);
127
+ }
128
+ /*
129
+ * A role is not an assignee - it became the role's first user. Seen after the activity's list is taken
130
+ * over, so a list saved on the activity before this is refused too. § assignee-input.ts
131
+ */
132
+ if ((0, assignee_input_js_1.roleAssigneeRefusal)(assignees)) {
133
+ throw Object.assign(new Error(context.t('error.activity-instance a role is not an assignee')), {
134
+ code: assignee_input_js_1.ROLE_IS_NOT_AN_ASSIGNEE
135
+ });
136
+ }
116
137
  /*
117
138
  * **결재를 받겠다고 낸 건은 결재선이 있어야 한다.**
118
139
  *
@@ -224,7 +245,7 @@ async function issue(activityInstance, context) {
224
245
  1. 하나의 thread가 필요한 경우 (대부분)
225
246
  조건 : activityInstance의 threadsMin이 1 이어야 한다.
226
247
  로직 1 : getSystemUserFromOrgMemberItem 의 결과가 한명이라면 그대로 activityThread.assignee로 직접 할당한다.
227
- 로직 2 : getSystemUserFromOrgMemberItem 의 결과가 한명이 아니라면, assignee타입이 assigneeRole 인 경우에만 처리가 가능하다.
248
+ 로직 2 : getSystemUserFromOrgMemberItem 의 결과가 한명이 아니라면, assigneeRoles 가 있는 경우에만 처리가 가능하다.
228
249
  이 경우에는 assignee를 비운다. - createActivityThreadWithUnassigned
229
250
  2. 할당되지 않은 복수개의 thread가 필요한 경우
230
251
  조건 : threadsMin이 1 이상이어야 한다.
@@ -237,9 +258,9 @@ async function issue(activityInstance, context) {
237
258
  // assignees가 복수인 것은 아무 의미가 없다.
238
259
  const assignedUsers = await Promise.all((assignees || []).map(assignee => (0, common_js_1.getSystemUserFromOrgMemberItem)(assignee, context)));
239
260
  if (assignedUsers.length == 0) {
240
- if (issued.assigneeRoleId) {
261
+ if ((0, assignee_pool_js_1.poolIdsOf)(issued).length) {
241
262
  if (issued.threadsMin === 0) {
242
- /* assigneeRoleId에 해당하는 모든 assignee 에게 태스크를 할당하는 경우 */
263
+ /* 풀의 역할들 중 하나라도 가진 사람 모두에게 — 한 사람에 한 번 */
243
264
  await (0, common_js_1.createActivityThreadsForAllRoleUsers)('issue', issued, context);
244
265
  }
245
266
  else {
@@ -1 +1 @@
1
- {"version":3,"file":"issue.js","sourceRoot":"","sources":["../../../server/controllers/activity-instance/issue.ts"],"names":[],"mappings":"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","sourcesContent":["import { In } from 'typeorm'\nimport { Activity } from '../../service/activity/activity.js'\nimport { ActivityInstance, ActivityInstanceStatus } from '../../service/activity-instance/activity-instance.js'\nimport { ActivityInstanceIssue } from '../../service/activity-instance/activity-instance-type.js'\nimport {\n createActivityThreadWithUnassigned,\n createActivityThreadsForAllRoleUsers,\n createActivityThreadsForUsers,\n getSystemUserFromOrgMemberItem,\n createActivityThreadForUser,\n fillActivitySearchKeys\n} from '../common.js'\nimport { submit } from '../activity-thread/submit.js'\nimport { LINE_IS_SET, approvalLineInputRefusal } from './approval-line-input.js'\nimport { assertMayWorkOnDraft } from './entitlement.js'\nimport { claimDraft } from './claim-draft.js'\nimport { expectationBroken, judgeApprovalExpectation } from './approval-expectation.js'\nimport { STANDING_REFUSAL_KEYS, judgeSomeoneElseCanJudge, standingBroken } from '../activity-approval/own-request.js'\nimport { resolveJudgeableSeats } from '../activity-approval/resolve-judgeable-seats.js'\n\nexport async function issue(\n activityInstance: ActivityInstanceIssue,\n context: ResolverContext\n): Promise<ActivityInstance> {\n const { domain, user, tx } = context.state\n const issuedAt = new Date()\n var {\n id,\n name,\n description,\n assignees,\n uiType,\n uiSource,\n viewType,\n viewSource,\n dueAt,\n priority,\n approvalLine,\n activityId,\n activityType,\n assigneeRole,\n supervisoryRole,\n input\n } = activityInstance\n\n var origin = id\n ? await tx.getRepository(ActivityInstance).findOne({\n where: { domain: { id: domain.id }, id },\n relations: [\n 'domain',\n 'activity',\n 'assigneeRole',\n 'supervisoryRole',\n 'updater',\n 'creator',\n 'starter',\n 'terminator'\n ]\n })\n : null\n\n /*\n 남의 초안은 못 내보낸다.\n\n 내보내는 순간 결재선과 담당자가 서고, 그것은 **다른 사람들의 의무**가 된다. 새로 시작하는\n 것(id 없음)은 주인이 없으므로 이 검사에 안 걸린다. 자격 규칙은 `entitlement.ts` 에 있다.\n */\n if (origin) {\n await assertMayWorkOnDraft(origin, context)\n }\n\n /* \n Prerequisites for a Task to Be Issued.\n - The previous state of the task should be Draft.\n */\n if (origin && origin.state !== ActivityInstanceStatus.Draft) {\n throw new Error(\n context.t(`error.activity-instance is already issued`, {\n id: id,\n state: origin.state\n })\n )\n }\n\n /*\n * An id that is not here is refused - not taken as a new case. Taken as new, a draft discarded a moment\n * ago came back under the same id, issued. And a draft that is here is claimed before anything else is\n * written, so a discard cannot slip in between - see claim-draft.ts.\n */\n if (id && !origin) {\n throw new Error(context.t('error.activity-instance not found', { activityInstance: id }))\n }\n\n if (origin) {\n await claimDraft(origin.id, context)\n }\n\n var repository = tx.getRepository(Activity)\n var activity =\n origin?.activity ||\n (await repository.findOne({\n where: { domain: domain.parentId ? { id: In([domain.id, domain.parentId]) } : { id: domain.id }, id: activityId },\n relations: ['assigneeRole', 'supervisoryRole']\n }))\n\n if (!activity) {\n throw new Error(\n context.t('error.activity not found', {\n activity: activityId\n })\n )\n }\n\n /*\n * The activity's approval line is set on its detail screen and nowhere else (ruling 2026-09-24).\n * A line handed in must be absent or the same one - § approval-line-input.ts.\n */\n if (approvalLineInputRefusal(activity.approvalLine, approvalLine)) {\n throw Object.assign(new Error(context.t('error.activity-instance approval line is set')), { code: LINE_IS_SET })\n }\n\n if (!origin) {\n activityInstance.name = name || activity.name\n activityInstance.description = description || activity.description\n activityInstance.activityType = activityType || activity.activityType\n activityInstance.uiType = uiType || activity.uiType\n activityInstance.uiSource = uiSource || activity.uiSource\n activityInstance.viewType = viewType || activity.viewType\n activityInstance.viewSource = viewSource || activity.viewSource\n activityInstance.assigneeRole = assigneeRole || activity.assigneeRole\n activityInstance.supervisoryRole = supervisoryRole || activity.supervisoryRole\n activityInstance.priority = priority ?? activity.priority\n\n activityInstance.adhocType = 'standard'\n activityInstance.refBy = activityId\n\n /*\n * 낸 쪽이 직접 주지 않았으면 액티비티의 선언을 따른다.\n *\n * 위의 `if (!assignees || !approvalLine)` 안에 두지 않는다 — 둘 다 준 건에서는 그 블록에 안 들어가고,\n * 그러면 이 축만 조용히 빠진다.\n */\n if (activityInstance.rejectionIsFinal === undefined || activityInstance.rejectionIsFinal === null) {\n activityInstance.rejectionIsFinal = activity.rejectionIsFinal ?? false\n }\n\n if (!assignees || !approvalLine) {\n if (!assignees) {\n assignees = activityInstance.assignees = activity.assignees\n }\n\n if (!approvalLine) {\n approvalLine = activityInstance.approvalLine = activity.approvalLine\n }\n\n if (!dueAt && activity.standardTime) {\n activityInstance.dueAt = new Date(issuedAt.getTime() + activity.standardTime * 1000)\n }\n }\n }\n\n /*\n * **결재를 받겠다고 낸 건은 결재선이 있어야 한다.**\n *\n * 액티비티의 결재선을 물려받은 뒤에 본다 — 낸 쪽이 직접 주지 않아도 액티비티가 들고 있으면 그것이\n * 이 건의 결재선이다. 물려받기 전에 보면 멀쩡한 건을 막는다.\n *\n * 초안을 다시 내는 경우 결재선은 원본이 들고 있으므로 그것까지 함께 본다.\n *\n * 판단은 `approval-expectation.ts` 가 든다 — 여기서 조건을 적으면 시험할 수 없다.\n */\n const expectation = judgeApprovalExpectation({\n requireApproval: activityInstance.requireApproval,\n approvalLine: activityInstance.approvalLine ?? origin?.approvalLine,\n activityName: activity.name\n })\n\n if (expectationBroken(expectation)) {\n throw new Error(\n context.t('error.approval line is empty but approval was required', {\n activity: expectation.activityName,\n defaultValue:\n `\"${expectation.activityName}\" 에 결재선이 없습니다. 이 건은 결재를 받기로 하고 낸 것이라 올리지 않습니다 — ` +\n '그대로 두면 결재 없이 확정됩니다.'\n })\n )\n }\n\n /*\n * **올린 사람 말고 설 사람이 있어야 올릴 수 있다.**\n *\n * 결재는 누르는 자리에서도 막지만(§`own-request.ts`), 거기서만 막으면 결재선에 올린 사람밖에 없는\n * 건이 **아무에게도 안 간다.** 정비 완료 확인이 그 모양이면 설비가 정비중으로 남고 라인이 선다.\n *\n * 그래서 올리는 이 자리에서 먼저 본다. 여기서 걸리면 결재선을 고칠 수 있는 사람이 그 자리에서\n * 무엇을 고쳐야 하는지 읽는다. (ADR-0052 ②)\n *\n * 판단은 `own-request.ts` 가 들고, 자리를 푸는 것은 `resolve-judgeable-seats.ts` 가 한다 — 여기에\n * 조건을 적으면 위의 결재선 검사와 같은 이유로 시험할 수 없다.\n */\n const standing = judgeSomeoneElseCanJudge({\n issuerId: user?.id,\n seats: await resolveJudgeableSeats(activityInstance.approvalLine ?? origin?.approvalLine, context)\n })\n\n if (standingBroken(standing)) {\n /*\n * 사유마다 한 문장씩 — **자리가 같다고 사유를 합치지 않는다.** 관리자가 할 일이 다르다: 하나는\n * 「그 자리에 다른 결재자를」이고 하나는 「그 역할에 사람을」이다. (ADR-0047 ② 의 낱말)\n */\n throw new Error(\n standing.refusals\n .map(refusal => {\n /* 이름이 안 풀린 자리는 결재선에 적힌 값이라도 보인다 — 관리자가 고칠 실마리가 그것뿐이다. */\n const seats = refusal.seats\n .map(seat => `${seat.order}차${seat.name ? ` (${seat.name})` : seat.pointsAt ? ` (${seat.pointsAt})` : ''}`)\n .join(', ')\n\n const said = {\n 'approval-role-empty': `${seats} 결재 자리가 지목한 역할에 사람이 없습니다. 그대로 올리면 아무도 결재할 수 없습니다 — 그 역할에 사람을 넣어 주십시오.`,\n 'approval-person-missing': `${seats} 결재 자리가 지목한 사람을 찾을 수 없습니다. 그대로 올리면 아무도 결재할 수 없습니다 — 그 자리에 지금 있는 사람을 다시 지정해 주십시오.`,\n 'only-the-issuer-could-judge': `${seats} 결재 자리에 올린 사람 말고 설 사람이 없습니다. 그대로 올리면 아무도 결재할 수 없습니다 — 그 자리에 다른 결재자를 넣거나, 그 역할을 가진 사람을 늘려 주십시오.`\n }[refusal.because]\n\n return context.t(STANDING_REFUSAL_KEYS[refusal.because], { seats, defaultValue: said })\n })\n .join(' ')\n )\n }\n\n const activitySearchKeys = fillActivitySearchKeys(activity?.searchKeys, input)\n\n if (activity.startingType == 'post') {\n /* \n startingType이 'post'이면, \n activityInstance 이슈와 동시에 issuer에게 할당된 activityThread가 생성되고 submit까지 모두 완료되게 된다.\n 만일, 결재선이 있다면, 상신되게 된다.\n */\n const posted = await tx.getRepository(ActivityInstance).save({\n version: 1,\n creator: user,\n ...origin,\n ...activityInstance,\n transaction: 'post',\n activity,\n ...activitySearchKeys,\n state: ActivityInstanceStatus.Started,\n domain,\n issuer: user,\n updater: user,\n issuedAt,\n startedAt: issuedAt\n })\n\n const thread = await createActivityThreadForUser('post', posted, user, context)\n\n /* post 방식은 thread가 하나이므로, instance의 output을 그대로 thread의 output이라고 판단할 수 있다. 따라서, 아래 로직은 제거한다. */\n // const data = { ...activityInstance.input, ...activityInstance.output }\n // const output = (activity.model || [])\n // .filter(item => item.inout === 'inout' || item.inout === 'out')\n // .reduce((inout, item) => {\n // inout[item.tag] = data[item.tag]\n // return inout\n // }, {})\n\n await submit({ id: thread.id, output: activityInstance.output }, context)\n\n return await tx.getRepository(ActivityInstance).findOneBy({ id: posted.id })\n } else {\n const issued = await tx.getRepository(ActivityInstance).save({\n version: 1,\n creator: user,\n ...origin,\n ...activityInstance,\n transaction: 'issue',\n activity,\n ...activitySearchKeys,\n state: ActivityInstanceStatus.Issued,\n domain,\n issuer: user,\n updater: user,\n issuedAt\n })\n\n /*\n 1. 하나의 thread가 필요한 경우 (대부분)\n 조건 : activityInstance의 threadsMin이 1 이어야 한다.\n 로직 1 : getSystemUserFromOrgMemberItem 의 결과가 한명이라면 그대로 activityThread.assignee로 직접 할당한다.\n 로직 2 : getSystemUserFromOrgMemberItem 의 결과가 한명이 아니라면, assignee타입이 assigneeRole 인 경우에만 처리가 가능하다.\n 이 경우에는 assignee를 비운다. - createActivityThreadWithUnassigned\n 2. 할당되지 않은 복수개의 thread가 필요한 경우\n 조건 : threadsMin이 1 이상이어야 한다.\n 로직 1 : getSystemUserFromOrgMemberItem 의 결과와 threadsMin 값이 같다면, 그대로 activityThread.assignee로 직접 할당한다. createActivityThreadsForAllRoleUsers\n 로직 2 : createActivityThreadWithUnassigned\n 3. 모든 assignee가 처리를 해야하는 경우\n 조건 : threadsMin이 0 이어야 한다.\n 로직 : getSystemUserFromOrgMemberItem 의 결과로 createActivityThreadsForUsers\n */\n\n // assignees가 복수인 것은 아무 의미가 없다.\n const assignedUsers = await Promise.all(\n (assignees || []).map(assignee => getSystemUserFromOrgMemberItem(assignee, context))\n )\n\n if (assignedUsers.length == 0) {\n if (issued.assigneeRoleId) {\n if (issued.threadsMin === 0) {\n /* assigneeRoleId에 해당하는 모든 assignee 에게 태스크를 할당하는 경우 */\n await createActivityThreadsForAllRoleUsers('issue', issued, context)\n } else {\n /* minimum threads 설정에 따른 pending assignment 상태로 activityThread를 만드는 경우 */\n await createActivityThreadWithUnassigned('issue', issued, context)\n }\n }\n } else {\n await createActivityThreadsForUsers('issue', issued, assignedUsers, context)\n }\n\n return await tx.getRepository(ActivityInstance).findOneBy({ id: issued.id })\n }\n}\n"]}
1
+ {"version":3,"file":"issue.js","sourceRoot":"","sources":["../../../server/controllers/activity-instance/issue.ts"],"names":[],"mappings":"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","sourcesContent":["import { In } from 'typeorm'\nimport { Activity } from '../../service/activity/activity.js'\nimport { ActivityInstance, ActivityInstanceStatus } from '../../service/activity-instance/activity-instance.js'\nimport { ActivityInstanceIssue } from '../../service/activity-instance/activity-instance-type.js'\nimport {\n createActivityThreadWithUnassigned,\n createActivityThreadsForAllRoleUsers,\n createActivityThreadsForUsers,\n getSystemUserFromOrgMemberItem,\n createActivityThreadForUser,\n fillActivitySearchKeys\n} from '../common.js'\nimport { submit } from '../activity-thread/submit.js'\nimport { LINE_IS_SET, approvalLineInputRefusal } from './approval-line-input.js'\nimport { ROLE_IS_NOT_AN_ASSIGNEE, roleAssigneeRefusal } from './assignee-input.js'\nimport { poolIdsOf, resolvePoolRoles } from '../../service/activity-instance/assignee-pool.js'\nimport { assertMayWorkOnDraft } from './entitlement.js'\nimport { claimDraft } from './claim-draft.js'\nimport { expectationBroken, judgeApprovalExpectation } from './approval-expectation.js'\nimport { STANDING_REFUSAL_KEYS, judgeSomeoneElseCanJudge, standingBroken } from '../activity-approval/own-request.js'\nimport { resolveJudgeableSeats } from '../activity-approval/resolve-judgeable-seats.js'\n\nexport async function issue(\n activityInstance: ActivityInstanceIssue,\n context: ResolverContext\n): Promise<ActivityInstance> {\n const { domain, user, tx } = context.state\n const issuedAt = new Date()\n var {\n id,\n name,\n description,\n assignees,\n uiType,\n uiSource,\n viewType,\n viewSource,\n dueAt,\n priority,\n approvalLine,\n activityId,\n activityType,\n assigneeRoles,\n supervisoryRole,\n input\n } = activityInstance\n\n var origin = id\n ? await tx.getRepository(ActivityInstance).findOne({\n where: { domain: { id: domain.id }, id },\n relations: [\n 'domain',\n 'activity',\n 'assigneeRoles',\n 'supervisoryRole',\n 'updater',\n 'creator',\n 'starter',\n 'terminator'\n ]\n })\n : null\n\n /*\n 남의 초안은 못 내보낸다.\n\n 내보내는 순간 결재선과 담당자가 서고, 그것은 **다른 사람들의 의무**가 된다. 새로 시작하는\n 것(id 없음)은 주인이 없으므로 이 검사에 안 걸린다. 자격 규칙은 `entitlement.ts` 에 있다.\n */\n if (origin) {\n await assertMayWorkOnDraft(origin, context)\n }\n\n /* \n Prerequisites for a Task to Be Issued.\n - The previous state of the task should be Draft.\n */\n if (origin && origin.state !== ActivityInstanceStatus.Draft) {\n throw new Error(\n context.t(`error.activity-instance is already issued`, {\n id: id,\n state: origin.state\n })\n )\n }\n\n /*\n * An id that is not here is refused - not taken as a new case. Taken as new, a draft discarded a moment\n * ago came back under the same id, issued. And a draft that is here is claimed before anything else is\n * written, so a discard cannot slip in between - see claim-draft.ts.\n */\n if (id && !origin) {\n throw new Error(context.t('error.activity-instance not found', { activityInstance: id }))\n }\n\n if (origin) {\n await claimDraft(origin.id, context)\n }\n\n var repository = tx.getRepository(Activity)\n var activity =\n origin?.activity ||\n (await repository.findOne({\n where: { domain: domain.parentId ? { id: In([domain.id, domain.parentId]) } : { id: domain.id }, id: activityId },\n relations: ['assigneeRoles', 'supervisoryRole']\n }))\n\n if (!activity) {\n throw new Error(\n context.t('error.activity not found', {\n activity: activityId\n })\n )\n }\n\n /*\n * The activity's approval line is set on its detail screen and nowhere else (ruling 2026-09-24).\n * A line handed in must be absent or the same one - § approval-line-input.ts.\n */\n if (approvalLineInputRefusal(activity.approvalLine, approvalLine)) {\n throw Object.assign(new Error(context.t('error.activity-instance approval line is set')), { code: LINE_IS_SET })\n }\n\n if (!origin) {\n activityInstance.name = name || activity.name\n activityInstance.description = description || activity.description\n activityInstance.activityType = activityType || activity.activityType\n activityInstance.uiType = uiType || activity.uiType\n activityInstance.uiSource = uiSource || activity.uiSource\n activityInstance.viewType = viewType || activity.viewType\n activityInstance.viewSource = viewSource || activity.viewSource\n activityInstance.supervisoryRole = supervisoryRole || activity.supervisoryRole\n activityInstance.priority = priority ?? activity.priority\n\n activityInstance.adhocType = 'standard'\n activityInstance.refBy = activityId\n\n /*\n * 낸 쪽이 직접 주지 않았으면 액티비티의 선언을 따른다.\n *\n * 위의 `if (!assignees || !approvalLine)` 안에 두지 않는다 — 둘 다 준 건에서는 그 블록에 안 들어가고,\n * 그러면 이 축만 조용히 빠진다.\n */\n if (activityInstance.rejectionIsFinal === undefined || activityInstance.rejectionIsFinal === null) {\n activityInstance.rejectionIsFinal = activity.rejectionIsFinal ?? false\n }\n\n if (!assignees || !approvalLine) {\n if (!assignees) {\n assignees = activityInstance.assignees = activity.assignees\n }\n\n if (!approvalLine) {\n approvalLine = activityInstance.approvalLine = activity.approvalLine\n }\n\n if (!dueAt && activity.standardTime) {\n activityInstance.dueAt = new Date(issuedAt.getTime() + activity.standardTime * 1000)\n }\n }\n }\n\n /*\n * The pool - handed in, else the activity's for a new case (a draft keeps the one it was written with). Every\n * role is read in the case's domain, each once, or the case is refused by name: a role nobody here holds only\n * looks like a pool. § assignee-pool.ts\n */\n if (assigneeRoles) {\n activityInstance.assigneeRoles = await resolvePoolRoles(assigneeRoles, context)\n } else if (!origin) {\n activityInstance.assigneeRoles = await resolvePoolRoles(activity.assigneeRoles, context)\n }\n\n /*\n * A role is not an assignee - it became the role's first user. Seen after the activity's list is taken\n * over, so a list saved on the activity before this is refused too. § assignee-input.ts\n */\n if (roleAssigneeRefusal(assignees)) {\n throw Object.assign(new Error(context.t('error.activity-instance a role is not an assignee')), {\n code: ROLE_IS_NOT_AN_ASSIGNEE\n })\n }\n\n /*\n * **결재를 받겠다고 낸 건은 결재선이 있어야 한다.**\n *\n * 액티비티의 결재선을 물려받은 뒤에 본다 — 낸 쪽이 직접 주지 않아도 액티비티가 들고 있으면 그것이\n * 이 건의 결재선이다. 물려받기 전에 보면 멀쩡한 건을 막는다.\n *\n * 초안을 다시 내는 경우 결재선은 원본이 들고 있으므로 그것까지 함께 본다.\n *\n * 판단은 `approval-expectation.ts` 가 든다 — 여기서 조건을 적으면 시험할 수 없다.\n */\n const expectation = judgeApprovalExpectation({\n requireApproval: activityInstance.requireApproval,\n approvalLine: activityInstance.approvalLine ?? origin?.approvalLine,\n activityName: activity.name\n })\n\n if (expectationBroken(expectation)) {\n throw new Error(\n context.t('error.approval line is empty but approval was required', {\n activity: expectation.activityName,\n defaultValue:\n `\"${expectation.activityName}\" 에 결재선이 없습니다. 이 건은 결재를 받기로 하고 낸 것이라 올리지 않습니다 — ` +\n '그대로 두면 결재 없이 확정됩니다.'\n })\n )\n }\n\n /*\n * **올린 사람 말고 설 사람이 있어야 올릴 수 있다.**\n *\n * 결재는 누르는 자리에서도 막지만(§`own-request.ts`), 거기서만 막으면 결재선에 올린 사람밖에 없는\n * 건이 **아무에게도 안 간다.** 정비 완료 확인이 그 모양이면 설비가 정비중으로 남고 라인이 선다.\n *\n * 그래서 올리는 이 자리에서 먼저 본다. 여기서 걸리면 결재선을 고칠 수 있는 사람이 그 자리에서\n * 무엇을 고쳐야 하는지 읽는다. (ADR-0052 ②)\n *\n * 판단은 `own-request.ts` 가 들고, 자리를 푸는 것은 `resolve-judgeable-seats.ts` 가 한다 — 여기에\n * 조건을 적으면 위의 결재선 검사와 같은 이유로 시험할 수 없다.\n */\n const standing = judgeSomeoneElseCanJudge({\n issuerId: user?.id,\n seats: await resolveJudgeableSeats(activityInstance.approvalLine ?? origin?.approvalLine, context)\n })\n\n if (standingBroken(standing)) {\n /*\n * 사유마다 한 문장씩 — **자리가 같다고 사유를 합치지 않는다.** 관리자가 할 일이 다르다: 하나는\n * 「그 자리에 다른 결재자를」이고 하나는 「그 역할에 사람을」이다. (ADR-0047 ② 의 낱말)\n */\n throw new Error(\n standing.refusals\n .map(refusal => {\n /* 이름이 안 풀린 자리는 결재선에 적힌 값이라도 보인다 — 관리자가 고칠 실마리가 그것뿐이다. */\n const seats = refusal.seats\n .map(seat => `${seat.order}차${seat.name ? ` (${seat.name})` : seat.pointsAt ? ` (${seat.pointsAt})` : ''}`)\n .join(', ')\n\n const said = {\n 'approval-role-empty': `${seats} 결재 자리가 지목한 역할에 사람이 없습니다. 그대로 올리면 아무도 결재할 수 없습니다 — 그 역할에 사람을 넣어 주십시오.`,\n 'approval-person-missing': `${seats} 결재 자리가 지목한 사람을 찾을 수 없습니다. 그대로 올리면 아무도 결재할 수 없습니다 — 그 자리에 지금 있는 사람을 다시 지정해 주십시오.`,\n 'only-the-issuer-could-judge': `${seats} 결재 자리에 올린 사람 말고 설 사람이 없습니다. 그대로 올리면 아무도 결재할 수 없습니다 — 그 자리에 다른 결재자를 넣거나, 그 역할을 가진 사람을 늘려 주십시오.`\n }[refusal.because]\n\n return context.t(STANDING_REFUSAL_KEYS[refusal.because], { seats, defaultValue: said })\n })\n .join(' ')\n )\n }\n\n const activitySearchKeys = fillActivitySearchKeys(activity?.searchKeys, input)\n\n if (activity.startingType == 'post') {\n /* \n startingType이 'post'이면, \n activityInstance 이슈와 동시에 issuer에게 할당된 activityThread가 생성되고 submit까지 모두 완료되게 된다.\n 만일, 결재선이 있다면, 상신되게 된다.\n */\n const posted = await tx.getRepository(ActivityInstance).save({\n version: 1,\n creator: user,\n ...origin,\n ...activityInstance,\n transaction: 'post',\n activity,\n ...activitySearchKeys,\n state: ActivityInstanceStatus.Started,\n domain,\n issuer: user,\n updater: user,\n issuedAt,\n startedAt: issuedAt\n })\n\n const thread = await createActivityThreadForUser('post', posted, user, context)\n\n /* post 방식은 thread가 하나이므로, instance의 output을 그대로 thread의 output이라고 판단할 수 있다. 따라서, 아래 로직은 제거한다. */\n // const data = { ...activityInstance.input, ...activityInstance.output }\n // const output = (activity.model || [])\n // .filter(item => item.inout === 'inout' || item.inout === 'out')\n // .reduce((inout, item) => {\n // inout[item.tag] = data[item.tag]\n // return inout\n // }, {})\n\n await submit({ id: thread.id, output: activityInstance.output }, context)\n\n return await tx.getRepository(ActivityInstance).findOneBy({ id: posted.id })\n } else {\n const issued = await tx.getRepository(ActivityInstance).save({\n version: 1,\n creator: user,\n ...origin,\n ...activityInstance,\n transaction: 'issue',\n activity,\n ...activitySearchKeys,\n state: ActivityInstanceStatus.Issued,\n domain,\n issuer: user,\n updater: user,\n issuedAt\n })\n\n /*\n 1. 하나의 thread가 필요한 경우 (대부분)\n 조건 : activityInstance의 threadsMin이 1 이어야 한다.\n 로직 1 : getSystemUserFromOrgMemberItem 의 결과가 한명이라면 그대로 activityThread.assignee로 직접 할당한다.\n 로직 2 : getSystemUserFromOrgMemberItem 의 결과가 한명이 아니라면, assigneeRoles 가 있는 경우에만 처리가 가능하다.\n 이 경우에는 assignee를 비운다. - createActivityThreadWithUnassigned\n 2. 할당되지 않은 복수개의 thread가 필요한 경우\n 조건 : threadsMin이 1 이상이어야 한다.\n 로직 1 : getSystemUserFromOrgMemberItem 의 결과와 threadsMin 값이 같다면, 그대로 activityThread.assignee로 직접 할당한다. createActivityThreadsForAllRoleUsers\n 로직 2 : createActivityThreadWithUnassigned\n 3. 모든 assignee가 처리를 해야하는 경우\n 조건 : threadsMin이 0 이어야 한다.\n 로직 : getSystemUserFromOrgMemberItem 의 결과로 createActivityThreadsForUsers\n */\n\n // assignees가 복수인 것은 아무 의미가 없다.\n const assignedUsers = await Promise.all(\n (assignees || []).map(assignee => getSystemUserFromOrgMemberItem(assignee, context))\n )\n\n if (assignedUsers.length == 0) {\n if (poolIdsOf(issued).length) {\n if (issued.threadsMin === 0) {\n /* 풀의 역할들 중 하나라도 가진 사람 모두에게 — 한 사람에 한 번 */\n await createActivityThreadsForAllRoleUsers('issue', issued, context)\n } else {\n /* minimum threads 설정에 따른 pending assignment 상태로 activityThread를 만드는 경우 */\n await createActivityThreadWithUnassigned('issue', issued, context)\n }\n }\n } else {\n await createActivityThreadsForUsers('issue', issued, assignedUsers, context)\n }\n\n return await tx.getRepository(ActivityInstance).findOneBy({ id: issued.id })\n }\n}\n"]}
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.pick = pick;
4
4
  const typeorm_1 = require("typeorm");
5
+ const shell_1 = require("@things-factory/shell");
5
6
  const activity_instance_js_1 = require("../../service/activity-instance/activity-instance.js");
6
7
  const common_js_1 = require("../common.js");
7
8
  const entitlement_js_1 = require("./entitlement.js");
@@ -10,7 +11,7 @@ async function pick(id, context) {
10
11
  const repository = tx.getRepository(activity_instance_js_1.ActivityInstance);
11
12
  var activityInstance = await repository.findOne({
12
13
  where: { domain: { id: domain.id }, id },
13
- relations: ['domain', 'activity', 'assigneeRole', 'supervisoryRole', 'updater', 'creator', 'starter', 'terminator']
14
+ relations: ['domain', 'activity', 'assigneeRoles', 'supervisoryRole', 'updater', 'creator', 'starter', 'terminator']
14
15
  });
15
16
  if (!activityInstance) {
16
17
  throw new Error(context.t('error.activity-instance not found', {
@@ -20,7 +21,7 @@ async function pick(id, context) {
20
21
  /*
21
22
  누가 집을 수 있나 — 상태만 보면 **아무나 집는다.**
22
23
 
23
- `assigneeRole` 을 relations 로 불러오면서 한 번도 안 봤다. 그래서 그 도메인에 로그인한
24
+ 담당 역할을 relations 로 불러오면서 한 번도 안 봤다. 그래서 그 도메인에 로그인한
24
25
  사람이면 특정 역할에 걸어 둔 업무도 집을 수 있었다. 자격 규칙은 `entitlement.ts` 에 있다.
25
26
  */
26
27
  await (0, entitlement_js_1.assertMayPick)(activityInstance, context);
@@ -50,8 +51,26 @@ async function pick(id, context) {
50
51
  if (!claimed.affected) {
51
52
  throw new Error(context.t('error.activity-approval taken by someone else', { activityApproval: id }));
52
53
  }
54
+ /*
55
+ * Read it again under the hold, and ask again who may take it. What was read above came before the wait:
56
+ * a manager may have put the case in a role's pool meanwhile (assign.ts), and saving that old read back
57
+ * would wipe the pool out - and let someone outside the role take it.
58
+ */
59
+ const current = await repository.findOne({
60
+ where: { domain: { id: domain.id }, id },
61
+ relations: ['domain', 'activity', 'assigneeRoles', 'supervisoryRole', 'updater', 'creator', 'starter', 'terminator'],
62
+ /*
63
+ * Lock the case's row only - postgres refuses FOR UPDATE on the nullable side of the relations' outer
64
+ * joins. `tables` takes table names, not entity names (typeorm SelectQueryBuilder), so it is read off
65
+ * the metadata rather than written here; 'ActivityInstance' failed every pick on postgres.
66
+ */
67
+ ...((0, shell_1.writeLockSupported)(tx)
68
+ ? { lock: { mode: 'pessimistic_write', tables: [repository.metadata.tableNameWithoutPrefix] } }
69
+ : {})
70
+ });
71
+ await (0, entitlement_js_1.assertMayPick)(current, context);
53
72
  const picked = await repository.save({
54
- ...activityInstance,
73
+ ...current,
55
74
  transaction: 'pick',
56
75
  updater: user
57
76
  });
@@ -1 +1 @@
1
- {"version":3,"file":"pick.js","sourceRoot":"","sources":["../../../server/controllers/activity-instance/pick.ts"],"names":[],"mappings":";;AAOA,oBAoEC;AA3ED,qCAA4B;AAE5B,+FAA+G;AAE/G,4CAA4D;AAC5D,qDAAgD;AAEzC,KAAK,UAAU,IAAI,CAAC,EAAU,EAAE,OAAwB;IAC7D,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,EAAE,GAAG,OAAO,CAAC,KAAK,CAAA;IAC1C,MAAM,UAAU,GAAG,EAAE,CAAC,aAAa,CAAC,uCAAgB,CAAC,CAAA;IAErD,IAAI,gBAAgB,GAAG,MAAM,UAAU,CAAC,OAAO,CAAC;QAC9C,KAAK,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE;QACxC,SAAS,EAAE,CAAC,QAAQ,EAAE,UAAU,EAAE,cAAc,EAAE,iBAAiB,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE,YAAY,CAAC;KACpH,CAAC,CAAA;IAEF,IAAI,CAAC,gBAAgB,EAAE,CAAC;QACtB,MAAM,IAAI,KAAK,CACb,OAAO,CAAC,CAAC,CAAC,mCAAmC,EAAE;YAC7C,gBAAgB,EAAE,EAAE;SACrB,CAAC,CACH,CAAA;IACH,CAAC;IAED;;;;;MAKE;IACF,MAAM,IAAA,8BAAa,EAAC,gBAAgB,EAAE,OAAO,CAAC,CAAA;IAE9C;;;MAGE;IACF,IACE,gBAAgB,CAAC,KAAK,KAAK,6CAAsB,CAAC,MAAM;QACxD,gBAAgB,CAAC,KAAK,KAAK,6CAAsB,CAAC,UAAU,EAC5D,CAAC;QACD,MAAM,IAAI,KAAK,CACb,OAAO,CAAC,CAAC,CAAC,gDAAgD,EAAE;YAC1D,EAAE;YACF,MAAM,EAAE,gBAAgB,CAAC,KAAK;SAC/B,CAAC,CACH,CAAA;IACH,CAAC;IAED;;;;;;OAMG;IACH,MAAM,OAAO,GAAG,MAAM,UAAU,CAAC,MAAM,CACrC;QACE,MAAM,EAAE,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE;QACzB,EAAE;QACF,KAAK,EAAE,IAAA,YAAE,EAAC,CAAC,6CAAsB,CAAC,MAAM,EAAE,6CAAsB,CAAC,UAAU,CAAC,CAAC;KAC9E,EACD,EAAE,OAAO,EAAE,IAAI,EAAE,CAClB,CAAA;IAED,IAAI,CAAC,OAAO,CAAC,QAAQ,EAAE,CAAC;QACtB,MAAM,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,+CAA+C,EAAE,EAAE,gBAAgB,EAAE,EAAE,EAAE,CAAC,CAAC,CAAA;IACvG,CAAC;IAED,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC,IAAI,CAAC;QACnC,GAAG,gBAAgB;QACnB,WAAW,EAAE,MAAM;QACnB,OAAO,EAAE,IAAI;KACd,CAAC,CAAA;IAEF,OAAO,CAAC,MAAM,IAAA,yCAA6B,EAAC,MAAM,EAAE,MAAM,EAAE,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;AAClF,CAAC","sourcesContent":["import { In } from 'typeorm'\n\nimport { ActivityInstance, ActivityInstanceStatus } from '../../service/activity-instance/activity-instance.js'\nimport { ActivityThread } from '../../service/activity-thread/activity-thread.js'\nimport { createActivityThreadsForUsers } from '../common.js'\nimport { assertMayPick } from './entitlement.js'\n\nexport async function pick(id: string, context: ResolverContext): Promise<ActivityThread> {\n const { domain, user, tx } = context.state\n const repository = tx.getRepository(ActivityInstance)\n\n var activityInstance = await repository.findOne({\n where: { domain: { id: domain.id }, id },\n relations: ['domain', 'activity', 'assigneeRole', 'supervisoryRole', 'updater', 'creator', 'starter', 'terminator']\n })\n\n if (!activityInstance) {\n throw new Error(\n context.t('error.activity-instance not found', {\n activityInstance: id\n })\n )\n }\n\n /*\n 누가 집을 수 있나 — 상태만 보면 **아무나 집는다.**\n\n `assigneeRole` 을 relations 로 불러오면서 한 번도 안 봤다. 그래서 그 도메인에 로그인한\n 사람이면 특정 역할에 걸어 둔 업무도 집을 수 있었다. 자격 규칙은 `entitlement.ts` 에 있다.\n */\n await assertMayPick(activityInstance, context)\n\n /* \n Prerequisites for a Task to Be Picked.\n - The previous state of the task must not be Assigned.\n */\n if (\n activityInstance.state !== ActivityInstanceStatus.Issued &&\n activityInstance.state !== ActivityInstanceStatus.Unassigned\n ) {\n throw new Error(\n context.t(`error.activity-instance should not be assigned`, {\n id,\n actual: activityInstance.state\n })\n )\n }\n\n /*\n * Take hold of it before making a thread. Measured on postgres (tests/pick-race.pg.test.ts): two people\n * picking a case that waits for one both read it as issued and both got a thread. The UPDATE matches\n * only a case still waiting and holds its row to the end of this transaction; whoever is second waits,\n * then finds it assigned and is refused by name. A case waiting for more than one person is still\n * unassigned after the first pick, so the second person gets in - as intended.\n */\n const claimed = await repository.update(\n {\n domain: { id: domain.id },\n id,\n state: In([ActivityInstanceStatus.Issued, ActivityInstanceStatus.Unassigned])\n },\n { updater: user }\n )\n\n if (!claimed.affected) {\n throw new Error(context.t('error.activity-approval taken by someone else', { activityApproval: id }))\n }\n\n const picked = await repository.save({\n ...activityInstance,\n transaction: 'pick',\n updater: user\n })\n\n return (await createActivityThreadsForUsers('pick', picked, [user], context))[0]\n}\n"]}
1
+ {"version":3,"file":"pick.js","sourceRoot":"","sources":["../../../server/controllers/activity-instance/pick.ts"],"names":[],"mappings":"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","sourcesContent":["import { In } from 'typeorm'\n\nimport { writeLockSupported } from '@things-factory/shell'\n\nimport { ActivityInstance, ActivityInstanceStatus } from '../../service/activity-instance/activity-instance.js'\nimport { ActivityThread } from '../../service/activity-thread/activity-thread.js'\nimport { createActivityThreadsForUsers } from '../common.js'\nimport { assertMayPick } from './entitlement.js'\n\nexport async function pick(id: string, context: ResolverContext): Promise<ActivityThread> {\n const { domain, user, tx } = context.state\n const repository = tx.getRepository(ActivityInstance)\n\n var activityInstance = await repository.findOne({\n where: { domain: { id: domain.id }, id },\n relations: ['domain', 'activity', 'assigneeRoles', 'supervisoryRole', 'updater', 'creator', 'starter', 'terminator']\n })\n\n if (!activityInstance) {\n throw new Error(\n context.t('error.activity-instance not found', {\n activityInstance: id\n })\n )\n }\n\n /*\n 누가 집을 수 있나 — 상태만 보면 **아무나 집는다.**\n\n 담당 역할을 relations 로 불러오면서 한 번도 안 봤다. 그래서 그 도메인에 로그인한\n 사람이면 특정 역할에 걸어 둔 업무도 집을 수 있었다. 자격 규칙은 `entitlement.ts` 에 있다.\n */\n await assertMayPick(activityInstance, context)\n\n /* \n Prerequisites for a Task to Be Picked.\n - The previous state of the task must not be Assigned.\n */\n if (\n activityInstance.state !== ActivityInstanceStatus.Issued &&\n activityInstance.state !== ActivityInstanceStatus.Unassigned\n ) {\n throw new Error(\n context.t(`error.activity-instance should not be assigned`, {\n id,\n actual: activityInstance.state\n })\n )\n }\n\n /*\n * Take hold of it before making a thread. Measured on postgres (tests/pick-race.pg.test.ts): two people\n * picking a case that waits for one both read it as issued and both got a thread. The UPDATE matches\n * only a case still waiting and holds its row to the end of this transaction; whoever is second waits,\n * then finds it assigned and is refused by name. A case waiting for more than one person is still\n * unassigned after the first pick, so the second person gets in - as intended.\n */\n const claimed = await repository.update(\n {\n domain: { id: domain.id },\n id,\n state: In([ActivityInstanceStatus.Issued, ActivityInstanceStatus.Unassigned])\n },\n { updater: user }\n )\n\n if (!claimed.affected) {\n throw new Error(context.t('error.activity-approval taken by someone else', { activityApproval: id }))\n }\n\n /*\n * Read it again under the hold, and ask again who may take it. What was read above came before the wait:\n * a manager may have put the case in a role's pool meanwhile (assign.ts), and saving that old read back\n * would wipe the pool out - and let someone outside the role take it.\n */\n const current = await repository.findOne({\n where: { domain: { id: domain.id }, id },\n relations: ['domain', 'activity', 'assigneeRoles', 'supervisoryRole', 'updater', 'creator', 'starter', 'terminator'],\n /*\n * Lock the case's row only - postgres refuses FOR UPDATE on the nullable side of the relations' outer\n * joins. `tables` takes table names, not entity names (typeorm SelectQueryBuilder), so it is read off\n * the metadata rather than written here; 'ActivityInstance' failed every pick on postgres.\n */\n ...(writeLockSupported(tx)\n ? { lock: { mode: 'pessimistic_write' as const, tables: [repository.metadata.tableNameWithoutPrefix] } }\n : {})\n })\n\n await assertMayPick(current, context)\n\n const picked = await repository.save({\n ...current,\n transaction: 'pick',\n updater: user\n })\n\n return (await createActivityThreadsForUsers('pick', picked, [user], context))[0]\n}\n"]}
@@ -9,6 +9,7 @@ const typeorm_1 = require("typeorm");
9
9
  const shell_1 = require("@things-factory/shell");
10
10
  const activity_approval_js_1 = require("../../service/activity-approval/activity-approval.js");
11
11
  const activity_instance_js_1 = require("../../service/activity-instance/activity-instance.js");
12
+ const assignee_pool_js_1 = require("../../service/activity-instance/assignee-pool.js");
12
13
  const activity_thread_js_1 = require("../../service/activity-thread/activity-thread.js");
13
14
  const participates_js_1 = require("./participates.js");
14
15
  /** Whoever may read every instance may read this one. */
@@ -47,7 +48,7 @@ async function factsOfInstance(instance) {
47
48
  return {
48
49
  domainId: instance.domainId,
49
50
  assigneeIds: threads.map(one => one.assigneeId),
50
- waitingRoleId: instance.assigneeRoleId,
51
+ waitingRoleIds: (0, assignee_pool_js_1.poolIdsOf)(instance),
51
52
  issuerId: instance.issuerId,
52
53
  creatorId: instance.creatorId,
53
54
  starterId: instance.starterId,
@@ -1 +1 @@
1
- {"version":3,"file":"assert-may-read.js","sourceRoot":"","sources":["../../../server/controllers/activity-read/assert-may-read.ts"],"names":[],"mappings":";;AAiFA,0CAEC;AASD,sDAKC;AAGD,kDAKC;AAGD,sDAcC;AA1HD,yDAAiE;AACjE,qCAA4B;AAE5B,iDAAqD;AAErD,+FAAuF;AACvF,+FAAuF;AACvF,yFAAiF;AACjF,uDAA2D;AAE3D,yDAAyD;AACzD,KAAK,UAAU,cAAc,CAAC,OAAwB;IACpD,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,oBAAoB,EAAE,GAAG,OAAO,CAAC,KAAK,CAAA;IAEtE,OAAO,MAAM,IAAA,2BAAe,EAC1B,EAAE,QAAQ,EAAE,mBAAmB,EAAE,SAAS,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,EAClE,IAAI,EACJ,MAAM,EACN,QAAQ,EACR,oBAAoB,CACrB,CAAA;AACH,CAAC;AAED,KAAK,UAAU,OAAO,CAAC,MAAsB;IAC3C,MAAM,QAAQ,GAAG,MAAM,CAAC,kBAAkB;QACxC,CAAC,CAAC,MAAM,IAAA,qBAAa,EAAC,uCAAgB,CAAC,CAAC,OAAO,CAAC;YAC5C,KAAK,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,MAAM,CAAC,QAAQ,EAAE,EAAE,EAAE,EAAE,MAAM,CAAC,kBAAkB,EAAE;SAC1E,CAAC;QACJ,CAAC,CAAC,IAAI,CAAA;IAER,MAAM,SAAS,GAAG,MAAM,IAAA,qBAAa,EAAC,uCAAgB,CAAC,CAAC,IAAI,CAAC;QAC3D,KAAK,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,MAAM,CAAC,QAAQ,EAAE,EAAE,cAAc,EAAE,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE,EAAE;KAC9E,CAAC,CAAA;IAEF,OAAO;QACL,QAAQ,EAAE,MAAM,CAAC,QAAQ;QACzB,WAAW,EAAE,CAAC,MAAM,CAAC,UAAU,CAAC;QAChC,QAAQ,EAAE,QAAQ,EAAE,QAAQ;QAC5B,SAAS,EAAE,QAAQ,EAAE,SAAS;QAC9B,SAAS,EAAE,QAAQ,EAAE,SAAS;QAC9B,iBAAiB,EAAE,QAAQ,EAAE,iBAAiB;QAC9C,SAAS,EAAE,SAAS,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,EAAE,UAAU,EAAE,GAAG,CAAC,UAAU,EAAE,cAAc,EAAE,GAAG,CAAC,cAAc,EAAE,CAAC,CAAC;KACtG,CAAA;AACH,CAAC;AAED,KAAK,UAAU,eAAe,CAAC,QAA0B;IACvD,MAAM,OAAO,GAAG,MAAM,IAAA,qBAAa,EAAC,mCAAc,CAAC,CAAC,IAAI,CAAC;QACvD,KAAK,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,QAAQ,CAAC,QAAQ,EAAE,EAAE,gBAAgB,EAAE,EAAE,EAAE,EAAE,QAAQ,CAAC,EAAE,EAAE,EAAE;KACpF,CAAC,CAAA;IAEF,MAAM,SAAS,GAAG,OAAO,CAAC,MAAM;QAC9B,CAAC,CAAC,MAAM,IAAA,qBAAa,EAAC,uCAAgB,CAAC,CAAC,IAAI,CAAC;YACzC,KAAK,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,QAAQ,CAAC,QAAQ,EAAE,EAAE,cAAc,EAAE,EAAE,EAAE,EAAE,IAAA,YAAE,EAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE;SACrG,CAAC;QACJ,CAAC,CAAC,EAAE,CAAA;IAEN,OAAO;QACL,QAAQ,EAAE,QAAQ,CAAC,QAAQ;QAC3B,WAAW,EAAE,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,UAAU,CAAC;QAC/C,aAAa,EAAE,QAAQ,CAAC,cAAc;QACtC,QAAQ,EAAE,QAAQ,CAAC,QAAQ;QAC3B,SAAS,EAAE,QAAQ,CAAC,SAAS;QAC7B,SAAS,EAAE,QAAQ,CAAC,SAAS;QAC7B,iBAAiB,EAAE,QAAQ,CAAC,iBAAiB;QAC7C,SAAS,EAAE,SAAS,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,EAAE,UAAU,EAAE,GAAG,CAAC,UAAU,EAAE,cAAc,EAAE,GAAG,CAAC,cAAc,EAAE,CAAC,CAAC;KACtG,CAAA;AACH,CAAC;AAED,KAAK,UAAU,QAAQ,CAAC,OAAwB;IAC9C,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,OAAO,CAAC,KAAK,CAAA;IAEtC,MAAM,SAAS,GAAG,MAAM,IAAA,qBAAa,EAAC,gBAAI,CAAC,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE,IAAI,CAAC,EAAE,EAAE,EAAE,SAAS,EAAE,CAAC,OAAO,CAAC,EAAE,CAAC,CAAA;IACrG,MAAM,OAAO,GAAG,CAAC,SAAS,EAAE,KAAK,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,QAAQ,KAAK,MAAM,CAAC,EAAE,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;IAEzG,OAAO,EAAE,QAAQ,EAAE,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,EAAE,OAAO,EAAE,CAAA;AAC1D,CAAC;AAED;;;GAGG;AACI,KAAK,UAAU,eAAe,CAAC,QAA0B,EAAE,OAAwB;IACxF,OAAO,IAAA,8BAAY,EAAC,MAAM,eAAe,CAAC,QAAQ,CAAC,EAAE,MAAM,QAAQ,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,MAAM,cAAc,CAAC,OAAO,CAAC,CAAC,CAAA;AAClH,CAAC;AAED;;;;;;GAMG;AACI,KAAK,UAAU,qBAAqB,CAAC,QAA0B,EAAE,OAAwB;IAC9F,IAAI,IAAA,8BAAY,EAAC,MAAM,eAAe,CAAC,QAAQ,CAAC,EAAE,MAAM,QAAQ,CAAC,OAAO,CAAC,CAAC;QAAE,OAAM;IAClF,IAAI,MAAM,cAAc,CAAC,OAAO,CAAC;QAAE,OAAM;IAEzC,MAAM,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,uCAAuC,CAAC,CAAC,CAAA;AACrE,CAAC;AAED,8FAA8F;AACvF,KAAK,UAAU,mBAAmB,CAAC,MAAsB,EAAE,OAAwB;IACxF,IAAI,IAAA,8BAAY,EAAC,MAAM,OAAO,CAAC,MAAM,CAAC,EAAE,MAAM,QAAQ,CAAC,OAAO,CAAC,CAAC;QAAE,OAAM;IACxE,IAAI,MAAM,cAAc,CAAC,OAAO,CAAC;QAAE,OAAM;IAEzC,MAAM,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,uCAAuC,CAAC,CAAC,CAAA;AACrE,CAAC;AAED,iFAAiF;AAC1E,KAAK,UAAU,qBAAqB,CAAC,QAA0B,EAAE,OAAwB;IAC9F,MAAM,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,KAAK,CAAA;IAEhC,MAAM,MAAM,GAAG,QAAQ,CAAC,gBAAgB;QACtC,CAAC,CAAC,MAAM,IAAA,qBAAa,EAAC,mCAAc,CAAC,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,QAAQ,CAAC,gBAAgB,EAAE,EAAE,CAAC;QACtH,CAAC,CAAC,IAAI,CAAA;IAER,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,8EAA8E;QAC9E,IAAI,MAAM,cAAc,CAAC,OAAO,CAAC;YAAE,OAAM;QACzC,MAAM,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,uCAAuC,CAAC,CAAC,CAAA;IACrE,CAAC;IAED,MAAM,mBAAmB,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;AAC5C,CAAC","sourcesContent":["import { checkPermission, User } from '@things-factory/auth-base'\nimport { In } from 'typeorm'\n\nimport { getRepository } from '@things-factory/shell'\n\nimport { ActivityApproval } from '../../service/activity-approval/activity-approval.js'\nimport { ActivityInstance } from '../../service/activity-instance/activity-instance.js'\nimport { ActivityThread } from '../../service/activity-thread/activity-thread.js'\nimport { CaseFacts, participates } from './participates.js'\n\n/** Whoever may read every instance may read this one. */\nasync function readsEveryCase(context: ResolverContext): Promise<boolean> {\n const { domain, user, unsafeIP, prohibitedPrivileges } = context.state\n\n return await checkPermission(\n { category: 'activity-instance', privilege: 'query', owner: true },\n user,\n domain,\n unsafeIP,\n prohibitedPrivileges\n )\n}\n\nasync function factsOf(thread: ActivityThread): Promise<CaseFacts> {\n const instance = thread.activityInstanceId\n ? await getRepository(ActivityInstance).findOne({\n where: { domain: { id: thread.domainId }, id: thread.activityInstanceId }\n })\n : null\n\n const approvals = await getRepository(ActivityApproval).find({\n where: { domain: { id: thread.domainId }, activityThread: { id: thread.id } }\n })\n\n return {\n domainId: thread.domainId,\n assigneeIds: [thread.assigneeId],\n issuerId: instance?.issuerId,\n creatorId: instance?.creatorId,\n starterId: instance?.starterId,\n supervisoryRoleId: instance?.supervisoryRoleId,\n approvals: approvals.map(one => ({ approverId: one.approverId, approverRoleId: one.approverRoleId }))\n }\n}\n\nasync function factsOfInstance(instance: ActivityInstance): Promise<CaseFacts> {\n const threads = await getRepository(ActivityThread).find({\n where: { domain: { id: instance.domainId }, activityInstance: { id: instance.id } }\n })\n\n const approvals = threads.length\n ? await getRepository(ActivityApproval).find({\n where: { domain: { id: instance.domainId }, activityThread: { id: In(threads.map(one => one.id)) } }\n })\n : []\n\n return {\n domainId: instance.domainId,\n assigneeIds: threads.map(one => one.assigneeId),\n waitingRoleId: instance.assigneeRoleId,\n issuerId: instance.issuerId,\n creatorId: instance.creatorId,\n starterId: instance.starterId,\n supervisoryRoleId: instance.supervisoryRoleId,\n approvals: approvals.map(one => ({ approverId: one.approverId, approverRoleId: one.approverRoleId }))\n }\n}\n\nasync function readerOf(context: ResolverContext) {\n const { domain, user } = context.state\n\n const withRoles = await getRepository(User).findOne({ where: { id: user.id }, relations: ['roles'] })\n const roleIds = (withRoles?.roles || []).filter(role => role.domainId === domain.id).map(role => role.id)\n\n return { domainId: domain.id, userId: user.id, roleIds }\n}\n\n/**\n * Whether the caller takes part in this instance or reads every case - for a module whose record hangs on a case and\n * opens to the same people (an expense's receipt to its approvers, ADR-0139).\n */\nexport async function mayReadInstance(instance: ActivityInstance, context: ResolverContext): Promise<boolean> {\n return participates(await factsOfInstance(instance), await readerOf(context)) || (await readsEveryCase(context))\n}\n\n/**\n * Throws a named refusal unless the caller takes part in this instance or reads every case.\n *\n * The draft screen reopens an instance by id. Four of plant's working roles (OPERATOR and the other\n * hands-on roles) hold no activity-instance query privilege, so while this door asked it they could\n * write a draft, see it under \"raised by me\", and not open it again.\n */\nexport async function assertMayReadInstance(instance: ActivityInstance, context: ResolverContext): Promise<void> {\n if (participates(await factsOfInstance(instance), await readerOf(context))) return\n if (await readsEveryCase(context)) return\n\n throw new Error(context.t('error.activity-case not a participant'))\n}\n\n/** Throws a named refusal unless the caller takes part in this thread or reads every case. */\nexport async function assertMayReadThread(thread: ActivityThread, context: ResolverContext): Promise<void> {\n if (participates(await factsOf(thread), await readerOf(context))) return\n if (await readsEveryCase(context)) return\n\n throw new Error(context.t('error.activity-case not a participant'))\n}\n\n/** An approval is read through its thread: the same people, the same refusal. */\nexport async function assertMayReadApproval(approval: ActivityApproval, context: ResolverContext): Promise<void> {\n const { domain } = context.state\n\n const thread = approval.activityThreadId\n ? await getRepository(ActivityThread).findOne({ where: { domain: { id: domain.id }, id: approval.activityThreadId } })\n : null\n\n if (!thread) {\n /* An approval with no thread in this domain has nobody to take part in it. */\n if (await readsEveryCase(context)) return\n throw new Error(context.t('error.activity-case not a participant'))\n }\n\n await assertMayReadThread(thread, context)\n}\n"]}
1
+ {"version":3,"file":"assert-may-read.js","sourceRoot":"","sources":["../../../server/controllers/activity-read/assert-may-read.ts"],"names":[],"mappings":"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","sourcesContent":["import { checkPermission, User } from '@things-factory/auth-base'\nimport { In } from 'typeorm'\n\nimport { getRepository } from '@things-factory/shell'\n\nimport { ActivityApproval } from '../../service/activity-approval/activity-approval.js'\nimport { ActivityInstance } from '../../service/activity-instance/activity-instance.js'\nimport { poolIdsOf } from '../../service/activity-instance/assignee-pool.js'\nimport { ActivityThread } from '../../service/activity-thread/activity-thread.js'\nimport { CaseFacts, participates } from './participates.js'\n\n/** Whoever may read every instance may read this one. */\nasync function readsEveryCase(context: ResolverContext): Promise<boolean> {\n const { domain, user, unsafeIP, prohibitedPrivileges } = context.state\n\n return await checkPermission(\n { category: 'activity-instance', privilege: 'query', owner: true },\n user,\n domain,\n unsafeIP,\n prohibitedPrivileges\n )\n}\n\nasync function factsOf(thread: ActivityThread): Promise<CaseFacts> {\n const instance = thread.activityInstanceId\n ? await getRepository(ActivityInstance).findOne({\n where: { domain: { id: thread.domainId }, id: thread.activityInstanceId }\n })\n : null\n\n const approvals = await getRepository(ActivityApproval).find({\n where: { domain: { id: thread.domainId }, activityThread: { id: thread.id } }\n })\n\n return {\n domainId: thread.domainId,\n assigneeIds: [thread.assigneeId],\n issuerId: instance?.issuerId,\n creatorId: instance?.creatorId,\n starterId: instance?.starterId,\n supervisoryRoleId: instance?.supervisoryRoleId,\n approvals: approvals.map(one => ({ approverId: one.approverId, approverRoleId: one.approverRoleId }))\n }\n}\n\nasync function factsOfInstance(instance: ActivityInstance): Promise<CaseFacts> {\n const threads = await getRepository(ActivityThread).find({\n where: { domain: { id: instance.domainId }, activityInstance: { id: instance.id } }\n })\n\n const approvals = threads.length\n ? await getRepository(ActivityApproval).find({\n where: { domain: { id: instance.domainId }, activityThread: { id: In(threads.map(one => one.id)) } }\n })\n : []\n\n return {\n domainId: instance.domainId,\n assigneeIds: threads.map(one => one.assigneeId),\n waitingRoleIds: poolIdsOf(instance),\n issuerId: instance.issuerId,\n creatorId: instance.creatorId,\n starterId: instance.starterId,\n supervisoryRoleId: instance.supervisoryRoleId,\n approvals: approvals.map(one => ({ approverId: one.approverId, approverRoleId: one.approverRoleId }))\n }\n}\n\nasync function readerOf(context: ResolverContext) {\n const { domain, user } = context.state\n\n const withRoles = await getRepository(User).findOne({ where: { id: user.id }, relations: ['roles'] })\n const roleIds = (withRoles?.roles || []).filter(role => role.domainId === domain.id).map(role => role.id)\n\n return { domainId: domain.id, userId: user.id, roleIds }\n}\n\n/**\n * Whether the caller takes part in this instance or reads every case - for a module whose record hangs on a case and\n * opens to the same people (an expense's receipt to its approvers, ADR-0139).\n */\nexport async function mayReadInstance(instance: ActivityInstance, context: ResolverContext): Promise<boolean> {\n return participates(await factsOfInstance(instance), await readerOf(context)) || (await readsEveryCase(context))\n}\n\n/**\n * Throws a named refusal unless the caller takes part in this instance or reads every case.\n *\n * The draft screen reopens an instance by id. Four of plant's working roles (OPERATOR and the other\n * hands-on roles) hold no activity-instance query privilege, so while this door asked it they could\n * write a draft, see it under \"raised by me\", and not open it again.\n */\nexport async function assertMayReadInstance(instance: ActivityInstance, context: ResolverContext): Promise<void> {\n if (participates(await factsOfInstance(instance), await readerOf(context))) return\n if (await readsEveryCase(context)) return\n\n throw new Error(context.t('error.activity-case not a participant'))\n}\n\n/** Throws a named refusal unless the caller takes part in this thread or reads every case. */\nexport async function assertMayReadThread(thread: ActivityThread, context: ResolverContext): Promise<void> {\n if (participates(await factsOf(thread), await readerOf(context))) return\n if (await readsEveryCase(context)) return\n\n throw new Error(context.t('error.activity-case not a participant'))\n}\n\n/** An approval is read through its thread: the same people, the same refusal. */\nexport async function assertMayReadApproval(approval: ActivityApproval, context: ResolverContext): Promise<void> {\n const { domain } = context.state\n\n const thread = approval.activityThreadId\n ? await getRepository(ActivityThread).findOne({ where: { domain: { id: domain.id }, id: approval.activityThreadId } })\n : null\n\n if (!thread) {\n /* An approval with no thread in this domain has nobody to take part in it. */\n if (await readsEveryCase(context)) return\n throw new Error(context.t('error.activity-case not a participant'))\n }\n\n await assertMayReadThread(thread, context)\n}\n"]}
@@ -10,7 +10,7 @@
10
10
  * the assignee of the thread (of any thread, when the case is the instance)
11
11
  * whoever raised the instance, wrote it, or started it
12
12
  * a holder of the instance's supervisory role
13
- * a holder of the role the instance waits for - they have to see it to pick it up (instance only)
13
+ * a holder of any one of the roles the instance waits for - they have to see it to pick it up (instance only)
14
14
  * every approver on the thread(s) - by person or by role, references included, and a delegator
15
15
  * keeps the row that was delegated
16
16
  *
@@ -23,7 +23,7 @@ export declare const NOT_A_PARTICIPANT = "not-a-participant";
23
23
  export type CaseFacts = {
24
24
  domainId?: string;
25
25
  assigneeIds: (string | undefined)[];
26
- waitingRoleId?: string;
26
+ waitingRoleIds?: readonly string[];
27
27
  issuerId?: string;
28
28
  creatorId?: string;
29
29
  starterId?: string;
@@ -11,7 +11,7 @@
11
11
  * the assignee of the thread (of any thread, when the case is the instance)
12
12
  * whoever raised the instance, wrote it, or started it
13
13
  * a holder of the instance's supervisory role
14
- * a holder of the role the instance waits for - they have to see it to pick it up (instance only)
14
+ * a holder of any one of the roles the instance waits for - they have to see it to pick it up (instance only)
15
15
  * every approver on the thread(s) - by person or by role, references included, and a delegator
16
16
  * keeps the row that was delegated
17
17
  *
@@ -35,7 +35,7 @@ function participates(facts, reader) {
35
35
  const holds = (roleId) => !!roleId && roles.has(roleId);
36
36
  if ([...(facts.assigneeIds || []), facts.issuerId, facts.creatorId, facts.starterId].includes(userId))
37
37
  return true;
38
- if (holds(facts.supervisoryRoleId) || holds(facts.waitingRoleId))
38
+ if (holds(facts.supervisoryRoleId) || (facts.waitingRoleIds || []).some(holds))
39
39
  return true;
40
40
  return (facts.approvals || []).some(one => one.approverId === userId || holds(one.approverRoleId));
41
41
  }
@@ -1 +1 @@
1
- {"version":3,"file":"participates.js","sourceRoot":"","sources":["../../../server/controllers/activity-read/participates.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;;;AAiBH,oCAcC;AA7BY,QAAA,iBAAiB,GAAG,mBAAmB,CAAA;AAepD,SAAgB,YAAY,CAAC,KAAgB,EAAE,MAAc;IAC3D,IAAI,CAAC,KAAK,EAAE,QAAQ,IAAI,CAAC,MAAM,EAAE,QAAQ,IAAI,KAAK,CAAC,QAAQ,KAAK,MAAM,CAAC,QAAQ;QAAE,OAAO,KAAK,CAAA;IAE7F,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,CAAA;IACzB,sGAAsG;IACtG,IAAI,CAAC,MAAM;QAAE,OAAO,KAAK,CAAA;IAEzB,MAAM,KAAK,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA;IACrC,MAAM,KAAK,GAAG,CAAC,MAAe,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,IAAI,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,CAAA;IAEhE,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,WAAW,IAAI,EAAE,CAAC,EAAE,KAAK,CAAC,QAAQ,EAAE,KAAK,CAAC,SAAS,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC;QAAE,OAAO,IAAI,CAAA;IAClH,IAAI,KAAK,CAAC,KAAK,CAAC,iBAAiB,CAAC,IAAI,KAAK,CAAC,KAAK,CAAC,aAAa,CAAC;QAAE,OAAO,IAAI,CAAA;IAE7E,OAAO,CAAC,KAAK,CAAC,SAAS,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,UAAU,KAAK,MAAM,IAAI,KAAK,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC,CAAA;AACpG,CAAC","sourcesContent":["/**\n * Who may read one case - one instance, one thread, or one approval on it - by its id.\n *\n * The lists of every thread and every approval ask the activity-instance query privilege. Reading\n * one by id cannot ask only that: the thread screen and the approval screen open the case for the\n * people working on it, and most of them hold no management privilege. So the body decides.\n *\n * The domain comes first. Then the people the case is about:\n *\n * the assignee of the thread (of any thread, when the case is the instance)\n * whoever raised the instance, wrote it, or started it\n * a holder of the instance's supervisory role\n * a holder of the role the instance waits for - they have to see it to pick it up (instance only)\n * every approver on the thread(s) - by person or by role, references included, and a delegator\n * keeps the row that was delegated\n *\n * Anyone else is refused by name (NOT_A_PARTICIPANT). A null would draw as \"no such case\" on the\n * approval screen, which is untrue and sends the reader looking for something that exists.\n *\n * Kept free of entities and repositories so the rule can be tested on its own.\n */\n\nexport const NOT_A_PARTICIPANT = 'not-a-participant'\n\nexport type CaseFacts = {\n domainId?: string\n assigneeIds: (string | undefined)[]\n waitingRoleId?: string\n issuerId?: string\n creatorId?: string\n starterId?: string\n supervisoryRoleId?: string\n approvals: { approverId?: string; approverRoleId?: string }[]\n}\n\nexport type Reader = { domainId?: string; userId: string; roleIds: readonly string[] }\n\nexport function participates(facts: CaseFacts, reader: Reader): boolean {\n if (!facts?.domainId || !reader?.domainId || facts.domainId !== reader.domainId) return false\n\n const { userId } = reader\n /* An empty id on either side is nobody - an unassigned thread must not match a reader without one. */\n if (!userId) return false\n\n const roles = new Set(reader.roleIds)\n const holds = (roleId?: string) => !!roleId && roles.has(roleId)\n\n if ([...(facts.assigneeIds || []), facts.issuerId, facts.creatorId, facts.starterId].includes(userId)) return true\n if (holds(facts.supervisoryRoleId) || holds(facts.waitingRoleId)) return true\n\n return (facts.approvals || []).some(one => one.approverId === userId || holds(one.approverRoleId))\n}\n"]}
1
+ {"version":3,"file":"participates.js","sourceRoot":"","sources":["../../../server/controllers/activity-read/participates.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;;;AAiBH,oCAcC;AA7BY,QAAA,iBAAiB,GAAG,mBAAmB,CAAA;AAepD,SAAgB,YAAY,CAAC,KAAgB,EAAE,MAAc;IAC3D,IAAI,CAAC,KAAK,EAAE,QAAQ,IAAI,CAAC,MAAM,EAAE,QAAQ,IAAI,KAAK,CAAC,QAAQ,KAAK,MAAM,CAAC,QAAQ;QAAE,OAAO,KAAK,CAAA;IAE7F,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,CAAA;IACzB,sGAAsG;IACtG,IAAI,CAAC,MAAM;QAAE,OAAO,KAAK,CAAA;IAEzB,MAAM,KAAK,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA;IACrC,MAAM,KAAK,GAAG,CAAC,MAAe,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,IAAI,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,CAAA;IAEhE,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,WAAW,IAAI,EAAE,CAAC,EAAE,KAAK,CAAC,QAAQ,EAAE,KAAK,CAAC,SAAS,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC;QAAE,OAAO,IAAI,CAAA;IAClH,IAAI,KAAK,CAAC,KAAK,CAAC,iBAAiB,CAAC,IAAI,CAAC,KAAK,CAAC,cAAc,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,IAAI,CAAA;IAE3F,OAAO,CAAC,KAAK,CAAC,SAAS,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,UAAU,KAAK,MAAM,IAAI,KAAK,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC,CAAA;AACpG,CAAC","sourcesContent":["/**\n * Who may read one case - one instance, one thread, or one approval on it - by its id.\n *\n * The lists of every thread and every approval ask the activity-instance query privilege. Reading\n * one by id cannot ask only that: the thread screen and the approval screen open the case for the\n * people working on it, and most of them hold no management privilege. So the body decides.\n *\n * The domain comes first. Then the people the case is about:\n *\n * the assignee of the thread (of any thread, when the case is the instance)\n * whoever raised the instance, wrote it, or started it\n * a holder of the instance's supervisory role\n * a holder of any one of the roles the instance waits for - they have to see it to pick it up (instance only)\n * every approver on the thread(s) - by person or by role, references included, and a delegator\n * keeps the row that was delegated\n *\n * Anyone else is refused by name (NOT_A_PARTICIPANT). A null would draw as \"no such case\" on the\n * approval screen, which is untrue and sends the reader looking for something that exists.\n *\n * Kept free of entities and repositories so the rule can be tested on its own.\n */\n\nexport const NOT_A_PARTICIPANT = 'not-a-participant'\n\nexport type CaseFacts = {\n domainId?: string\n assigneeIds: (string | undefined)[]\n waitingRoleIds?: readonly string[]\n issuerId?: string\n creatorId?: string\n starterId?: string\n supervisoryRoleId?: string\n approvals: { approverId?: string; approverRoleId?: string }[]\n}\n\nexport type Reader = { domainId?: string; userId: string; roleIds: readonly string[] }\n\nexport function participates(facts: CaseFacts, reader: Reader): boolean {\n if (!facts?.domainId || !reader?.domainId || facts.domainId !== reader.domainId) return false\n\n const { userId } = reader\n /* An empty id on either side is nobody - an unassigned thread must not match a reader without one. */\n if (!userId) return false\n\n const roles = new Set(reader.roleIds)\n const holds = (roleId?: string) => !!roleId && roles.has(roleId)\n\n if ([...(facts.assigneeIds || []), facts.issuerId, facts.creatorId, facts.starterId].includes(userId)) return true\n if (holds(facts.supervisoryRoleId) || (facts.waitingRoleIds || []).some(holds)) return true\n\n return (facts.approvals || []).some(one => one.approverId === userId || holds(one.approverRoleId))\n}\n"]}
@@ -5,6 +5,7 @@ const shell_1 = require("@things-factory/shell");
5
5
  const auth_base_1 = require("@things-factory/auth-base");
6
6
  const activity_thread_js_1 = require("../../service/activity-thread/activity-thread.js");
7
7
  const activity_instance_js_1 = require("../../service/activity-instance/activity-instance.js");
8
+ const assignee_pool_js_1 = require("../../service/activity-instance/assignee-pool.js");
8
9
  async function checkAuthority(activityThread, context) {
9
10
  const { user, tx } = context.state;
10
11
  if (activityThread.assigneeId && activityThread.assigneeId === user.id) {
@@ -12,15 +13,32 @@ async function checkAuthority(activityThread, context) {
12
13
  }
13
14
  // TODO activityInstance의 cardinality를 기준으로 unassigned thread를 추가로 만들 지, assigned thread로 대체할 지를 구현해야한다.
14
15
  if (activityThread.state == activity_thread_js_1.ActivityThreadStatus.Unassigned) {
15
- const activityInstance = await (0, shell_1.getRepository)(activity_instance_js_1.ActivityInstance, tx).findOneBy({
16
- id: activityThread.activityInstanceId
16
+ /*
17
+ * Claiming the pool's waiting thread. The case is held first - the order pick and a pool assign take
18
+ * (activity-instance/pick.ts, assign.ts) - and the thread is read again under it. The thread handed in
19
+ * was read before anything was held: someone else may have claimed it since, or a manager moved the
20
+ * case to another role's pool, and what this claims must be what is there now.
21
+ */
22
+ const activityInstance = await (0, shell_1.getRepository)(activity_instance_js_1.ActivityInstance, tx).findOne({
23
+ where: { id: activityThread.activityInstanceId },
24
+ ...((0, shell_1.writeLockSupported)(tx) ? { lock: { mode: 'pessimistic_write' } } : {})
17
25
  });
18
- if (activityInstance && activityInstance.assigneeRoleId) {
26
+ /* A locking read is a current read - a plain one may answer from the snapshot taken before the wait. */
27
+ const now = await (0, shell_1.getRepository)(activity_thread_js_1.ActivityThread, tx).findOne({
28
+ where: { id: activityThread.id },
29
+ ...((0, shell_1.writeLockSupported)(tx) ? { lock: { mode: 'pessimistic_write' } } : {})
30
+ });
31
+ if (now?.state !== activity_thread_js_1.ActivityThreadStatus.Unassigned) {
32
+ throw new Error(context.t('error.activity-approval taken by someone else', { activityApproval: activityThread.id }));
33
+ }
34
+ const poolIds = (0, assignee_pool_js_1.poolIdsOf)(activityInstance);
35
+ if (poolIds.length) {
19
36
  const roles = (await (0, shell_1.getRepository)(auth_base_1.User, tx).findOne({
20
37
  where: { id: user.id },
21
38
  relations: ['roles']
22
39
  }))?.roles;
23
- if (roles.find(role => role.id == activityInstance.assigneeRoleId)) {
40
+ /* Any one of the pool's roles is enough (ADR-0091 decision 9). */
41
+ if ((0, assignee_pool_js_1.holdsAnyOf)((roles || []).map(role => role.id), poolIds)) {
24
42
  activityThread.assignee = user;
25
43
  activityThread.state = activity_thread_js_1.ActivityThreadStatus.Assigned;
26
44
  return;
@@ -1 +1 @@
1
- {"version":3,"file":"check-authority.js","sourceRoot":"","sources":["../../../server/controllers/activity-thread/check-authority.ts"],"names":[],"mappings":";;AAKA,wCA+BC;AApCD,iDAAqD;AACrD,yDAAgD;AAChD,yFAAuG;AACvG,+FAAuF;AAEhF,KAAK,UAAU,cAAc,CAAC,cAA8B,EAAE,OAAwB;IAC3F,MAAM,EAAE,IAAI,EAAE,EAAE,EAAE,GAAG,OAAO,CAAC,KAAK,CAAA;IAElC,IAAI,cAAc,CAAC,UAAU,IAAI,cAAc,CAAC,UAAU,KAAK,IAAI,CAAC,EAAE,EAAE,CAAC;QACvE,OAAM;IACR,CAAC;IAED,wGAAwG;IACxG,IAAI,cAAc,CAAC,KAAK,IAAI,yCAAoB,CAAC,UAAU,EAAE,CAAC;QAC5D,MAAM,gBAAgB,GAAG,MAAM,IAAA,qBAAa,EAAC,uCAAgB,EAAE,EAAE,CAAC,CAAC,SAAS,CAAC;YAC3E,EAAE,EAAE,cAAc,CAAC,kBAAkB;SACtC,CAAC,CAAA;QAEF,IAAI,gBAAgB,IAAI,gBAAgB,CAAC,cAAc,EAAE,CAAC;YACxD,MAAM,KAAK,GAAG,CACZ,MAAM,IAAA,qBAAa,EAAC,gBAAI,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC;gBACpC,KAAK,EAAE,EAAE,EAAE,EAAE,IAAI,CAAC,EAAE,EAAE;gBACtB,SAAS,EAAE,CAAC,OAAO,CAAC;aACrB,CAAC,CACH,EAAE,KAAK,CAAA;YAER,IAAI,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,IAAI,gBAAgB,CAAC,cAAc,CAAC,EAAE,CAAC;gBACnE,cAAc,CAAC,QAAQ,GAAG,IAAI,CAAA;gBAC9B,cAAc,CAAC,KAAK,GAAG,yCAAoB,CAAC,QAAQ,CAAA;gBAEpD,OAAM;YACR,CAAC;QACH,CAAC;IACH,CAAC;IAED,MAAM,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,0CAA0C,CAAC,CAAC,CAAA;AACxE,CAAC","sourcesContent":["import { getRepository } from '@things-factory/shell'\nimport { User } from '@things-factory/auth-base'\nimport { ActivityThread, ActivityThreadStatus } from '../../service/activity-thread/activity-thread.js'\nimport { ActivityInstance } from '../../service/activity-instance/activity-instance.js'\n\nexport async function checkAuthority(activityThread: ActivityThread, context: ResolverContext) {\n const { user, tx } = context.state\n\n if (activityThread.assigneeId && activityThread.assigneeId === user.id) {\n return\n }\n\n // TODO activityInstance의 cardinality를 기준으로 unassigned thread를 추가로 만들 지, assigned thread로 대체할 지를 구현해야한다.\n if (activityThread.state == ActivityThreadStatus.Unassigned) {\n const activityInstance = await getRepository(ActivityInstance, tx).findOneBy({\n id: activityThread.activityInstanceId\n })\n\n if (activityInstance && activityInstance.assigneeRoleId) {\n const roles = (\n await getRepository(User, tx).findOne({\n where: { id: user.id },\n relations: ['roles']\n })\n )?.roles\n\n if (roles.find(role => role.id == activityInstance.assigneeRoleId)) {\n activityThread.assignee = user\n activityThread.state = ActivityThreadStatus.Assigned\n\n return\n }\n }\n }\n\n throw new Error(context.t(`error.not authorized to handle this task`))\n}\n"]}
1
+ {"version":3,"file":"check-authority.js","sourceRoot":"","sources":["../../../server/controllers/activity-thread/check-authority.ts"],"names":[],"mappings":";;AAMA,wCAmDC;AAzDD,iDAAyE;AACzE,yDAAgD;AAChD,yFAAuG;AACvG,+FAAuF;AACvF,uFAAwF;AAEjF,KAAK,UAAU,cAAc,CAAC,cAA8B,EAAE,OAAwB;IAC3F,MAAM,EAAE,IAAI,EAAE,EAAE,EAAE,GAAG,OAAO,CAAC,KAAK,CAAA;IAElC,IAAI,cAAc,CAAC,UAAU,IAAI,cAAc,CAAC,UAAU,KAAK,IAAI,CAAC,EAAE,EAAE,CAAC;QACvE,OAAM;IACR,CAAC;IAED,wGAAwG;IACxG,IAAI,cAAc,CAAC,KAAK,IAAI,yCAAoB,CAAC,UAAU,EAAE,CAAC;QAC5D;;;;;WAKG;QACH,MAAM,gBAAgB,GAAG,MAAM,IAAA,qBAAa,EAAC,uCAAgB,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC;YACzE,KAAK,EAAE,EAAE,EAAE,EAAE,cAAc,CAAC,kBAAkB,EAAE;YAChD,GAAG,CAAC,IAAA,0BAAkB,EAAC,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,mBAA4B,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACpF,CAAC,CAAA;QAEF,wGAAwG;QACxG,MAAM,GAAG,GAAG,MAAM,IAAA,qBAAa,EAAC,mCAAc,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC;YAC1D,KAAK,EAAE,EAAE,EAAE,EAAE,cAAc,CAAC,EAAE,EAAE;YAChC,GAAG,CAAC,IAAA,0BAAkB,EAAC,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,mBAA4B,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACpF,CAAC,CAAA;QAEF,IAAI,GAAG,EAAE,KAAK,KAAK,yCAAoB,CAAC,UAAU,EAAE,CAAC;YACnD,MAAM,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,+CAA+C,EAAE,EAAE,gBAAgB,EAAE,cAAc,CAAC,EAAE,EAAE,CAAC,CAAC,CAAA;QACtH,CAAC;QAED,MAAM,OAAO,GAAG,IAAA,4BAAS,EAAC,gBAAgB,CAAC,CAAA;QAE3C,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;YACnB,MAAM,KAAK,GAAG,CACZ,MAAM,IAAA,qBAAa,EAAC,gBAAI,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC;gBACpC,KAAK,EAAE,EAAE,EAAE,EAAE,IAAI,CAAC,EAAE,EAAE;gBACtB,SAAS,EAAE,CAAC,OAAO,CAAC;aACrB,CAAC,CACH,EAAE,KAAK,CAAA;YAER,kEAAkE;YAClE,IAAI,IAAA,6BAAU,EAAC,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,OAAO,CAAC,EAAE,CAAC;gBAC5D,cAAc,CAAC,QAAQ,GAAG,IAAI,CAAA;gBAC9B,cAAc,CAAC,KAAK,GAAG,yCAAoB,CAAC,QAAQ,CAAA;gBAEpD,OAAM;YACR,CAAC;QACH,CAAC;IACH,CAAC;IAED,MAAM,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,0CAA0C,CAAC,CAAC,CAAA;AACxE,CAAC","sourcesContent":["import { getRepository, writeLockSupported } from '@things-factory/shell'\nimport { User } from '@things-factory/auth-base'\nimport { ActivityThread, ActivityThreadStatus } from '../../service/activity-thread/activity-thread.js'\nimport { ActivityInstance } from '../../service/activity-instance/activity-instance.js'\nimport { holdsAnyOf, poolIdsOf } from '../../service/activity-instance/assignee-pool.js'\n\nexport async function checkAuthority(activityThread: ActivityThread, context: ResolverContext) {\n const { user, tx } = context.state\n\n if (activityThread.assigneeId && activityThread.assigneeId === user.id) {\n return\n }\n\n // TODO activityInstance의 cardinality를 기준으로 unassigned thread를 추가로 만들 지, assigned thread로 대체할 지를 구현해야한다.\n if (activityThread.state == ActivityThreadStatus.Unassigned) {\n /*\n * Claiming the pool's waiting thread. The case is held first - the order pick and a pool assign take\n * (activity-instance/pick.ts, assign.ts) - and the thread is read again under it. The thread handed in\n * was read before anything was held: someone else may have claimed it since, or a manager moved the\n * case to another role's pool, and what this claims must be what is there now.\n */\n const activityInstance = await getRepository(ActivityInstance, tx).findOne({\n where: { id: activityThread.activityInstanceId },\n ...(writeLockSupported(tx) ? { lock: { mode: 'pessimistic_write' as const } } : {})\n })\n\n /* A locking read is a current read - a plain one may answer from the snapshot taken before the wait. */\n const now = await getRepository(ActivityThread, tx).findOne({\n where: { id: activityThread.id },\n ...(writeLockSupported(tx) ? { lock: { mode: 'pessimistic_write' as const } } : {})\n })\n\n if (now?.state !== ActivityThreadStatus.Unassigned) {\n throw new Error(context.t('error.activity-approval taken by someone else', { activityApproval: activityThread.id }))\n }\n\n const poolIds = poolIdsOf(activityInstance)\n\n if (poolIds.length) {\n const roles = (\n await getRepository(User, tx).findOne({\n where: { id: user.id },\n relations: ['roles']\n })\n )?.roles\n\n /* Any one of the pool's roles is enough (ADR-0091 decision 9). */\n if (holdsAnyOf((roles || []).map(role => role.id), poolIds)) {\n activityThread.assignee = user\n activityThread.state = ActivityThreadStatus.Assigned\n\n return\n }\n }\n }\n\n throw new Error(context.t(`error.not authorized to handle this task`))\n}\n"]}
@@ -10,6 +10,7 @@ exports.getSystemUserFromMyDepartmentManager = getSystemUserFromMyDepartmentMana
10
10
  exports.getSystemUserFromMySupervosor = getSystemUserFromMySupervosor;
11
11
  exports.getSystemUserFromOrgMemberItem = getSystemUserFromOrgMemberItem;
12
12
  exports.fillActivitySearchKeys = fillActivitySearchKeys;
13
+ const typeorm_1 = require("typeorm");
13
14
  const auth_base_1 = require("@things-factory/auth-base");
14
15
  const organization_1 = require("@things-factory/organization");
15
16
  const activity_js_1 = require("../service/activity/activity.js");
@@ -17,6 +18,8 @@ const activity_instance_js_1 = require("../service/activity-instance/activity-in
17
18
  const activity_thread_js_1 = require("../service/activity-thread/activity-thread.js");
18
19
  const activity_approval_js_1 = require("../service/activity-approval/activity-approval.js");
19
20
  const activity_installation_controller_js_1 = require("./activity-installation/activity-installation-controller.js");
21
+ const assignee_pool_js_1 = require("../service/activity-instance/assignee-pool.js");
22
+ const assignee_input_js_1 = require("./activity-instance/assignee-input.js");
20
23
  async function updateActivityInstanceState(id, { causedBy }, context) {
21
24
  const { tx } = context.state;
22
25
  var activityInstance = await tx.getRepository(activity_instance_js_1.ActivityInstance).findOne({
@@ -25,7 +28,7 @@ async function updateActivityInstanceState(id, { causedBy }, context) {
25
28
  'domain',
26
29
  'activity',
27
30
  'activityThreads',
28
- 'assigneeRole',
31
+ 'assigneeRoles',
29
32
  'supervisoryRole',
30
33
  'updater',
31
34
  'creator',
@@ -213,12 +216,10 @@ async function createActivityThreadsForUsers(transaction, activityInstance, assi
213
216
  }
214
217
  async function createActivityThreadsForAllRoleUsers(transaction, activityInstance, context) {
215
218
  const { tx } = context.state;
216
- // find how many users are having given role
217
- const assigneeRole = (await tx.getRepository(auth_base_1.Role).findOne({
218
- where: { id: activityInstance.assigneeRoleId },
219
- relations: ['users']
220
- }));
221
- const assignees = (assigneeRole?.users || []).filter(user => user.userType == 'user');
219
+ /* Everyone the pool reaches - any one of its roles, each person once (assignee-pool.ts). */
220
+ const poolIds = (0, assignee_pool_js_1.poolIdsOf)(activityInstance);
221
+ const roles = poolIds.length ? await tx.getRepository(auth_base_1.Role).find({ where: { id: (0, typeorm_1.In)(poolIds) }, relations: ['users'] }) : [];
222
+ const assignees = (0, assignee_pool_js_1.poolMembers)(roles);
222
223
  if (assignees.length === 0) {
223
224
  throw new Error("there's no users having given role");
224
225
  }
@@ -287,11 +288,15 @@ async function getSystemUserFromOrgMemberItem(item, context) {
287
288
  return await tx.getRepository(auth_base_1.User).findOneBy({ id: department.manager.userId });
288
289
  }
289
290
  else if (type == organization_1.OrgMemberTargetType.Role) {
290
- const role = id && (await tx.getRepository(auth_base_1.Role).findOne({ where: { domain: { id: domain.id }, id }, relations: ['users'] }));
291
- if (!role || !role.users || !role.users[0]) {
292
- return;
293
- }
294
- return await tx.getRepository(auth_base_1.User).findOneBy({ id: role.users[0].id });
291
+ /*
292
+ * A role is not one person. This answered the role's first user - an unordered relation, so an
293
+ * arbitrary one - and issue() gave that person work meant for the whole role. Each caller decides
294
+ * what a role means there before asking this: the approval line seats the role (create-approval.ts),
295
+ * assignees refuse it (assignee-input.ts). Reaching here is a caller that skipped that step.
296
+ */
297
+ throw Object.assign(new Error(context.t('error.activity-instance a role is not an assignee')), {
298
+ code: assignee_input_js_1.ROLE_IS_NOT_AN_ASSIGNEE
299
+ });
295
300
  }
296
301
  else if (type == organization_1.OrgMemberTargetType.Myself) {
297
302
  return user;