@things-factory/worklist 10.1.258 → 10.1.260

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 (57) hide show
  1. package/dist-client/chip-counts.d.ts +13 -0
  2. package/dist-client/chip-counts.js +11 -0
  3. package/dist-client/chip-counts.js.map +1 -1
  4. package/dist-client/chip-filters.d.ts +29 -0
  5. package/dist-client/chip-filters.js +17 -1
  6. package/dist-client/chip-filters.js.map +1 -1
  7. package/dist-client/pages/approval-progress/approval-progress-page.d.ts +0 -4
  8. package/dist-client/pages/approval-progress/approval-progress-page.js +37 -31
  9. package/dist-client/pages/approval-progress/approval-progress-page.js.map +1 -1
  10. package/dist-client/pages/todo/approval-pending-list-page.d.ts +2 -3
  11. package/dist-client/pages/todo/approval-pending-list-page.js +33 -47
  12. package/dist-client/pages/todo/approval-pending-list-page.js.map +1 -1
  13. package/dist-client/pages/todo/draft-list-page.js +13 -10
  14. package/dist-client/pages/todo/draft-list-page.js.map +1 -1
  15. package/dist-client/pages/todo/todo-list-page.d.ts +0 -3
  16. package/dist-client/pages/todo/todo-list-page.js +30 -42
  17. package/dist-client/pages/todo/todo-list-page.js.map +1 -1
  18. package/dist-client/tsconfig.tsbuildinfo +1 -1
  19. package/dist-server/controllers/activity-installation/activity-installation-controller.js +1 -0
  20. package/dist-server/controllers/activity-installation/activity-installation-controller.js.map +1 -1
  21. package/dist-server/controllers/activity-installation/installable-to-activity.d.ts +10 -0
  22. package/dist-server/controllers/activity-installation/installable-to-activity.js +22 -1
  23. package/dist-server/controllers/activity-installation/installable-to-activity.js.map +1 -1
  24. package/dist-server/controllers/activity-instance/assign.js +7 -2
  25. package/dist-server/controllers/activity-instance/assign.js.map +1 -1
  26. package/dist-server/controllers/activity-instance/complete.d.ts +13 -0
  27. package/dist-server/controllers/activity-instance/complete.js +58 -0
  28. package/dist-server/controllers/activity-instance/complete.js.map +1 -0
  29. package/dist-server/controllers/activity-instance/index.d.ts +1 -0
  30. package/dist-server/controllers/activity-instance/index.js +1 -0
  31. package/dist-server/controllers/activity-instance/index.js.map +1 -1
  32. package/dist-server/service/activity-approval/activity-approval.d.ts +3 -0
  33. package/dist-server/service/activity-approval/activity-approval.js +8 -0
  34. package/dist-server/service/activity-approval/activity-approval.js.map +1 -1
  35. package/dist-server/service/activity-instance/event-subscriber.d.ts +1 -0
  36. package/dist-server/service/activity-instance/event-subscriber.js +11 -0
  37. package/dist-server/service/activity-instance/event-subscriber.js.map +1 -1
  38. package/dist-server/service/approval-notice/approval-notice.js +3 -2
  39. package/dist-server/service/approval-notice/approval-notice.js.map +1 -1
  40. package/dist-server/service/approval-progress/approval-progress-query.js +4 -0
  41. package/dist-server/service/approval-progress/approval-progress-query.js.map +1 -1
  42. package/dist-server/service/approval-progress/approval-progress-rule.d.ts +4 -0
  43. package/dist-server/service/approval-progress/approval-progress-rule.js +8 -0
  44. package/dist-server/service/approval-progress/approval-progress-rule.js.map +1 -1
  45. package/dist-server/service/approval-progress/approval-progress-type.d.ts +1 -0
  46. package/dist-server/service/approval-progress/approval-progress-type.js +4 -0
  47. package/dist-server/service/approval-progress/approval-progress-type.js.map +1 -1
  48. package/dist-server/tsconfig.tsbuildinfo +1 -1
  49. package/package.json +12 -12
  50. package/tests/assign-role-pool.test.ts +27 -1
  51. package/tests/complete-db.test.ts +62 -0
  52. package/translations/en.json +3 -2
  53. package/translations/es.json +3 -2
  54. package/translations/ja.json +3 -2
  55. package/translations/ko.json +3 -2
  56. package/translations/ms.json +3 -2
  57. package/translations/zh.json +3 -2
@@ -43,6 +43,7 @@ async function installActivity(name, domain, options = {}) {
43
43
  return await repository.save({
44
44
  ...(existing ?? {}),
45
45
  ...(0, installable_to_activity_js_1.activityRowFrom)(template),
46
+ ...(0, installable_to_activity_js_1.activityDefaultsFrom)(template, existing),
46
47
  state: activity_js_1.ActivityStatus.Released,
47
48
  domain,
48
49
  creator: existing ? existing.creator : user,
@@ -1 +1 @@
1
- {"version":3,"file":"activity-installation-controller.js","sourceRoot":"","sources":["../../../server/controllers/activity-installation/activity-installation-controller.ts"],"names":[],"mappings":"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","sourcesContent":["import { EntityManager } from 'typeorm'\n\nimport { Domain, getRepository } from '@things-factory/shell'\n\nimport { Activity, ActivityStatus } from '../../service/activity/activity.js'\nimport { InstallableActivity } from '../../service/installable-activity/installable-activity.js'\nimport { ActivityReadiness } from '../../service/installable-activity/activity-readiness.js'\nimport { assertDoorSlugIsFree, assertNotAlreadyInstalled, assertNotDeclaredIntended } from './registry-conflict.js'\nimport { doorSlug } from '../../service/activity/decision-key-declaration.js'\nimport { activityRowFrom } from './installable-to-activity.js'\n\n/**\n * 등록된 양식을 **도메인에 실제로 세운다.**\n *\n * ── 왜 함수로 내놓나 ───────────────────────────────────────────────────────\n * 앞서는 이 일이 화면 뮤테이션 안에만 있었다. 그래서 양식을 세우는 길이 **사람이 화면에서 누르는 것\n * 하나뿐**이었고, 새로 배포하거나 도메인을 하나 더 만들 때마다 그 클릭을 기억해야 했다. 기억하지\n * 못하면 그 기능이 오류 보고 없이 없다.\n *\n * 앱이 「내 기능이 서려면 이 양식이 있어야 한다」를 아는 경우가 있다. 그런 앱은 기동할 때 스스로\n * 세울 수 있어야 한다. 언제 세울지는 앱이 정하고, 세우는 방법은 여기가 한 벌만 갖는다.\n *\n * ── 있는 것을 덮되 결재선은 건드리지 않는다 ────────────────────────────────\n * 이미 있으면 양식의 선언(모델 · 보는 화면 · 시작 방식)으로 덮는다. 버전이 오르면 그 값이 따라와야\n * 하기 때문이다.\n *\n * 그런데 **결재선은 양식의 것이 아니라 그 현장의 것**이다. 누가 결재하는지는 공장이 정한다. 그래서\n * 양식에 결재선 선언이 없으면 이미 있는 결재선이 그대로 남는다 — 버전을 올릴 때마다 결재자가 지워지면\n * 그 뒤로 상신이 곧 완료가 된다.\n */\nexport async function installActivity(\n name: string,\n domain: Domain,\n options: { manager?: EntityManager; user?: any } = {}\n): Promise<Activity> {\n const template = ActivityInstallations.get(name)\n\n if (!template) {\n /*\n * 없는 이름을 세울 수는 없다. 조용히 넘기면 부르는 쪽은 세운 줄 알고, 그 기능은 없는 채로 선다.\n */\n throw new Error(\n `installActivity: there is no installable activity named \"${name}\". ` +\n `known: ${ActivityInstallations.list()\n .map(one => one.name)\n .join(', ')}`\n )\n }\n\n const { manager, user } = options\n const repository = getRepository(Activity, manager)\n\n const existing = await repository.findOneBy({ domain: { id: domain.id }, name })\n\n return await repository.save({\n ...(existing ?? {}),\n ...activityRowFrom(template),\n state: ActivityStatus.Released,\n domain,\n creator: existing ? (existing as any).creator : user,\n updater: user\n } as Activity)\n}\n\n/**\n * Work a module has identified as a person's decision and **has not built.**\n *\n * Deliberately not an InstallableActivity. A template with no callback can be installed and\n * issued, and then someone approves a request and nothing happens — on the twin side that means a\n * person presses approve and no equipment moves. So these live in their own registry: the catalog\n * reads them, `installActivity` cannot reach them.\n *\n * They belong on the screen because the catalog is the plan of record. The question \"what is left\"\n * should be answered where people look, not in one person's head.\n */\nexport type IntendedActivity = {\n /** What the activity's `name` will be once it exists. */\n key: string\n /** What the catalog shows. The activity's own name is usually not a sentence a person reads. */\n label: string\n category?: string\n /** What this work is, for the person who will receive it. Draft wording is fine. */\n guide?: string[]\n readiness: ActivityReadiness\n}\n\nexport class ActivityInstallations {\n static templates: { [name: string]: InstallableActivity } = {}\n\n /** Keyed separately from `templates`, so nothing here can be installed by mistake. */\n static intended: { [key: string]: IntendedActivity } = {}\n\n static installActivityTemplate(template: InstallableActivity) {\n assertNotDeclaredIntended(template.name, ActivityInstallations.intended)\n assertDoorSlugIsFree(template.name, doorSlug, ActivityInstallations.templates)\n\n ActivityInstallations.templates[template.name] = template\n }\n\n /**\n * Declare work that should reach someone's worklist and does not yet.\n *\n * A module calls this for each item it has identified. When the real thing is built, delete the\n * call and register the template instead — the catalog row moves from 예정 to 동작 확인됨 on its\n * own.\n */\n static declareIntendedActivity(intended: IntendedActivity) {\n assertNotAlreadyInstalled(intended.key, ActivityInstallations.templates)\n\n ActivityInstallations.intended[intended.key] = intended\n }\n\n static get(name: string): InstallableActivity {\n return ActivityInstallations.templates[name]\n }\n\n static list(): InstallableActivity[] {\n return Object.values(ActivityInstallations.templates)\n }\n\n static listIntended(): IntendedActivity[] {\n return Object.values(ActivityInstallations.intended)\n }\n\n /**\n * 액티비티인스턴스의 변화가 발생했을 때, 호출되는 콜백함수\n * @param name 액티비티인스턴스가 속한 액티비티명\n * @param activityInstance 해당 액티비티인스턴스\n * @param activityThreads 액티비티인스턴스의 변화를 일으킨 원인이 되는 액티비티쓰레드들\n * @param context 서비스 컨텍스트 : domain, user, translation, ..\n * @returns\n */\n static async callback(name: string, activityInstance: any, activityThreads: any[], context: ResolverContext) {\n const installableActivity = ActivityInstallations.templates[name]\n if (!installableActivity) {\n return\n }\n\n const { callback } = installableActivity\n\n callback && (await callback(activityInstance, { causedBy: activityThreads }, context))\n }\n}\n"]}
1
+ {"version":3,"file":"activity-installation-controller.js","sourceRoot":"","sources":["../../../server/controllers/activity-installation/activity-installation-controller.ts"],"names":[],"mappings":"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","sourcesContent":["import { EntityManager } from 'typeorm'\n\nimport { Domain, getRepository } from '@things-factory/shell'\n\nimport { Activity, ActivityStatus } from '../../service/activity/activity.js'\nimport { InstallableActivity } from '../../service/installable-activity/installable-activity.js'\nimport { ActivityReadiness } from '../../service/installable-activity/activity-readiness.js'\nimport { assertDoorSlugIsFree, assertNotAlreadyInstalled, assertNotDeclaredIntended } from './registry-conflict.js'\nimport { doorSlug } from '../../service/activity/decision-key-declaration.js'\nimport { activityDefaultsFrom, activityRowFrom } from './installable-to-activity.js'\n\n/**\n * 등록된 양식을 **도메인에 실제로 세운다.**\n *\n * ── 왜 함수로 내놓나 ───────────────────────────────────────────────────────\n * 앞서는 이 일이 화면 뮤테이션 안에만 있었다. 그래서 양식을 세우는 길이 **사람이 화면에서 누르는 것\n * 하나뿐**이었고, 새로 배포하거나 도메인을 하나 더 만들 때마다 그 클릭을 기억해야 했다. 기억하지\n * 못하면 그 기능이 오류 보고 없이 없다.\n *\n * 앱이 「내 기능이 서려면 이 양식이 있어야 한다」를 아는 경우가 있다. 그런 앱은 기동할 때 스스로\n * 세울 수 있어야 한다. 언제 세울지는 앱이 정하고, 세우는 방법은 여기가 한 벌만 갖는다.\n *\n * ── 있는 것을 덮되 결재선은 건드리지 않는다 ────────────────────────────────\n * 이미 있으면 양식의 선언(모델 · 보는 화면 · 시작 방식)으로 덮는다. 버전이 오르면 그 값이 따라와야\n * 하기 때문이다.\n *\n * 그런데 **결재선은 양식의 것이 아니라 그 현장의 것**이다. 누가 결재하는지는 공장이 정한다. 그래서\n * 양식에 결재선 선언이 없으면 이미 있는 결재선이 그대로 남는다 — 버전을 올릴 때마다 결재자가 지워지면\n * 그 뒤로 상신이 곧 완료가 된다.\n */\nexport async function installActivity(\n name: string,\n domain: Domain,\n options: { manager?: EntityManager; user?: any } = {}\n): Promise<Activity> {\n const template = ActivityInstallations.get(name)\n\n if (!template) {\n /*\n * 없는 이름을 세울 수는 없다. 조용히 넘기면 부르는 쪽은 세운 줄 알고, 그 기능은 없는 채로 선다.\n */\n throw new Error(\n `installActivity: there is no installable activity named \"${name}\". ` +\n `known: ${ActivityInstallations.list()\n .map(one => one.name)\n .join(', ')}`\n )\n }\n\n const { manager, user } = options\n const repository = getRepository(Activity, manager)\n\n const existing = await repository.findOneBy({ domain: { id: domain.id }, name })\n\n return await repository.save({\n ...(existing ?? {}),\n ...activityRowFrom(template),\n ...activityDefaultsFrom(template, existing),\n state: ActivityStatus.Released,\n domain,\n creator: existing ? (existing as any).creator : user,\n updater: user\n } as Activity)\n}\n\n/**\n * Work a module has identified as a person's decision and **has not built.**\n *\n * Deliberately not an InstallableActivity. A template with no callback can be installed and\n * issued, and then someone approves a request and nothing happens — on the twin side that means a\n * person presses approve and no equipment moves. So these live in their own registry: the catalog\n * reads them, `installActivity` cannot reach them.\n *\n * They belong on the screen because the catalog is the plan of record. The question \"what is left\"\n * should be answered where people look, not in one person's head.\n */\nexport type IntendedActivity = {\n /** What the activity's `name` will be once it exists. */\n key: string\n /** What the catalog shows. The activity's own name is usually not a sentence a person reads. */\n label: string\n category?: string\n /** What this work is, for the person who will receive it. Draft wording is fine. */\n guide?: string[]\n readiness: ActivityReadiness\n}\n\nexport class ActivityInstallations {\n static templates: { [name: string]: InstallableActivity } = {}\n\n /** Keyed separately from `templates`, so nothing here can be installed by mistake. */\n static intended: { [key: string]: IntendedActivity } = {}\n\n static installActivityTemplate(template: InstallableActivity) {\n assertNotDeclaredIntended(template.name, ActivityInstallations.intended)\n assertDoorSlugIsFree(template.name, doorSlug, ActivityInstallations.templates)\n\n ActivityInstallations.templates[template.name] = template\n }\n\n /**\n * Declare work that should reach someone's worklist and does not yet.\n *\n * A module calls this for each item it has identified. When the real thing is built, delete the\n * call and register the template instead — the catalog row moves from 예정 to 동작 확인됨 on its\n * own.\n */\n static declareIntendedActivity(intended: IntendedActivity) {\n assertNotAlreadyInstalled(intended.key, ActivityInstallations.templates)\n\n ActivityInstallations.intended[intended.key] = intended\n }\n\n static get(name: string): InstallableActivity {\n return ActivityInstallations.templates[name]\n }\n\n static list(): InstallableActivity[] {\n return Object.values(ActivityInstallations.templates)\n }\n\n static listIntended(): IntendedActivity[] {\n return Object.values(ActivityInstallations.intended)\n }\n\n /**\n * 액티비티인스턴스의 변화가 발생했을 때, 호출되는 콜백함수\n * @param name 액티비티인스턴스가 속한 액티비티명\n * @param activityInstance 해당 액티비티인스턴스\n * @param activityThreads 액티비티인스턴스의 변화를 일으킨 원인이 되는 액티비티쓰레드들\n * @param context 서비스 컨텍스트 : domain, user, translation, ..\n * @returns\n */\n static async callback(name: string, activityInstance: any, activityThreads: any[], context: ResolverContext) {\n const installableActivity = ActivityInstallations.templates[name]\n if (!installableActivity) {\n return\n }\n\n const { callback } = installableActivity\n\n callback && (await callback(activityInstance, { causedBy: activityThreads }, context))\n }\n}\n"]}
@@ -39,6 +39,16 @@ type TemplateFields = {
39
39
  [field: string]: unknown;
40
40
  };
41
41
  export declare const ACTIVITY_ROW_FIELDS: readonly ["name", "label", "description", "activityType", "priority", "startingType", "multiple", "model", "decisionKeys", "uiType", "uiSource", "viewType", "viewSource", "reportType", "reportSource", "thumbnail"];
42
+ /**
43
+ * The template fields that only seed a row: the template's value is the default, and once the row has one it is the
44
+ * domain's - the activity screen edits it, and installing again at every boot does not take it back.
45
+ *
46
+ * standardTime how long the work may take; an instance's due is its issue plus this (issue.ts). Never copied
47
+ * before, so every installed approval had no due (the user, 2026-11-08: 「각 결재에 기한이 없군」).
48
+ */
49
+ export declare const ACTIVITY_DEFAULT_FIELDS: readonly ["standardTime"];
50
+ /** The defaults of a template the row does not have yet. */
51
+ export declare function activityDefaultsFrom(template: object | undefined, existing: object | null | undefined): TemplateFields;
42
52
  /**
43
53
  * The part of a template that is stored on the Activity row.
44
54
  *
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.ACTIVITY_ROW_FIELDS = void 0;
3
+ exports.ACTIVITY_DEFAULT_FIELDS = exports.ACTIVITY_ROW_FIELDS = void 0;
4
+ exports.activityDefaultsFrom = activityDefaultsFrom;
4
5
  exports.activityRowFrom = activityRowFrom;
5
6
  exports.ACTIVITY_ROW_FIELDS = [
6
7
  'name',
@@ -20,6 +21,26 @@ exports.ACTIVITY_ROW_FIELDS = [
20
21
  'reportSource',
21
22
  'thumbnail'
22
23
  ];
24
+ /**
25
+ * The template fields that only seed a row: the template's value is the default, and once the row has one it is the
26
+ * domain's - the activity screen edits it, and installing again at every boot does not take it back.
27
+ *
28
+ * standardTime how long the work may take; an instance's due is its issue plus this (issue.ts). Never copied
29
+ * before, so every installed approval had no due (the user, 2026-11-08: 「각 결재에 기한이 없군」).
30
+ */
31
+ exports.ACTIVITY_DEFAULT_FIELDS = ['standardTime'];
32
+ /** The defaults of a template the row does not have yet. */
33
+ function activityDefaultsFrom(template, existing) {
34
+ const fields = (template ?? {});
35
+ const row = (existing ?? {});
36
+ const defaults = {};
37
+ for (const field of exports.ACTIVITY_DEFAULT_FIELDS) {
38
+ if ((row[field] === undefined || row[field] === null) && fields[field] !== undefined) {
39
+ defaults[field] = fields[field];
40
+ }
41
+ }
42
+ return defaults;
43
+ }
23
44
  /**
24
45
  * The part of a template that is stored on the Activity row.
25
46
  *
@@ -1 +1 @@
1
- {"version":3,"file":"installable-to-activity.js","sourceRoot":"","sources":["../../../server/controllers/activity-installation/installable-to-activity.ts"],"names":[],"mappings":";;;AA2FA,0CAaC;AAlEY,QAAA,mBAAmB,GAAG;IACjC,MAAM;IACN,OAAO;IACP,aAAa;IACb,cAAc;IACd,UAAU;IACV,cAAc;IACd,UAAU;IACV,OAAO;IACP,cAAc;IACd,QAAQ;IACR,UAAU;IACV,UAAU;IACV,YAAY;IACZ,YAAY;IACZ,cAAc;IACd,WAAW;CACH,CAAA;AAEV;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,SAAgB,eAAe,CAAC,QAA4B;IAC1D,MAAM,MAAM,GAAG,CAAC,QAAQ,IAAI,EAAE,CAAmB,CAAA;IACjD,MAAM,GAAG,GAAmB,EAAE,CAAA;IAE9B,KAAK,MAAM,KAAK,IAAI,2BAAmB,EAAE,CAAC;QACxC,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,CAAA;QAE3B,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;YACxB,GAAG,CAAC,KAAK,CAAC,GAAG,KAAK,CAAA;QACpB,CAAC;IACH,CAAC;IAED,OAAO,GAAG,CAAA;AACZ,CAAC","sourcesContent":["/**\n * The template fields that belong on the Activity row, named one by one.\n *\n * ── Why not spread the template ───────────────────────────────────────────\n * installActivity used `{ ...(template as any) }`. A template field with no column on Activity was\n * dropped by the save, which reported success — and `as any` meant the types said nothing about it.\n * Nine fields were being handed over with no home: activity, approvalSteps, callback, category,\n * guide, label, provider, readiness, release.\n *\n * `label` was the one that hurt. It is the display name a person reads, and it reached exactly one\n * screen — the catalog, which reads the template registry directly — while every screen reading the\n * row showed the instance name instead. decisionKeys only worked because a column happened to be\n * added for it in the same change; the next field would have been left to the same luck.\n *\n * Naming the fields buys three things: a new template field has to be wired here on purpose, the\n * eight that stay behind are recorded as catalog-only rather than lost, and the two `as any` casts\n * go away. (2026-09-10, architect's ruling)\n *\n * ── What stays in the template registry, and why ──────────────────────────\n * activity the installed row itself, attached by the catalog query. Writing it back would\n * be a row pointing at itself\n * approvalSteps counted per domain by the catalog query from the line that is actually set up\n * callback a function\n * category how the catalog groups it\n * guide what the work is, for the catalog's reader\n * provider which module registered it\n * readiness how far along the registering module says it is\n * release which version registered it\n *\n * Each of those is answered by the registry, which is loaded in every process that serves the\n * catalog. Copying them to the row would make a second copy that goes stale on the next release.\n *\n * ── The template shape is spelled out, not imported ───────────────────────\n * InstallableActivity carries type-graphql decorators, and the test runner strips types without\n * reading them, so importing it takes the whole test file down. That happened three times in one\n * day — to search-key-declaration, to decision-key-declaration, and here.\n */\ntype TemplateFields = { [field: string]: unknown }\nexport const ACTIVITY_ROW_FIELDS = [\n 'name',\n 'label',\n 'description',\n 'activityType',\n 'priority',\n 'startingType',\n 'multiple',\n 'model',\n 'decisionKeys',\n 'uiType',\n 'uiSource',\n 'viewType',\n 'viewSource',\n 'reportType',\n 'reportSource',\n 'thumbnail'\n] as const\n\n/**\n * The part of a template that is stored on the Activity row.\n *\n * A field the template left undefined is not copied, so installing again does not blank a value\n * somebody edited on the row — `save` with an explicit undefined writes null. That protection is\n * real: the activity list screen lets a domain edit viewType and viewSource on the row, and\n * installActivity runs at every boot. A field the template set to null is copied, because null is\n * how a template says the value is gone.\n *\n * ── So leaving a field out is not the same as clearing it ─────────────────\n * A template that stops providing something has to say so, because silence means \"keep whatever the\n * row has\":\n *\n * viewSource: undefined the template says nothing — the row keeps its value\n * viewSource: null the template says there is none — the row is cleared\n *\n * plant hit this: they removed two viewSource declarations, the rows kept pointing at elements that\n * no longer existed, and approvers met a screen that could not load. (2026-09-11)\n *\n * ── Why null and not an empty string ──────────────────────────────────────\n * The rule was first written as `viewSource: ''`, and for a string field that does clear it. But\n * five of the sixteen fields carried here are not strings — priority is a number, multiple and\n * activityType and startingType are enums, model and decisionKeys are json — and none of them has\n * an empty string. `priority: ''` puts a non-number in an integer column; `decisionKeys: ''` puts a\n * string where the screen expects an array and every declared slot disappears with no error. A rule\n * that only holds for two thirds of the list is the kind that gets applied to the other third.\n *\n * null is the one signal that reads the same on all sixteen, and it says \"there is none\" rather\n * than naming a value that happens to be falsy. It needs nothing here: the copy below asks only\n * whether the template said anything, so null crosses over and the save writes NULL.\n *\n * An empty string still clears a string field — it is a value, and it lands in the column. It is\n * simply not the rule. (2026-09-16, answering the architect)\n */\nexport function activityRowFrom(template: object | undefined): TemplateFields {\n const fields = (template ?? {}) as TemplateFields\n const row: TemplateFields = {}\n\n for (const field of ACTIVITY_ROW_FIELDS) {\n const value = fields[field]\n\n if (value !== undefined) {\n row[field] = value\n }\n }\n\n return row\n}\n"]}
1
+ {"version":3,"file":"installable-to-activity.js","sourceRoot":"","sources":["../../../server/controllers/activity-installation/installable-to-activity.ts"],"names":[],"mappings":";;;AAmEA,oDAYC;AAoCD,0CAaC;AA1FY,QAAA,mBAAmB,GAAG;IACjC,MAAM;IACN,OAAO;IACP,aAAa;IACb,cAAc;IACd,UAAU;IACV,cAAc;IACd,UAAU;IACV,OAAO;IACP,cAAc;IACd,QAAQ;IACR,UAAU;IACV,UAAU;IACV,YAAY;IACZ,YAAY;IACZ,cAAc;IACd,WAAW;CACH,CAAA;AAEV;;;;;;GAMG;AACU,QAAA,uBAAuB,GAAG,CAAC,cAAc,CAAU,CAAA;AAEhE,4DAA4D;AAC5D,SAAgB,oBAAoB,CAAC,QAA4B,EAAE,QAAmC;IACpG,MAAM,MAAM,GAAG,CAAC,QAAQ,IAAI,EAAE,CAAmB,CAAA;IACjD,MAAM,GAAG,GAAG,CAAC,QAAQ,IAAI,EAAE,CAAmB,CAAA;IAC9C,MAAM,QAAQ,GAAmB,EAAE,CAAA;IAEnC,KAAK,MAAM,KAAK,IAAI,+BAAuB,EAAE,CAAC;QAC5C,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,SAAS,IAAI,GAAG,CAAC,KAAK,CAAC,KAAK,IAAI,CAAC,IAAI,MAAM,CAAC,KAAK,CAAC,KAAK,SAAS,EAAE,CAAC;YACrF,QAAQ,CAAC,KAAK,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,CAAA;QACjC,CAAC;IACH,CAAC;IAED,OAAO,QAAQ,CAAA;AACjB,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,SAAgB,eAAe,CAAC,QAA4B;IAC1D,MAAM,MAAM,GAAG,CAAC,QAAQ,IAAI,EAAE,CAAmB,CAAA;IACjD,MAAM,GAAG,GAAmB,EAAE,CAAA;IAE9B,KAAK,MAAM,KAAK,IAAI,2BAAmB,EAAE,CAAC;QACxC,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,CAAA;QAE3B,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;YACxB,GAAG,CAAC,KAAK,CAAC,GAAG,KAAK,CAAA;QACpB,CAAC;IACH,CAAC;IAED,OAAO,GAAG,CAAA;AACZ,CAAC","sourcesContent":["/**\n * The template fields that belong on the Activity row, named one by one.\n *\n * ── Why not spread the template ───────────────────────────────────────────\n * installActivity used `{ ...(template as any) }`. A template field with no column on Activity was\n * dropped by the save, which reported success — and `as any` meant the types said nothing about it.\n * Nine fields were being handed over with no home: activity, approvalSteps, callback, category,\n * guide, label, provider, readiness, release.\n *\n * `label` was the one that hurt. It is the display name a person reads, and it reached exactly one\n * screen — the catalog, which reads the template registry directly — while every screen reading the\n * row showed the instance name instead. decisionKeys only worked because a column happened to be\n * added for it in the same change; the next field would have been left to the same luck.\n *\n * Naming the fields buys three things: a new template field has to be wired here on purpose, the\n * eight that stay behind are recorded as catalog-only rather than lost, and the two `as any` casts\n * go away. (2026-09-10, architect's ruling)\n *\n * ── What stays in the template registry, and why ──────────────────────────\n * activity the installed row itself, attached by the catalog query. Writing it back would\n * be a row pointing at itself\n * approvalSteps counted per domain by the catalog query from the line that is actually set up\n * callback a function\n * category how the catalog groups it\n * guide what the work is, for the catalog's reader\n * provider which module registered it\n * readiness how far along the registering module says it is\n * release which version registered it\n *\n * Each of those is answered by the registry, which is loaded in every process that serves the\n * catalog. Copying them to the row would make a second copy that goes stale on the next release.\n *\n * ── The template shape is spelled out, not imported ───────────────────────\n * InstallableActivity carries type-graphql decorators, and the test runner strips types without\n * reading them, so importing it takes the whole test file down. That happened three times in one\n * day — to search-key-declaration, to decision-key-declaration, and here.\n */\ntype TemplateFields = { [field: string]: unknown }\nexport const ACTIVITY_ROW_FIELDS = [\n 'name',\n 'label',\n 'description',\n 'activityType',\n 'priority',\n 'startingType',\n 'multiple',\n 'model',\n 'decisionKeys',\n 'uiType',\n 'uiSource',\n 'viewType',\n 'viewSource',\n 'reportType',\n 'reportSource',\n 'thumbnail'\n] as const\n\n/**\n * The template fields that only seed a row: the template's value is the default, and once the row has one it is the\n * domain's - the activity screen edits it, and installing again at every boot does not take it back.\n *\n * standardTime how long the work may take; an instance's due is its issue plus this (issue.ts). Never copied\n * before, so every installed approval had no due (the user, 2026-11-08: 「각 결재에 기한이 없군」).\n */\nexport const ACTIVITY_DEFAULT_FIELDS = ['standardTime'] as const\n\n/** The defaults of a template the row does not have yet. */\nexport function activityDefaultsFrom(template: object | undefined, existing: object | null | undefined): TemplateFields {\n const fields = (template ?? {}) as TemplateFields\n const row = (existing ?? {}) as TemplateFields\n const defaults: TemplateFields = {}\n\n for (const field of ACTIVITY_DEFAULT_FIELDS) {\n if ((row[field] === undefined || row[field] === null) && fields[field] !== undefined) {\n defaults[field] = fields[field]\n }\n }\n\n return defaults\n}\n\n/**\n * The part of a template that is stored on the Activity row.\n *\n * A field the template left undefined is not copied, so installing again does not blank a value\n * somebody edited on the row — `save` with an explicit undefined writes null. That protection is\n * real: the activity list screen lets a domain edit viewType and viewSource on the row, and\n * installActivity runs at every boot. A field the template set to null is copied, because null is\n * how a template says the value is gone.\n *\n * ── So leaving a field out is not the same as clearing it ─────────────────\n * A template that stops providing something has to say so, because silence means \"keep whatever the\n * row has\":\n *\n * viewSource: undefined the template says nothing — the row keeps its value\n * viewSource: null the template says there is none — the row is cleared\n *\n * plant hit this: they removed two viewSource declarations, the rows kept pointing at elements that\n * no longer existed, and approvers met a screen that could not load. (2026-09-11)\n *\n * ── Why null and not an empty string ──────────────────────────────────────\n * The rule was first written as `viewSource: ''`, and for a string field that does clear it. But\n * five of the sixteen fields carried here are not strings — priority is a number, multiple and\n * activityType and startingType are enums, model and decisionKeys are json — and none of them has\n * an empty string. `priority: ''` puts a non-number in an integer column; `decisionKeys: ''` puts a\n * string where the screen expects an array and every declared slot disappears with no error. A rule\n * that only holds for two thirds of the list is the kind that gets applied to the other third.\n *\n * null is the one signal that reads the same on all sixteen, and it says \"there is none\" rather\n * than naming a value that happens to be falsy. It needs nothing here: the copy below asks only\n * whether the template said anything, so null crosses over and the save writes NULL.\n *\n * An empty string still clears a string field — it is a value, and it lands in the column. It is\n * simply not the rule. (2026-09-16, answering the architect)\n */\nexport function activityRowFrom(template: object | undefined): TemplateFields {\n const fields = (template ?? {}) as TemplateFields\n const row: TemplateFields = {}\n\n for (const field of ACTIVITY_ROW_FIELDS) {\n const value = fields[field]\n\n if (value !== undefined) {\n row[field] = value\n }\n }\n\n return row\n}\n"]}
@@ -142,9 +142,14 @@ async function assignToPool(activityInstance, roleIds, dueAt, context) {
142
142
  });
143
143
  /*
144
144
  * A case already in a pool moves to the other pool with its one waiting thread - nobody has taken it, so
145
- * a manager who chose the wrong role puts it right here instead of aborting the case.
145
+ * a manager who chose the wrong role puts it right here instead of aborting the case. Its state does not
146
+ * change, so nothing else saves it: saved whole here, read under the hold, so its history keeps the new
147
+ * pool (ADR-0091 decision 9). A new waiting thread saves the case through the state it works out.
146
148
  */
147
- if (!threads.some(thread => thread.state === activity_thread_js_1.ActivityThreadStatus.Unassigned)) {
149
+ if (threads.some(thread => thread.state === activity_thread_js_1.ActivityThreadStatus.Unassigned)) {
150
+ await repository.save({ ...pooled, transaction: 'assign' });
151
+ }
152
+ else {
148
153
  await (0, common_js_1.createActivityThreadWithUnassigned)('assign', pooled, context);
149
154
  }
150
155
  return await repository.findOneBy({ id });
@@ -1 +1 @@
1
- {"version":3,"file":"assign.js","sourceRoot":"","sources":["../../../server/controllers/activity-instance/assign.ts"],"names":[],"mappings":"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","sourcesContent":["import { In } from 'typeorm'\n\nimport { User } from '@things-factory/auth-base'\nimport { writeLockSupported } from '@things-factory/shell'\n\nimport { ActivityInstance, ActivityInstanceStatus } from '../../service/activity-instance/activity-instance.js'\nimport { ActivityThread, ActivityThreadStatus } from '../../service/activity-thread/activity-thread.js'\nimport { CASE_POOL, poolMembers, resolvePoolRoles } from '../../service/activity-instance/assignee-pool.js'\nimport { createActivityThreadWithUnassigned, createActivityThreadsForUsers } from '../common.js'\n\nexport async function assign(\n {\n id,\n assignees,\n assigneeRoles,\n reason,\n dueAt\n }: { id: string; assignees?: string[]; assigneeRoles?: string[]; reason?: string; dueAt?: Date },\n context: ResolverContext\n): Promise<ActivityInstance> {\n const { domain, user, tx } = context.state\n\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 * People or a pool, not both in one call. Given both, the case would wait for the pool and already be\n * held by the people, and which of the two it means could not be read off the case afterwards.\n */\n if (assigneeRoles?.length && assignees?.length) {\n throw new Error(context.t('error.activity-instance assign people or a role, not both', { id }))\n }\n\n if (assigneeRoles?.length) {\n return await assignToPool(activityInstance, assigneeRoles, dueAt, context)\n }\n\n /*\n Prerequisites for a Task to Be Assigned.\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 if (dueAt) {\n activityInstance.dueAt = dueAt\n }\n\n /*\n * People of this domain only. By email alone a manager could hand the task to someone of another\n * tenant, whose to-do list reads their own domain and would never show it. An address that is not\n * here is named rather than dropped - dropped, the task went to fewer people than were asked.\n */\n const assignedUsers = await tx.getRepository(User).find({\n where: { email: In(assignees || []), domains: { id: domain.id } },\n relations: ['domains']\n })\n\n const missing = (assignees || []).filter(email => !assignedUsers.some(one => one.email === email))\n\n if (missing.length) {\n throw new Error(context.t('error.activity-instance assignee not in this domain', { emails: missing.join(', ') }))\n }\n\n await createActivityThreadsForUsers('assign', activityInstance, assignedUsers, context)\n\n return await tx.getRepository(ActivityInstance).findOneBy({ id: activityInstance.id })\n}\n\n/**\n * Put a case in a pool: whoever holds any one of its roles and picks it first holds it.\n *\n * The pool is what issue() makes when the activity names assignee roles and waits for one person -\n * `assigneeRoles` on the case, `threadsMin` 1, and one unassigned thread. To-do lists and the pickable\n * list read exactly that (assignee-pool.ts), so this makes the same shape and nothing else; a second\n * shape would be a second pool nobody's list reads.\n *\n * Only a case nobody holds - `Issued`, no valid thread (common.ts evalActivityInstanceState). A case\n * someone already holds is theirs; taking it back into a pool is another action, not this one.\n */\nasync function assignToPool(\n activityInstance: ActivityInstance,\n roleIds: string[],\n dueAt: Date | undefined,\n context: ResolverContext\n): Promise<ActivityInstance> {\n const { domain, user, tx } = context.state\n const repository = tx.getRepository(ActivityInstance)\n const { id } = activityInstance\n\n if (activityInstance.state !== ActivityInstanceStatus.Issued) {\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 * Roles of this domain only, each once - the same reason as people above: another tenant's pool is read\n * by nobody here (assignee-pool.ts refuses by name).\n */\n const roles = await resolvePoolRoles(roleIds, context)\n\n /*\n * A pool nobody is in is a case nobody sees. Its people are every holder of any of its roles, counted\n * once each, the way issue() counts them (createActivityThreadsForAllRoleUsers).\n */\n if (!poolMembers(roles).length) {\n throw new Error(context.t('error.activity-instance role has no members', { role: roles.map(role => role.name).join(', ') }))\n }\n\n /*\n * Take hold of the case first, as pick does: the UPDATE matches only a case still issued and holds its\n * row to the end of this transaction. Every way into a waiting case takes this row before its threads -\n * pick's UPDATE, and a member claiming the pool's waiting thread (activity-thread/check-authority.ts) - so\n * whoever is second waits for the first to commit and then reads what it did. One order everywhere, so\n * no two of them deadlock.\n */\n const claimed = await repository.update(\n { domain: { id: domain.id }, id, state: ActivityInstanceStatus.Issued },\n { threadsMin: 1, updater: user, ...(dueAt ? { dueAt } : {}) }\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 * Nobody holds it - asked of the threads too, not only of the state. The state is worked out again only\n * when a thread changes (common.ts updateActivityInstanceState); a thread someone holds under a case still\n * marked issued is theirs, and the refusal rolls the UPDATE above back.\n */\n const threads = await tx.getRepository(ActivityThread).find({\n where: { activityInstance: { id } },\n /* A locking read is a current read - a plain one may answer from the snapshot taken before the wait. */\n ...(writeLockSupported(tx) ? { lock: { mode: 'pessimistic_write' as const } } : {})\n })\n\n if (threads.some(thread => thread.isValid())) {\n throw new Error(context.t('error.activity-approval taken by someone else', { activityApproval: id }))\n }\n\n /*\n * The pool itself is a join table, not a column the UPDATE can set. Written under the hold the UPDATE took,\n * and written whole: every row of this case goes, then the new list. Worked out as a difference from the\n * case read above, it was a difference from a read taken before the hold - two assigns at once on postgres\n * left the union of both lists, the pool of neither (measured 2026-10-20). A DELETE reads what is there\n * now, so the pool is the list of whichever assign held the case last. Not a save - that would write a\n * history row of a half-read case.\n */\n await tx\n .createQueryBuilder()\n .delete()\n .from(CASE_POOL.table)\n .where(`${CASE_POOL.caseColumn} = :id`, { id })\n .execute()\n\n await repository\n .createQueryBuilder()\n .relation(ActivityInstance, 'assigneeRoles')\n .of(id)\n .add(roles.map(role => role.id))\n\n const pooled = await repository.findOne({\n where: { id },\n relations: ['domain', 'activity', 'assigneeRoles', 'supervisoryRole', 'updater', 'creator', 'starter', 'terminator']\n })\n\n /*\n * A case already in a pool moves to the other pool with its one waiting thread - nobody has taken it, so\n * a manager who chose the wrong role puts it right here instead of aborting the case.\n */\n if (!threads.some(thread => thread.state === ActivityThreadStatus.Unassigned)) {\n await createActivityThreadWithUnassigned('assign', pooled, context)\n }\n\n return await repository.findOneBy({ id })\n}\n"]}
1
+ {"version":3,"file":"assign.js","sourceRoot":"","sources":["../../../server/controllers/activity-instance/assign.ts"],"names":[],"mappings":"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","sourcesContent":["import { In } from 'typeorm'\n\nimport { User } from '@things-factory/auth-base'\nimport { writeLockSupported } from '@things-factory/shell'\n\nimport { ActivityInstance, ActivityInstanceStatus } from '../../service/activity-instance/activity-instance.js'\nimport { ActivityThread, ActivityThreadStatus } from '../../service/activity-thread/activity-thread.js'\nimport { CASE_POOL, poolMembers, resolvePoolRoles } from '../../service/activity-instance/assignee-pool.js'\nimport { createActivityThreadWithUnassigned, createActivityThreadsForUsers } from '../common.js'\n\nexport async function assign(\n {\n id,\n assignees,\n assigneeRoles,\n reason,\n dueAt\n }: { id: string; assignees?: string[]; assigneeRoles?: string[]; reason?: string; dueAt?: Date },\n context: ResolverContext\n): Promise<ActivityInstance> {\n const { domain, user, tx } = context.state\n\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 * People or a pool, not both in one call. Given both, the case would wait for the pool and already be\n * held by the people, and which of the two it means could not be read off the case afterwards.\n */\n if (assigneeRoles?.length && assignees?.length) {\n throw new Error(context.t('error.activity-instance assign people or a role, not both', { id }))\n }\n\n if (assigneeRoles?.length) {\n return await assignToPool(activityInstance, assigneeRoles, dueAt, context)\n }\n\n /*\n Prerequisites for a Task to Be Assigned.\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 if (dueAt) {\n activityInstance.dueAt = dueAt\n }\n\n /*\n * People of this domain only. By email alone a manager could hand the task to someone of another\n * tenant, whose to-do list reads their own domain and would never show it. An address that is not\n * here is named rather than dropped - dropped, the task went to fewer people than were asked.\n */\n const assignedUsers = await tx.getRepository(User).find({\n where: { email: In(assignees || []), domains: { id: domain.id } },\n relations: ['domains']\n })\n\n const missing = (assignees || []).filter(email => !assignedUsers.some(one => one.email === email))\n\n if (missing.length) {\n throw new Error(context.t('error.activity-instance assignee not in this domain', { emails: missing.join(', ') }))\n }\n\n await createActivityThreadsForUsers('assign', activityInstance, assignedUsers, context)\n\n return await tx.getRepository(ActivityInstance).findOneBy({ id: activityInstance.id })\n}\n\n/**\n * Put a case in a pool: whoever holds any one of its roles and picks it first holds it.\n *\n * The pool is what issue() makes when the activity names assignee roles and waits for one person -\n * `assigneeRoles` on the case, `threadsMin` 1, and one unassigned thread. To-do lists and the pickable\n * list read exactly that (assignee-pool.ts), so this makes the same shape and nothing else; a second\n * shape would be a second pool nobody's list reads.\n *\n * Only a case nobody holds - `Issued`, no valid thread (common.ts evalActivityInstanceState). A case\n * someone already holds is theirs; taking it back into a pool is another action, not this one.\n */\nasync function assignToPool(\n activityInstance: ActivityInstance,\n roleIds: string[],\n dueAt: Date | undefined,\n context: ResolverContext\n): Promise<ActivityInstance> {\n const { domain, user, tx } = context.state\n const repository = tx.getRepository(ActivityInstance)\n const { id } = activityInstance\n\n if (activityInstance.state !== ActivityInstanceStatus.Issued) {\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 * Roles of this domain only, each once - the same reason as people above: another tenant's pool is read\n * by nobody here (assignee-pool.ts refuses by name).\n */\n const roles = await resolvePoolRoles(roleIds, context)\n\n /*\n * A pool nobody is in is a case nobody sees. Its people are every holder of any of its roles, counted\n * once each, the way issue() counts them (createActivityThreadsForAllRoleUsers).\n */\n if (!poolMembers(roles).length) {\n throw new Error(context.t('error.activity-instance role has no members', { role: roles.map(role => role.name).join(', ') }))\n }\n\n /*\n * Take hold of the case first, as pick does: the UPDATE matches only a case still issued and holds its\n * row to the end of this transaction. Every way into a waiting case takes this row before its threads -\n * pick's UPDATE, and a member claiming the pool's waiting thread (activity-thread/check-authority.ts) - so\n * whoever is second waits for the first to commit and then reads what it did. One order everywhere, so\n * no two of them deadlock.\n */\n const claimed = await repository.update(\n { domain: { id: domain.id }, id, state: ActivityInstanceStatus.Issued },\n { threadsMin: 1, updater: user, ...(dueAt ? { dueAt } : {}) }\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 * Nobody holds it - asked of the threads too, not only of the state. The state is worked out again only\n * when a thread changes (common.ts updateActivityInstanceState); a thread someone holds under a case still\n * marked issued is theirs, and the refusal rolls the UPDATE above back.\n */\n const threads = await tx.getRepository(ActivityThread).find({\n where: { activityInstance: { id } },\n /* A locking read is a current read - a plain one may answer from the snapshot taken before the wait. */\n ...(writeLockSupported(tx) ? { lock: { mode: 'pessimistic_write' as const } } : {})\n })\n\n if (threads.some(thread => thread.isValid())) {\n throw new Error(context.t('error.activity-approval taken by someone else', { activityApproval: id }))\n }\n\n /*\n * The pool itself is a join table, not a column the UPDATE can set. Written under the hold the UPDATE took,\n * and written whole: every row of this case goes, then the new list. Worked out as a difference from the\n * case read above, it was a difference from a read taken before the hold - two assigns at once on postgres\n * left the union of both lists, the pool of neither (measured 2026-10-20). A DELETE reads what is there\n * now, so the pool is the list of whichever assign held the case last. Not a save - that would write a\n * history row of a half-read case.\n */\n await tx\n .createQueryBuilder()\n .delete()\n .from(CASE_POOL.table)\n .where(`${CASE_POOL.caseColumn} = :id`, { id })\n .execute()\n\n await repository\n .createQueryBuilder()\n .relation(ActivityInstance, 'assigneeRoles')\n .of(id)\n .add(roles.map(role => role.id))\n\n const pooled = await repository.findOne({\n where: { id },\n relations: ['domain', 'activity', 'assigneeRoles', 'supervisoryRole', 'updater', 'creator', 'starter', 'terminator']\n })\n\n /*\n * A case already in a pool moves to the other pool with its one waiting thread - nobody has taken it, so\n * a manager who chose the wrong role puts it right here instead of aborting the case. Its state does not\n * change, so nothing else saves it: saved whole here, read under the hold, so its history keeps the new\n * pool (ADR-0091 decision 9). A new waiting thread saves the case through the state it works out.\n */\n if (threads.some(thread => thread.state === ActivityThreadStatus.Unassigned)) {\n await repository.save({ ...pooled, transaction: 'assign' })\n } else {\n await createActivityThreadWithUnassigned('assign', pooled, context)\n }\n\n return await repository.findOneBy({ id })\n}\n"]}
@@ -0,0 +1,13 @@
1
+ import { ActivityInstance } from '../../service/activity-instance/activity-instance.js';
2
+ /**
3
+ * Ends a case whose work is done - every thread still open ends as done, then the case ends. The counterpart of
4
+ * `abort` for a to-do the records show finished: a case handed to several people, or to whoever holds a role, is
5
+ * done for all of them when its work is done, and its history says ended, not aborted.
6
+ *
7
+ * For the module that issued the case and judges its work (an application's own to-do); a person ends their own
8
+ * thread through the thread's `end`.
9
+ */
10
+ export declare function complete({ id, reason }: {
11
+ id: string;
12
+ reason?: string;
13
+ }, context: ResolverContext): Promise<ActivityInstance>;
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.complete = complete;
4
+ const typeorm_1 = require("typeorm");
5
+ const activity_thread_js_1 = require("../../service/activity-thread/activity-thread.js");
6
+ const activity_instance_js_1 = require("../../service/activity-instance/activity-instance.js");
7
+ const common_js_1 = require("../common.js");
8
+ /**
9
+ * Ends a case whose work is done - every thread still open ends as done, then the case ends. The counterpart of
10
+ * `abort` for a to-do the records show finished: a case handed to several people, or to whoever holds a role, is
11
+ * done for all of them when its work is done, and its history says ended, not aborted.
12
+ *
13
+ * For the module that issued the case and judges its work (an application's own to-do); a person ends their own
14
+ * thread through the thread's `end`.
15
+ */
16
+ async function complete({ id, reason }, context) {
17
+ const { domain, user, tx } = context.state;
18
+ const repository = tx.getRepository(activity_instance_js_1.ActivityInstance);
19
+ const activityInstance = await repository.findOne({
20
+ where: { domain: { id: domain.id }, id },
21
+ relations: ['domain', 'activity', 'activityThreads', 'assigneeRoles', 'supervisoryRole', 'updater', 'creator', 'starter', 'terminator']
22
+ });
23
+ if (!activityInstance) {
24
+ throw new Error(context.t('error.activity-instance not found', { activityInstance: id }));
25
+ }
26
+ if (activityInstance.state == activity_instance_js_1.ActivityInstanceStatus.Aborted || activityInstance.state == activity_instance_js_1.ActivityInstanceStatus.Ended) {
27
+ throw new Error(context.t(`error.activity-instance is already terminated`, { id, state: activityInstance.state }));
28
+ }
29
+ const TERMINATED = [activity_thread_js_1.ActivityThreadStatus.Aborted, activity_thread_js_1.ActivityThreadStatus.Ended];
30
+ const open = (activityInstance.activityThreads ?? []).filter(thread => !TERMINATED.includes(thread.state));
31
+ const now = new Date();
32
+ if (open.length) {
33
+ const threads = tx.getRepository(activity_thread_js_1.ActivityThread);
34
+ for (const thread of await threads.find({ where: { id: (0, typeorm_1.In)(open.map(one => one.id)) } })) {
35
+ await threads.save({
36
+ ...thread,
37
+ reason,
38
+ state: activity_thread_js_1.ActivityThreadStatus.Ended,
39
+ transaction: 'end',
40
+ startedAt: thread.startedAt ?? now,
41
+ updater: user,
42
+ terminatedAt: now
43
+ });
44
+ }
45
+ }
46
+ return await repository.save({
47
+ ...activityInstance,
48
+ activityThreads: undefined,
49
+ reason,
50
+ ...(await (0, common_js_1.evalActivityInstanceState)(id, context)),
51
+ startedAt: activityInstance.startedAt ?? now,
52
+ transaction: 'end',
53
+ updater: user,
54
+ terminatedAt: now,
55
+ terminator: user
56
+ });
57
+ }
58
+ //# sourceMappingURL=complete.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"complete.js","sourceRoot":"","sources":["../../../server/controllers/activity-instance/complete.ts"],"names":[],"mappings":";;AAcA,4BA8CC;AA5DD,qCAA4B;AAE5B,yFAAuG;AACvG,+FAA+G;AAC/G,4CAAwD;AAExD;;;;;;;GAOG;AACI,KAAK,UAAU,QAAQ,CAAC,EAAE,EAAE,EAAE,MAAM,EAAmC,EAAE,OAAwB;IACtG,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,MAAM,gBAAgB,GAAG,MAAM,UAAU,CAAC,OAAO,CAAC;QAChD,KAAK,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE;QACxC,SAAS,EAAE,CAAC,QAAQ,EAAE,UAAU,EAAE,iBAAiB,EAAE,eAAe,EAAE,iBAAiB,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE,YAAY,CAAC;KACxI,CAAC,CAAA;IAEF,IAAI,CAAC,gBAAgB,EAAE,CAAC;QACtB,MAAM,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,mCAAmC,EAAE,EAAE,gBAAgB,EAAE,EAAE,EAAE,CAAC,CAAC,CAAA;IAC3F,CAAC;IAED,IAAI,gBAAgB,CAAC,KAAK,IAAI,6CAAsB,CAAC,OAAO,IAAI,gBAAgB,CAAC,KAAK,IAAI,6CAAsB,CAAC,KAAK,EAAE,CAAC;QACvH,MAAM,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,+CAA+C,EAAE,EAAE,EAAE,EAAE,KAAK,EAAE,gBAAgB,CAAC,KAAK,EAAE,CAAC,CAAC,CAAA;IACpH,CAAC;IAED,MAAM,UAAU,GAAG,CAAC,yCAAoB,CAAC,OAAO,EAAE,yCAAoB,CAAC,KAAK,CAAC,CAAA;IAC7E,MAAM,IAAI,GAAG,CAAC,gBAAgB,CAAC,eAAe,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,UAAU,CAAC,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAA;IAC1G,MAAM,GAAG,GAAG,IAAI,IAAI,EAAE,CAAA;IACtB,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;QAChB,MAAM,OAAO,GAAG,EAAE,CAAC,aAAa,CAAC,mCAAc,CAAC,CAAA;QAChD,KAAK,MAAM,MAAM,IAAI,MAAM,OAAO,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE,IAAA,YAAE,EAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC;YACxF,MAAM,OAAO,CAAC,IAAI,CAAC;gBACjB,GAAG,MAAM;gBACT,MAAM;gBACN,KAAK,EAAE,yCAAoB,CAAC,KAAK;gBACjC,WAAW,EAAE,KAAK;gBAClB,SAAS,EAAE,MAAM,CAAC,SAAS,IAAI,GAAG;gBAClC,OAAO,EAAE,IAAI;gBACb,YAAY,EAAE,GAAG;aAClB,CAAC,CAAA;QACJ,CAAC;IACH,CAAC;IAED,OAAO,MAAM,UAAU,CAAC,IAAI,CAAC;QAC3B,GAAG,gBAAgB;QACnB,eAAe,EAAE,SAAS;QAC1B,MAAM;QACN,GAAG,CAAC,MAAM,IAAA,qCAAyB,EAAC,EAAE,EAAE,OAAO,CAAC,CAAC;QACjD,SAAS,EAAE,gBAAgB,CAAC,SAAS,IAAI,GAAG;QAC5C,WAAW,EAAE,KAAK;QAClB,OAAO,EAAE,IAAI;QACb,YAAY,EAAE,GAAG;QACjB,UAAU,EAAE,IAAI;KACV,CAAC,CAAA;AACX,CAAC","sourcesContent":["import { In } from 'typeorm'\n\nimport { ActivityThread, ActivityThreadStatus } from '../../service/activity-thread/activity-thread.js'\nimport { ActivityInstance, ActivityInstanceStatus } from '../../service/activity-instance/activity-instance.js'\nimport { evalActivityInstanceState } from '../common.js'\n\n/**\n * Ends a case whose work is done - every thread still open ends as done, then the case ends. The counterpart of\n * `abort` for a to-do the records show finished: a case handed to several people, or to whoever holds a role, is\n * done for all of them when its work is done, and its history says ended, not aborted.\n *\n * For the module that issued the case and judges its work (an application's own to-do); a person ends their own\n * thread through the thread's `end`.\n */\nexport async function complete({ id, reason }: { id: string; reason?: string }, context: ResolverContext): Promise<ActivityInstance> {\n const { domain, user, tx } = context.state\n const repository = tx.getRepository(ActivityInstance)\n\n const activityInstance = await repository.findOne({\n where: { domain: { id: domain.id }, id },\n relations: ['domain', 'activity', 'activityThreads', 'assigneeRoles', 'supervisoryRole', 'updater', 'creator', 'starter', 'terminator']\n })\n\n if (!activityInstance) {\n throw new Error(context.t('error.activity-instance not found', { activityInstance: id }))\n }\n\n if (activityInstance.state == ActivityInstanceStatus.Aborted || activityInstance.state == ActivityInstanceStatus.Ended) {\n throw new Error(context.t(`error.activity-instance is already terminated`, { id, state: activityInstance.state }))\n }\n\n const TERMINATED = [ActivityThreadStatus.Aborted, ActivityThreadStatus.Ended]\n const open = (activityInstance.activityThreads ?? []).filter(thread => !TERMINATED.includes(thread.state))\n const now = new Date()\n if (open.length) {\n const threads = tx.getRepository(ActivityThread)\n for (const thread of await threads.find({ where: { id: In(open.map(one => one.id)) } })) {\n await threads.save({\n ...thread,\n reason,\n state: ActivityThreadStatus.Ended,\n transaction: 'end',\n startedAt: thread.startedAt ?? now,\n updater: user,\n terminatedAt: now\n })\n }\n }\n\n return await repository.save({\n ...activityInstance,\n activityThreads: undefined,\n reason,\n ...(await evalActivityInstanceState(id, context)),\n startedAt: activityInstance.startedAt ?? now,\n transaction: 'end',\n updater: user,\n terminatedAt: now,\n terminator: user\n } as any)\n}\n"]}
@@ -4,3 +4,4 @@ export * from './pick.js';
4
4
  export * from './assign.js';
5
5
  export * from './abort.js';
6
6
  export * from './end.js';
7
+ export * from './complete.js';
@@ -7,4 +7,5 @@ tslib_1.__exportStar(require("./pick.js"), exports);
7
7
  tslib_1.__exportStar(require("./assign.js"), exports);
8
8
  tslib_1.__exportStar(require("./abort.js"), exports);
9
9
  tslib_1.__exportStar(require("./end.js"), exports);
10
+ tslib_1.__exportStar(require("./complete.js"), exports);
10
11
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../server/controllers/activity-instance/index.ts"],"names":[],"mappings":";;;AAAA,qDAA0B;AAC1B,qDAA0B;AAC1B,oDAAyB;AACzB,sDAA2B;AAC3B,qDAA0B;AAC1B,mDAAwB","sourcesContent":["export * from './draft.js'\nexport * from './issue.js'\nexport * from './pick.js'\nexport * from './assign.js'\nexport * from './abort.js'\nexport * from './end.js'\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../server/controllers/activity-instance/index.ts"],"names":[],"mappings":";;;AAAA,qDAA0B;AAC1B,qDAA0B;AAC1B,oDAAyB;AACzB,sDAA2B;AAC3B,qDAA0B;AAC1B,mDAAwB;AACxB,wDAA6B","sourcesContent":["export * from './draft.js'\nexport * from './issue.js'\nexport * from './pick.js'\nexport * from './assign.js'\nexport * from './abort.js'\nexport * from './end.js'\nexport * from './complete.js'\n"]}
@@ -1,4 +1,5 @@
1
1
  import { Domain } from '@things-factory/shell';
2
+ import type { NoticeDelivery } from '@things-factory/notification';
2
3
  import { Role, User } from '@things-factory/auth-base';
3
4
  import { ActivityThread } from '../activity-thread/activity-thread.js';
4
5
  export declare enum ActivityApprovalJudgment {
@@ -55,6 +56,8 @@ export declare class ActivityApproval {
55
56
  */
56
57
  commentedAt?: Date;
57
58
  transaction?: string;
59
+ /** What each notice of this seat sent (ADR-0164) - mail goes after the request is over, so this is where it is known. */
60
+ notices?: NoticeDelivery[];
58
61
  createdAt?: Date;
59
62
  updatedAt?: Date;
60
63
  terminatedAt?: Date;
@@ -127,6 +127,14 @@ tslib_1.__decorate([
127
127
  (0, type_graphql_1.Field)({ nullable: true }),
128
128
  tslib_1.__metadata("design:type", String)
129
129
  ], ActivityApproval.prototype, "transaction", void 0);
130
+ tslib_1.__decorate([
131
+ (0, typeorm_1.Column)('simple-json', { nullable: true }),
132
+ (0, type_graphql_1.Field)(type => shell_1.ScalarObject, {
133
+ nullable: true,
134
+ description: 'Each notice told of this seat - [{ at, kind: request | reference | delegated | approved | rejected, receivers, email: { sent, failed }, teams: { sent, failed } }], the newest last.'
135
+ }),
136
+ tslib_1.__metadata("design:type", Array)
137
+ ], ActivityApproval.prototype, "notices", void 0);
130
138
  tslib_1.__decorate([
131
139
  (0, typeorm_1.CreateDateColumn)(),
132
140
  (0, type_graphql_1.Field)({ nullable: true }),
@@ -1 +1 @@
1
- {"version":3,"file":"activity-approval.js","sourceRoot":"","sources":["../../../server/service/activity-approval/activity-approval.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n CreateDateColumn,\n UpdateDateColumn,\n Entity,\n Index,\n Column,\n RelationId,\n ManyToOne,\n OneToMany,\n PrimaryGeneratedColumn\n} from 'typeorm'\nimport { ObjectType, Field, Int, ID, registerEnumType } from 'type-graphql'\n\nimport { Domain } from '@things-factory/shell'\nimport { Role, User } from '@things-factory/auth-base'\nimport { ActivityThread } from '../activity-thread/activity-thread.js'\n\nexport enum ActivityApprovalJudgment {\n Pending = '',\n Rejected = 'rejected',\n Escalated = 'escalated',\n Delegated = 'delegated',\n Approved = 'approved',\n Aborted = 'aborted',\n /**\n * **건이 끝나서 닫혔다** — 판정한 것이 아니다.\n *\n * 참조는 결재하지 않으므로 승인·반려로 끝나지 않는다. 건이 끝나면 이 값으로 닫는다.\n *\n * 의견을 냈는지는 `comment` 가 말한다. 비어 있으면 **「통지받았고 말하지 않았다」**이고,\n * 그것은 동의가 아니다 — 나중에 결재 이력을 읽는 사람이 침묵을 동의로 읽으면 안 된다.\n */\n Closed = 'closed'\n}\n\nregisterEnumType(ActivityApprovalJudgment, {\n name: 'ActivityApprovalJudgment',\n description: 'judgment enumeration of a activityApproval'\n})\n\n@Entity()\n@Index(\n 'ix_activity_approval_0',\n (activityApproval: ActivityApproval) => [\n activityApproval.domain,\n activityApproval.activityThread,\n activityApproval.order,\n activityApproval.approver,\n activityApproval.createdAt\n ],\n { unique: true }\n)\n@ObjectType({ description: 'Entity for ActivityApproval' })\nexport class ActivityApproval {\n @PrimaryGeneratedColumn('uuid')\n @Field(type => ID)\n readonly id: string\n\n @ManyToOne(type => Domain)\n @Field(type => Domain)\n domain?: Domain\n\n @RelationId((activityApproval: ActivityApproval) => activityApproval.domain)\n domainId?: string\n\n @ManyToOne(type => ActivityApproval, { nullable: true })\n @Field(type => ActivityApproval, { nullable: true, description: 'delegator approval' })\n originalApproval?: ActivityApproval\n\n @RelationId((activityApproval: ActivityApproval) => activityApproval.originalApproval)\n originalApprovalId?: string\n\n @OneToMany(type => ActivityApproval, activityApproval => activityApproval.originalApproval)\n @Field(type => [ActivityApproval])\n derivedApprovals?: ActivityApproval\n\n @ManyToOne(type => ActivityThread, { nullable: true })\n @Field(type => ActivityThread, { nullable: true })\n activityThread?: ActivityThread\n\n @RelationId((activityApproval: ActivityApproval) => activityApproval.activityThread)\n activityThreadId?: string\n\n @Column({ nullable: true, default: 1 })\n @Field({ nullable: true })\n round: number = 1\n\n @Column({ nullable: true, default: 0 })\n @Field({ nullable: true, description: 'Nth order approval for the given activity thread' })\n order?: number = 0\n\n /**\n * **무슨 자격으로 서 있나** — `approval` 이면 결재하고 `reference` 면 의견만 낸다.\n *\n * 비어 있으면 결재다. 지금 쌓여 있는 행에는 이 칸이 없고, 그 행들이 전부 결재로 읽혀야 거동이 안 바뀐다.\n *\n * 목록 질의가 이 칸을 본다 — 참조 행이 승인 대기 목록에 나오면 그 사람에게 **승인하라고 요구하는**\n * 것이 되고, 참조의 뜻이 사라진다.\n */\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'In what capacity: approval decides, reference only comments. Empty means approval.' })\n participation?: string\n\n @ManyToOne(type => User, { nullable: true })\n @Field(type => User, { nullable: true })\n approver?: User\n\n @RelationId((activityApproval: ActivityApproval) => activityApproval.approver)\n approverId?: string\n\n @ManyToOne(type => Role, { nullable: true })\n @Field(type => Role, { nullable: true })\n approverRole?: Role\n\n @RelationId((activityApproval: ActivityApproval) => activityApproval.approverRole)\n approverRoleId?: string\n\n @Column({ nullable: true, default: '' })\n @Field({ nullable: true })\n judgment?: ActivityApprovalJudgment = ActivityApprovalJudgment.Pending\n\n @Column({ nullable: true })\n @Field({ nullable: true })\n comment?: string\n\n /**\n * 의견을 **언제 말했나** — 파생으로 알 수 없다.\n *\n * 화면이 「결정이 끝난 뒤에 달린 의견」을 표시하려면 이 값이 있어야 한다. `updatedAt` 과\n * `terminatedAt` 을 견주는 것으로는 안 된다 — 닫는 코드가 `terminatedAt` 을 저장 전에 만들고\n * `updatedAt` 은 DB 가 쓰는 순간 찍으므로, 닫힌 행은 **전부** 「나중에 바뀌었다」로 보인다.\n *\n * 비어 있으면 말하지 않은 것이다. 침묵은 동의도 반대도 아니다.\n */\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'When the opinion was said. Empty means nothing was said — silence is not consent.' })\n commentedAt?: Date\n\n @Column({ nullable: true })\n @Field({ nullable: true })\n transaction?: string\n\n @CreateDateColumn()\n @Field({ nullable: true })\n createdAt?: Date\n\n @UpdateDateColumn()\n @Field({ nullable: true })\n updatedAt?: Date\n\n @Column({ nullable: true })\n @Field({ nullable: true })\n terminatedAt?: Date\n\n @ManyToOne(type => User, { nullable: true })\n @Field(type => User, { nullable: true })\n creator?: User\n\n @RelationId((activityApproval: ActivityApproval) => activityApproval.creator)\n creatorId?: string\n\n @ManyToOne(type => User, { nullable: true })\n @Field(type => User, { nullable: true })\n updater?: User\n\n @RelationId((activityApproval: ActivityApproval) => activityApproval.updater)\n updaterId?: string\n\n @ManyToOne(type => User, { nullable: true })\n @Field({ nullable: true })\n terminator?: User\n\n @RelationId((activityApproval: ActivityApproval) => activityApproval.terminator)\n terminatorId?: string\n}\n"]}
1
+ {"version":3,"file":"activity-approval.js","sourceRoot":"","sources":["../../../server/service/activity-approval/activity-approval.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n CreateDateColumn,\n UpdateDateColumn,\n Entity,\n Index,\n Column,\n RelationId,\n ManyToOne,\n OneToMany,\n PrimaryGeneratedColumn\n} from 'typeorm'\nimport { ObjectType, Field, Int, ID, registerEnumType } from 'type-graphql'\n\nimport { Domain, ScalarObject } from '@things-factory/shell'\nimport type { NoticeDelivery } from '@things-factory/notification'\nimport { Role, User } from '@things-factory/auth-base'\nimport { ActivityThread } from '../activity-thread/activity-thread.js'\n\nexport enum ActivityApprovalJudgment {\n Pending = '',\n Rejected = 'rejected',\n Escalated = 'escalated',\n Delegated = 'delegated',\n Approved = 'approved',\n Aborted = 'aborted',\n /**\n * **건이 끝나서 닫혔다** — 판정한 것이 아니다.\n *\n * 참조는 결재하지 않으므로 승인·반려로 끝나지 않는다. 건이 끝나면 이 값으로 닫는다.\n *\n * 의견을 냈는지는 `comment` 가 말한다. 비어 있으면 **「통지받았고 말하지 않았다」**이고,\n * 그것은 동의가 아니다 — 나중에 결재 이력을 읽는 사람이 침묵을 동의로 읽으면 안 된다.\n */\n Closed = 'closed'\n}\n\nregisterEnumType(ActivityApprovalJudgment, {\n name: 'ActivityApprovalJudgment',\n description: 'judgment enumeration of a activityApproval'\n})\n\n@Entity()\n@Index(\n 'ix_activity_approval_0',\n (activityApproval: ActivityApproval) => [\n activityApproval.domain,\n activityApproval.activityThread,\n activityApproval.order,\n activityApproval.approver,\n activityApproval.createdAt\n ],\n { unique: true }\n)\n@ObjectType({ description: 'Entity for ActivityApproval' })\nexport class ActivityApproval {\n @PrimaryGeneratedColumn('uuid')\n @Field(type => ID)\n readonly id: string\n\n @ManyToOne(type => Domain)\n @Field(type => Domain)\n domain?: Domain\n\n @RelationId((activityApproval: ActivityApproval) => activityApproval.domain)\n domainId?: string\n\n @ManyToOne(type => ActivityApproval, { nullable: true })\n @Field(type => ActivityApproval, { nullable: true, description: 'delegator approval' })\n originalApproval?: ActivityApproval\n\n @RelationId((activityApproval: ActivityApproval) => activityApproval.originalApproval)\n originalApprovalId?: string\n\n @OneToMany(type => ActivityApproval, activityApproval => activityApproval.originalApproval)\n @Field(type => [ActivityApproval])\n derivedApprovals?: ActivityApproval\n\n @ManyToOne(type => ActivityThread, { nullable: true })\n @Field(type => ActivityThread, { nullable: true })\n activityThread?: ActivityThread\n\n @RelationId((activityApproval: ActivityApproval) => activityApproval.activityThread)\n activityThreadId?: string\n\n @Column({ nullable: true, default: 1 })\n @Field({ nullable: true })\n round: number = 1\n\n @Column({ nullable: true, default: 0 })\n @Field({ nullable: true, description: 'Nth order approval for the given activity thread' })\n order?: number = 0\n\n /**\n * **무슨 자격으로 서 있나** — `approval` 이면 결재하고 `reference` 면 의견만 낸다.\n *\n * 비어 있으면 결재다. 지금 쌓여 있는 행에는 이 칸이 없고, 그 행들이 전부 결재로 읽혀야 거동이 안 바뀐다.\n *\n * 목록 질의가 이 칸을 본다 — 참조 행이 승인 대기 목록에 나오면 그 사람에게 **승인하라고 요구하는**\n * 것이 되고, 참조의 뜻이 사라진다.\n */\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'In what capacity: approval decides, reference only comments. Empty means approval.' })\n participation?: string\n\n @ManyToOne(type => User, { nullable: true })\n @Field(type => User, { nullable: true })\n approver?: User\n\n @RelationId((activityApproval: ActivityApproval) => activityApproval.approver)\n approverId?: string\n\n @ManyToOne(type => Role, { nullable: true })\n @Field(type => Role, { nullable: true })\n approverRole?: Role\n\n @RelationId((activityApproval: ActivityApproval) => activityApproval.approverRole)\n approverRoleId?: string\n\n @Column({ nullable: true, default: '' })\n @Field({ nullable: true })\n judgment?: ActivityApprovalJudgment = ActivityApprovalJudgment.Pending\n\n @Column({ nullable: true })\n @Field({ nullable: true })\n comment?: string\n\n /**\n * 의견을 **언제 말했나** — 파생으로 알 수 없다.\n *\n * 화면이 「결정이 끝난 뒤에 달린 의견」을 표시하려면 이 값이 있어야 한다. `updatedAt` 과\n * `terminatedAt` 을 견주는 것으로는 안 된다 — 닫는 코드가 `terminatedAt` 을 저장 전에 만들고\n * `updatedAt` 은 DB 가 쓰는 순간 찍으므로, 닫힌 행은 **전부** 「나중에 바뀌었다」로 보인다.\n *\n * 비어 있으면 말하지 않은 것이다. 침묵은 동의도 반대도 아니다.\n */\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'When the opinion was said. Empty means nothing was said — silence is not consent.' })\n commentedAt?: Date\n\n @Column({ nullable: true })\n @Field({ nullable: true })\n transaction?: string\n\n /** What each notice of this seat sent (ADR-0164) - mail goes after the request is over, so this is where it is known. */\n @Column('simple-json', { nullable: true })\n @Field(type => ScalarObject, {\n nullable: true,\n description: 'Each notice told of this seat - [{ at, kind: request | reference | delegated | approved | rejected, receivers, email: { sent, failed }, teams: { sent, failed } }], the newest last.'\n })\n notices?: NoticeDelivery[]\n\n @CreateDateColumn()\n @Field({ nullable: true })\n createdAt?: Date\n\n @UpdateDateColumn()\n @Field({ nullable: true })\n updatedAt?: Date\n\n @Column({ nullable: true })\n @Field({ nullable: true })\n terminatedAt?: Date\n\n @ManyToOne(type => User, { nullable: true })\n @Field(type => User, { nullable: true })\n creator?: User\n\n @RelationId((activityApproval: ActivityApproval) => activityApproval.creator)\n creatorId?: string\n\n @ManyToOne(type => User, { nullable: true })\n @Field(type => User, { nullable: true })\n updater?: User\n\n @RelationId((activityApproval: ActivityApproval) => activityApproval.updater)\n updaterId?: string\n\n @ManyToOne(type => User, { nullable: true })\n @Field({ nullable: true })\n terminator?: User\n\n @RelationId((activityApproval: ActivityApproval) => activityApproval.terminator)\n terminatorId?: string\n}\n"]}
@@ -18,4 +18,5 @@ export declare class ActivityInstanceSubscriber implements EntitySubscriberInter
18
18
  export declare class ActivityInstanceHistoryEntitySubscriber extends HistoryEntitySubscriber<ActivityInstance, ActivityInstanceHistory> {
19
19
  get entity(): typeof ActivityInstance;
20
20
  get historyEntity(): typeof ActivityInstanceHistory;
21
+ afterUpdate(event: UpdateEvent<ActivityInstance>): Promise<void>;
21
22
  }
@@ -51,6 +51,17 @@ let ActivityInstanceHistoryEntitySubscriber = class ActivityInstanceHistoryEntit
51
51
  get historyEntity() {
52
52
  return activity_instance_history_js_1.ActivityInstanceHistory;
53
53
  }
54
+ /*
55
+ * A query-builder UPDATE (repository.update) hands subscribers only the columns it set - no id, no name.
56
+ * That is not a case, and a history row of it is refused by the table (name is NOT NULL): every claim that
57
+ * takes a case's row that way - pick, assign to a pool, claimDraft - failed whenever this subscriber was on.
58
+ * Those claims write no history of their own; the save each of them makes afterwards writes the case whole.
59
+ */
60
+ async afterUpdate(event) {
61
+ if (!event.entity?.id)
62
+ return;
63
+ await super.afterUpdate(event);
64
+ }
54
65
  };
55
66
  exports.ActivityInstanceHistoryEntitySubscriber = ActivityInstanceHistoryEntitySubscriber;
56
67
  exports.ActivityInstanceHistoryEntitySubscriber = ActivityInstanceHistoryEntitySubscriber = tslib_1.__decorate([
@@ -1 +1 @@
1
- {"version":3,"file":"event-subscriber.js","sourceRoot":"","sources":["../../../server/service/activity-instance/event-subscriber.ts"],"names":[],"mappings":";;;AAgBA,kCAYC;;AA5BD,qCAA6G;AAE7G,8DAAkE;AAClE,iDAA8C;AAE9C,iEAAyD;AACzD,iFAAwE;AACxE,yDAAyD;AAEzD;;;;;;GAMG;AACI,KAAK,UAAU,WAAW,CAAC,gBAAkC,EAAE,OAAsB;IAC1F,IAAI,CAAC,gBAAgB,EAAE,EAAE;QAAE,OAAO,gBAAgB,CAAA;IAClD,IAAI,gBAAgB,CAAC,eAAe,IAAI,gBAAgB,CAAC,aAAa,EAAE,CAAC;QACvE,OAAO,MAAM,CAAC,MAAM,CAAC,gBAAgB,EAAE,EAAE,eAAe,EAAE,IAAA,4BAAS,EAAC,gBAAgB,CAAC,EAAE,CAAC,CAAA;IAC1F,CAAC;IAED,MAAM,IAAI,GAA2B,MAAM,OAAO,CAAC,KAAK,CACtD,UAAU,4BAAS,CAAC,UAAU,qBAAqB,4BAAS,CAAC,KAAK,UAAU,4BAAS,CAAC,UAAU,MAAM,OAAO,CAAC,UAAU,CAAC,MAAM,CAAC,eAAe,CAAC,IAAI,EAAE,CAAC,CAAC,EAAE,EAC1J,CAAC,gBAAgB,CAAC,EAAE,CAAC,CACtB,CAAA;IAED,OAAO,MAAM,CAAC,MAAM,CAAC,gBAAgB,EAAE,EAAE,eAAe,EAAE,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAA;AAC5F,CAAC;AAGM,IAAM,0BAA0B,GAAhC,MAAM,0BAA0B;IACrC,QAAQ;QACN,OAAO,uCAAgB,CAAA;IACzB,CAAC;IAED,KAAK,CAAC,WAAW,CAAC,KAAoC;QACpD,cAAM,CAAC,OAAO,CAAC,mBAAmB,EAAE;YAClC,gBAAgB,EAAE,MAAM,WAAW,CAAC,KAAK,CAAC,MAA0B,EAAE,KAAK,CAAC,OAAO,CAAC;SACrF,CAAC,CAAA;IACJ,CAAC;IAED,KAAK,CAAC,WAAW,CAAC,KAAoC;QACpD,cAAM,CAAC,OAAO,CAAC,mBAAmB,EAAE;YAClC,gBAAgB,EAAE,MAAM,WAAW,CAAC,KAAK,CAAC,MAA0B,EAAE,KAAK,CAAC,OAAO,CAAC;SACrF,CAAC,CAAA;IACJ,CAAC;CACF,CAAA;AAhBY,gEAA0B;qCAA1B,0BAA0B;IADtC,IAAA,yBAAe,GAAE;GACL,0BAA0B,CAgBtC;AAGM,IAAM,uCAAuC,GAA7C,MAAM,uCAAwC,SAAQ,yCAG5D;IACC,IAAW,MAAM;QACf,OAAO,uCAAgB,CAAA;IACzB,CAAC;IAED,IAAW,aAAa;QACtB,OAAO,sDAAuB,CAAA;IAChC,CAAC;CAEF,CAAA;AAZY,0FAAuC;kDAAvC,uCAAuC;IADnD,IAAA,yBAAe,GAAE;GACL,uCAAuC,CAYnD","sourcesContent":["import { EntityManager, EventSubscriber, EntitySubscriberInterface, InsertEvent, UpdateEvent } from 'typeorm'\n\nimport { HistoryEntitySubscriber } from '@operato/typeorm-history'\nimport { pubsub } from '@things-factory/shell'\n\nimport { ActivityInstance } from './activity-instance.js'\nimport { ActivityInstanceHistory } from './activity-instance-history.js'\nimport { CASE_POOL, poolIdsOf } from './assignee-pool.js'\n\n/**\n * The pool's role ids on what is published - the subscription filter decides by them (subscription-filters.ts).\n *\n * A many-to-many is not a column: the saved object carries the roles only when the caller had them, and its\n * RelationId is not filled in on a write. Asked here once per event when it is missing, never once per\n * subscriber. An update with no entity (a query-builder UPDATE) publishes nothing to read, as before.\n */\nexport async function withPoolIds(activityInstance: ActivityInstance, manager: EntityManager): Promise<ActivityInstance> {\n if (!activityInstance?.id) return activityInstance\n if (activityInstance.assigneeRoleIds || activityInstance.assigneeRoles) {\n return Object.assign(activityInstance, { assigneeRoleIds: poolIdsOf(activityInstance) })\n }\n\n const rows: { roles_id: string }[] = await manager.query(\n `SELECT ${CASE_POOL.roleColumn} AS roles_id FROM ${CASE_POOL.table} WHERE ${CASE_POOL.caseColumn} = ${manager.connection.driver.createParameter('id', 0)}`,\n [activityInstance.id]\n )\n\n return Object.assign(activityInstance, { assigneeRoleIds: rows.map(row => row.roles_id) })\n}\n\n@EventSubscriber()\nexport class ActivityInstanceSubscriber implements EntitySubscriberInterface<ActivityInstance> {\n listenTo() {\n return ActivityInstance\n }\n\n async afterInsert(event: InsertEvent<ActivityInstance>): Promise<any | void> {\n pubsub.publish('activity-instance', {\n activityInstance: await withPoolIds(event.entity as ActivityInstance, event.manager)\n })\n }\n\n async afterUpdate(event: UpdateEvent<ActivityInstance>): Promise<any | void> {\n pubsub.publish('activity-instance', {\n activityInstance: await withPoolIds(event.entity as ActivityInstance, event.manager)\n })\n }\n}\n\n@EventSubscriber()\nexport class ActivityInstanceHistoryEntitySubscriber extends HistoryEntitySubscriber<\n ActivityInstance,\n ActivityInstanceHistory\n> {\n public get entity() {\n return ActivityInstance\n }\n\n public get historyEntity() {\n return ActivityInstanceHistory\n }\n\n}\n"]}
1
+ {"version":3,"file":"event-subscriber.js","sourceRoot":"","sources":["../../../server/service/activity-instance/event-subscriber.ts"],"names":[],"mappings":";;;AAgBA,kCAYC;;AA5BD,qCAA6G;AAE7G,8DAAkE;AAClE,iDAA8C;AAE9C,iEAAyD;AACzD,iFAAwE;AACxE,yDAAyD;AAEzD;;;;;;GAMG;AACI,KAAK,UAAU,WAAW,CAAC,gBAAkC,EAAE,OAAsB;IAC1F,IAAI,CAAC,gBAAgB,EAAE,EAAE;QAAE,OAAO,gBAAgB,CAAA;IAClD,IAAI,gBAAgB,CAAC,eAAe,IAAI,gBAAgB,CAAC,aAAa,EAAE,CAAC;QACvE,OAAO,MAAM,CAAC,MAAM,CAAC,gBAAgB,EAAE,EAAE,eAAe,EAAE,IAAA,4BAAS,EAAC,gBAAgB,CAAC,EAAE,CAAC,CAAA;IAC1F,CAAC;IAED,MAAM,IAAI,GAA2B,MAAM,OAAO,CAAC,KAAK,CACtD,UAAU,4BAAS,CAAC,UAAU,qBAAqB,4BAAS,CAAC,KAAK,UAAU,4BAAS,CAAC,UAAU,MAAM,OAAO,CAAC,UAAU,CAAC,MAAM,CAAC,eAAe,CAAC,IAAI,EAAE,CAAC,CAAC,EAAE,EAC1J,CAAC,gBAAgB,CAAC,EAAE,CAAC,CACtB,CAAA;IAED,OAAO,MAAM,CAAC,MAAM,CAAC,gBAAgB,EAAE,EAAE,eAAe,EAAE,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAA;AAC5F,CAAC;AAGM,IAAM,0BAA0B,GAAhC,MAAM,0BAA0B;IACrC,QAAQ;QACN,OAAO,uCAAgB,CAAA;IACzB,CAAC;IAED,KAAK,CAAC,WAAW,CAAC,KAAoC;QACpD,cAAM,CAAC,OAAO,CAAC,mBAAmB,EAAE;YAClC,gBAAgB,EAAE,MAAM,WAAW,CAAC,KAAK,CAAC,MAA0B,EAAE,KAAK,CAAC,OAAO,CAAC;SACrF,CAAC,CAAA;IACJ,CAAC;IAED,KAAK,CAAC,WAAW,CAAC,KAAoC;QACpD,cAAM,CAAC,OAAO,CAAC,mBAAmB,EAAE;YAClC,gBAAgB,EAAE,MAAM,WAAW,CAAC,KAAK,CAAC,MAA0B,EAAE,KAAK,CAAC,OAAO,CAAC;SACrF,CAAC,CAAA;IACJ,CAAC;CACF,CAAA;AAhBY,gEAA0B;qCAA1B,0BAA0B;IADtC,IAAA,yBAAe,GAAE;GACL,0BAA0B,CAgBtC;AAGM,IAAM,uCAAuC,GAA7C,MAAM,uCAAwC,SAAQ,yCAG5D;IACC,IAAW,MAAM;QACf,OAAO,uCAAgB,CAAA;IACzB,CAAC;IAED,IAAW,aAAa;QACtB,OAAO,sDAAuB,CAAA;IAChC,CAAC;IAED;;;;;OAKG;IACH,KAAK,CAAC,WAAW,CAAC,KAAoC;QACpD,IAAI,CAAE,KAAK,CAAC,MAA2B,EAAE,EAAE;YAAE,OAAM;QAEnD,MAAM,KAAK,CAAC,WAAW,CAAC,KAAK,CAAC,CAAA;IAChC,CAAC;CACF,CAAA;AAvBY,0FAAuC;kDAAvC,uCAAuC;IADnD,IAAA,yBAAe,GAAE;GACL,uCAAuC,CAuBnD","sourcesContent":["import { EntityManager, EventSubscriber, EntitySubscriberInterface, InsertEvent, UpdateEvent } from 'typeorm'\n\nimport { HistoryEntitySubscriber } from '@operato/typeorm-history'\nimport { pubsub } from '@things-factory/shell'\n\nimport { ActivityInstance } from './activity-instance.js'\nimport { ActivityInstanceHistory } from './activity-instance-history.js'\nimport { CASE_POOL, poolIdsOf } from './assignee-pool.js'\n\n/**\n * The pool's role ids on what is published - the subscription filter decides by them (subscription-filters.ts).\n *\n * A many-to-many is not a column: the saved object carries the roles only when the caller had them, and its\n * RelationId is not filled in on a write. Asked here once per event when it is missing, never once per\n * subscriber. An update with no entity (a query-builder UPDATE) publishes nothing to read, as before.\n */\nexport async function withPoolIds(activityInstance: ActivityInstance, manager: EntityManager): Promise<ActivityInstance> {\n if (!activityInstance?.id) return activityInstance\n if (activityInstance.assigneeRoleIds || activityInstance.assigneeRoles) {\n return Object.assign(activityInstance, { assigneeRoleIds: poolIdsOf(activityInstance) })\n }\n\n const rows: { roles_id: string }[] = await manager.query(\n `SELECT ${CASE_POOL.roleColumn} AS roles_id FROM ${CASE_POOL.table} WHERE ${CASE_POOL.caseColumn} = ${manager.connection.driver.createParameter('id', 0)}`,\n [activityInstance.id]\n )\n\n return Object.assign(activityInstance, { assigneeRoleIds: rows.map(row => row.roles_id) })\n}\n\n@EventSubscriber()\nexport class ActivityInstanceSubscriber implements EntitySubscriberInterface<ActivityInstance> {\n listenTo() {\n return ActivityInstance\n }\n\n async afterInsert(event: InsertEvent<ActivityInstance>): Promise<any | void> {\n pubsub.publish('activity-instance', {\n activityInstance: await withPoolIds(event.entity as ActivityInstance, event.manager)\n })\n }\n\n async afterUpdate(event: UpdateEvent<ActivityInstance>): Promise<any | void> {\n pubsub.publish('activity-instance', {\n activityInstance: await withPoolIds(event.entity as ActivityInstance, event.manager)\n })\n }\n}\n\n@EventSubscriber()\nexport class ActivityInstanceHistoryEntitySubscriber extends HistoryEntitySubscriber<\n ActivityInstance,\n ActivityInstanceHistory\n> {\n public get entity() {\n return ActivityInstance\n }\n\n public get historyEntity() {\n return ActivityInstanceHistory\n }\n\n /*\n * A query-builder UPDATE (repository.update) hands subscribers only the columns it set - no id, no name.\n * That is not a case, and a history row of it is refused by the table (name is NOT NULL): every claim that\n * takes a case's row that way - pick, assign to a pool, claimDraft - failed whenever this subscriber was on.\n * Those claims write no history of their own; the save each of them makes afterwards writes the case whole.\n */\n async afterUpdate(event: UpdateEvent<ActivityInstance>): Promise<void> {\n if (!(event.entity as ActivityInstance)?.id) return\n\n await super.afterUpdate(event)\n }\n}\n"]}
@@ -6,7 +6,8 @@ exports.noticeApproval = noticeApproval;
6
6
  *
7
7
  * The four places an approval moves call `noticeApproval`: createApproval (a turn or a reference), delegate (a
8
8
  * handed seat), approve with no one after (approved) and reject (rejected). Applications call nothing; a case that
9
- * goes through the approval line is told of. Sending is notification's (`sendNoticeWhenCommitted`).
9
+ * goes through the approval line is told of. Sending is notification's (`sendNoticeWhenCommitted`); what it sent is kept
10
+ * on the approval row (`notices`).
10
11
  */
11
12
  const auth_base_1 = require("@things-factory/auth-base");
12
13
  const env_1 = require("@things-factory/env");
@@ -57,7 +58,7 @@ async function noticeApproval(context, { kind, approvalId }) {
57
58
  address,
58
59
  link: (0, notification_1.noticeLinkOf)(context, address),
59
60
  label: `approval notice ${kind} of ${approval.id}`
60
- });
61
+ }, result => (0, notification_1.keepNoticeDelivery)(activity_approval_js_1.ActivityApproval, approval.id, 'notices', (0, notification_1.noticeDeliveryOf)(kind, result, receivers.length)));
61
62
  }
62
63
  catch (error) {
63
64
  env_1.logger.error(`[worklist] approval notice ${kind} of ${approvalId} could not be made`, error);
@@ -1 +1 @@
1
- {"version":3,"file":"approval-notice.js","sourceRoot":"","sources":["../../../server/service/approval-notice/approval-notice.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * The approval notice (ADR-0164) - the one place the worklist tells people of an approval's turn and judgment.\n *\n * The four places an approval moves call `noticeApproval`: createApproval (a turn or a reference), delegate (a\n * handed seat), approve with no one after (approved) and reject (rejected). Applications call nothing; a case that\n * goes through the approval line is told of. Sending is notification's (`sendNoticeWhenCommitted`).\n */\nimport { User, UserStatus } from '@things-factory/auth-base'\nimport { logger } from '@things-factory/env'\nimport { translatorFor } from '@things-factory/i18n-base/translator'\nimport { noticeLinkOf, sendNoticeWhenCommitted } from '@things-factory/notification'\n\nimport { ActivityApproval } from '../activity-approval/activity-approval.js'\nimport { poolMembers } from '../activity-instance/assignee-pool.js'\nimport {\n approvalStep,\n isTurnNotice,\n noticeAddress,\n noticeReceivers,\n noticeWords,\n type ApprovalNoticeKind,\n type NoticeFacts,\n type NoticePerson,\n type Translate\n} from './approval-notice-rules.js'\n\n/**\n * Tells of what just happened to approval `approvalId`, once the transaction has committed.\n *\n * Who and what are settled here, inside the transaction, from rows it can see. Nothing here throws: a notice that\n * cannot be made is reported and the approval stands.\n */\nexport async function noticeApproval(context: ResolverContext, { kind, approvalId }: { kind: ApprovalNoticeKind; approvalId: string }): Promise<void> {\n try {\n const { user, tx } = context.state\n\n const approval = await tx.getRepository(ActivityApproval).findOne({\n where: { id: approvalId },\n relations: ['approver', 'approverRole', 'approverRole.users', 'activityThread', 'activityThread.assignee', 'activityThread.activityInstance']\n })\n const thread = approval?.activityThread\n const instance = thread?.activityInstance\n if (!approval || !thread || !instance) return\n\n const receivers = noticeReceivers({\n kind,\n approver: active(approval.approver),\n roleHolders: poolMembers(approval.approverRole ? [approval.approverRole] : []).map(active).filter(Boolean),\n requester: active(thread.assignee),\n actorId: user?.id\n })\n if (!receivers.length) return\n\n const facts: NoticeFacts = {\n kind,\n name: instance.name || '',\n requester: thread.assignee?.name,\n actor: user?.name,\n ...(isTurnNotice(kind) ? approvalStep(instance.approvalLine, approval.order) : {})\n }\n const address = noticeAddress(kind, { approvalId: approval.id, threadId: thread.id })\n\n sendNoticeWhenCommitted(context, {\n type: 'approval',\n level: kind === 'rejected' ? 'warn' : 'info',\n receivers,\n words: lng => noticeWords(translatorOf(lng), facts),\n address,\n link: noticeLinkOf(context, address),\n label: `approval notice ${kind} of ${approval.id}`\n })\n } catch (error) {\n logger.error(`[worklist] approval notice ${kind} of ${approvalId} could not be made`, error)\n }\n}\n\n/** Only a person who can sign in is told. */\nfunction active(person?: User | null): NoticePerson | null {\n return person && person.status === UserStatus.ACTIVATED ? { id: person.id, name: person.name, email: person.email, locale: person.locale } : null\n}\n\n/** A sentence missing in the person's language is said in English, never as its key. */\nfunction translatorOf(lng: string): Translate {\n const own = translatorFor(lng)\n const english = translatorFor('en')\n return (key, options = {}) => own(key, { ...options, defaultValue: english(key, options) })\n}\n"]}
1
+ {"version":3,"file":"approval-notice.js","sourceRoot":"","sources":["../../../server/service/approval-notice/approval-notice.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * The approval notice (ADR-0164) - the one place the worklist tells people of an approval's turn and judgment.\n *\n * The four places an approval moves call `noticeApproval`: createApproval (a turn or a reference), delegate (a\n * handed seat), approve with no one after (approved) and reject (rejected). Applications call nothing; a case that\n * goes through the approval line is told of. Sending is notification's (`sendNoticeWhenCommitted`); what it sent is kept\n * on the approval row (`notices`).\n */\nimport { User, UserStatus } from '@things-factory/auth-base'\nimport { logger } from '@things-factory/env'\nimport { translatorFor } from '@things-factory/i18n-base/translator'\nimport { keepNoticeDelivery, noticeDeliveryOf, noticeLinkOf, sendNoticeWhenCommitted } from '@things-factory/notification'\n\nimport { ActivityApproval } from '../activity-approval/activity-approval.js'\nimport { poolMembers } from '../activity-instance/assignee-pool.js'\nimport {\n approvalStep,\n isTurnNotice,\n noticeAddress,\n noticeReceivers,\n noticeWords,\n type ApprovalNoticeKind,\n type NoticeFacts,\n type NoticePerson,\n type Translate\n} from './approval-notice-rules.js'\n\n/**\n * Tells of what just happened to approval `approvalId`, once the transaction has committed.\n *\n * Who and what are settled here, inside the transaction, from rows it can see. Nothing here throws: a notice that\n * cannot be made is reported and the approval stands.\n */\nexport async function noticeApproval(context: ResolverContext, { kind, approvalId }: { kind: ApprovalNoticeKind; approvalId: string }): Promise<void> {\n try {\n const { user, tx } = context.state\n\n const approval = await tx.getRepository(ActivityApproval).findOne({\n where: { id: approvalId },\n relations: ['approver', 'approverRole', 'approverRole.users', 'activityThread', 'activityThread.assignee', 'activityThread.activityInstance']\n })\n const thread = approval?.activityThread\n const instance = thread?.activityInstance\n if (!approval || !thread || !instance) return\n\n const receivers = noticeReceivers({\n kind,\n approver: active(approval.approver),\n roleHolders: poolMembers(approval.approverRole ? [approval.approverRole] : []).map(active).filter(Boolean),\n requester: active(thread.assignee),\n actorId: user?.id\n })\n if (!receivers.length) return\n\n const facts: NoticeFacts = {\n kind,\n name: instance.name || '',\n requester: thread.assignee?.name,\n actor: user?.name,\n ...(isTurnNotice(kind) ? approvalStep(instance.approvalLine, approval.order) : {})\n }\n const address = noticeAddress(kind, { approvalId: approval.id, threadId: thread.id })\n\n sendNoticeWhenCommitted(context, {\n type: 'approval',\n level: kind === 'rejected' ? 'warn' : 'info',\n receivers,\n words: lng => noticeWords(translatorOf(lng), facts),\n address,\n link: noticeLinkOf(context, address),\n label: `approval notice ${kind} of ${approval.id}`\n }, result => keepNoticeDelivery(ActivityApproval, approval.id, 'notices', noticeDeliveryOf(kind, result, receivers.length)))\n } catch (error) {\n logger.error(`[worklist] approval notice ${kind} of ${approvalId} could not be made`, error)\n }\n}\n\n/** Only a person who can sign in is told. */\nfunction active(person?: User | null): NoticePerson | null {\n return person && person.status === UserStatus.ACTIVATED ? { id: person.id, name: person.name, email: person.email, locale: person.locale } : null\n}\n\n/** A sentence missing in the person's language is said in English, never as its key. */\nfunction translatorOf(lng: string): Translate {\n const own = translatorFor(lng)\n const english = translatorFor('en')\n return (key, options = {}) => own(key, { ...options, defaultValue: english(key, options) })\n}\n"]}
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ApprovalProgressQuery = void 0;
4
4
  const tslib_1 = require("tslib");
5
+ const typeorm_1 = require("typeorm");
5
6
  const type_graphql_1 = require("type-graphql");
6
7
  const auth_base_1 = require("@things-factory/auth-base");
7
8
  const organization_1 = require("@things-factory/organization");
@@ -88,6 +89,8 @@ let ApprovalProgressQuery = class ApprovalProgressQuery {
88
89
  const instance = line.approvals[0].activityThread.activityInstance;
89
90
  const { names, roleName } = await (0, waiting_on_js_1.namesWaitedOn)((0, shell_1.getRepository)(activity_approval_js_1.ActivityApproval).manager, line.progress.waiting);
90
91
  const employee = employeeOf.get(requesterOf(line.approvals));
92
+ const deciderIds = [...new Set(line.progress.decided.map(row => row.terminatorId || row.approverId).filter(Boolean))];
93
+ const deciders = deciderIds.length ? await (0, shell_1.getRepository)(auth_base_1.User).find({ where: { id: (0, typeorm_1.In)(deciderIds) } }) : [];
91
94
  items.push({
92
95
  id: line.id,
93
96
  activityName: line.activityName,
@@ -100,6 +103,7 @@ let ApprovalProgressQuery = class ApprovalProgressQuery {
100
103
  step: line.progress.step,
101
104
  steps: line.progress.steps,
102
105
  waitingOn: names,
106
+ decidedBy: deciders.map(one => one.name),
103
107
  waitingRole: roleName,
104
108
  waitingSince: line.progress.waitingSince,
105
109
  dueAt: instance.dueAt,