@rytass/bpm-core-client 0.4.0 → 0.5.0

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 (64) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/eslint.config.mjs +19 -0
  3. package/jest.config.cts +10 -0
  4. package/package.json +3 -4
  5. package/project.json +66 -0
  6. package/src/lib/api-auth-client.ts +182 -0
  7. package/src/lib/client-config.ts +99 -0
  8. package/src/lib/form/form-api.spec.ts +81 -0
  9. package/src/lib/form/form-api.ts +402 -0
  10. package/src/lib/form/form-rendering.ts +580 -0
  11. package/src/lib/graphql-client.spec.ts +65 -0
  12. package/src/lib/graphql-client.ts +128 -0
  13. package/src/lib/member-api.ts +129 -0
  14. package/src/lib/organization/organization-api.spec.ts +90 -0
  15. package/src/lib/organization/organization-api.ts +711 -0
  16. package/src/lib/template/template-api.spec.ts +88 -0
  17. package/src/lib/template/template-api.ts +894 -0
  18. package/src/lib/workflow/workflow-api.spec.ts +132 -0
  19. package/src/lib/workflow/workflow-api.ts +2124 -0
  20. package/tsconfig.json +27 -0
  21. package/tsconfig.lib.json +16 -0
  22. package/tsconfig.spec.json +16 -0
  23. package/src/index.js +0 -8
  24. package/src/index.js.map +0 -1
  25. package/src/lib/api-auth-client.d.ts +0 -73
  26. package/src/lib/api-auth-client.js +0 -129
  27. package/src/lib/api-auth-client.js.map +0 -1
  28. package/src/lib/client-config.d.ts +0 -81
  29. package/src/lib/client-config.js +0 -63
  30. package/src/lib/client-config.js.map +0 -1
  31. package/src/lib/form/form-api.d.ts +0 -54
  32. package/src/lib/form/form-api.js +0 -240
  33. package/src/lib/form/form-api.js.map +0 -1
  34. package/src/lib/form/form-rendering.d.ts +0 -52
  35. package/src/lib/form/form-rendering.js +0 -341
  36. package/src/lib/form/form-rendering.js.map +0 -1
  37. package/src/lib/form/index.js +0 -6
  38. package/src/lib/form/index.js.map +0 -1
  39. package/src/lib/graphql-client.d.ts +0 -45
  40. package/src/lib/graphql-client.js +0 -101
  41. package/src/lib/graphql-client.js.map +0 -1
  42. package/src/lib/member-api.d.ts +0 -49
  43. package/src/lib/member-api.js +0 -72
  44. package/src/lib/member-api.js.map +0 -1
  45. package/src/lib/organization/index.js +0 -5
  46. package/src/lib/organization/index.js.map +0 -1
  47. package/src/lib/organization/organization-api.d.ts +0 -173
  48. package/src/lib/organization/organization-api.js +0 -394
  49. package/src/lib/organization/organization-api.js.map +0 -1
  50. package/src/lib/template/index.js +0 -5
  51. package/src/lib/template/index.js.map +0 -1
  52. package/src/lib/template/template-api.d.ts +0 -174
  53. package/src/lib/template/template-api.js +0 -469
  54. package/src/lib/template/template-api.js.map +0 -1
  55. package/src/lib/workflow/index.js +0 -5
  56. package/src/lib/workflow/index.js.map +0 -1
  57. package/src/lib/workflow/workflow-api.d.ts +0 -517
  58. package/src/lib/workflow/workflow-api.js +0 -1068
  59. package/src/lib/workflow/workflow-api.js.map +0 -1
  60. /package/src/{index.d.ts → index.ts} +0 -0
  61. /package/src/lib/form/{index.d.ts → index.ts} +0 -0
  62. /package/src/lib/organization/{index.d.ts → index.ts} +0 -0
  63. /package/src/lib/template/{index.d.ts → index.ts} +0 -0
  64. /package/src/lib/workflow/{index.d.ts → index.ts} +0 -0
@@ -0,0 +1,2124 @@
1
+ import {
2
+ FormDefinitionSchema,
3
+ FormFieldDefinition,
4
+ FormFieldValue,
5
+ FormUiSchema,
6
+ SelectFieldDefinition,
7
+ } from '@rytass/bpm-core-shared/form';
8
+ import { WorkflowDefinition } from '@rytass/bpm-core-shared/workflow';
9
+ import { requestGraphQl } from '../graphql-client';
10
+
11
+ export type ApprovalInstanceState =
12
+ | 'APPROVED'
13
+ | 'CANCELLED'
14
+ | 'DRAFT'
15
+ | 'EXPIRED'
16
+ | 'REJECTED'
17
+ | 'RETURNED'
18
+ | 'RUNNING';
19
+
20
+ export type TaskStatus =
21
+ | 'CANCELLED'
22
+ | 'COMPLETED'
23
+ | 'IN_PROGRESS'
24
+ | 'PENDING'
25
+ | 'TRANSFERRED';
26
+
27
+ export type TaskAssignmentType = 'CANDIDATE_GROUP' | 'DIRECT_MEMBER';
28
+
29
+ export type TaskDecisionAction =
30
+ | 'APPROVED'
31
+ | 'REJECTED'
32
+ | 'RETURNED'
33
+ | 'TRANSFERRED';
34
+
35
+ export type AdhocDirectiveType =
36
+ | 'COMPLETION_NOTIFY'
37
+ | 'COUNTERSIGN'
38
+ | 'PRE_APPROVAL'
39
+ | 'STAGE_NOTIFY';
40
+
41
+ export type AdhocDirectiveStatus = 'CANCELLED' | 'CONSUMED' | 'PENDING';
42
+
43
+ export type AdhocTargetKind =
44
+ | 'MEMBER'
45
+ | 'ORG_UNIT_MEMBER'
46
+ | 'POSITION'
47
+ | 'WEBHOOK';
48
+
49
+ export type AdhocPreApprovalRejectBehavior =
50
+ | 'REJECT_INSTANCE'
51
+ | 'RETURN_TO_ORIGIN';
52
+
53
+ /**
54
+ * Polymorphic target of an ad-hoc directive. Set exactly the fields matching
55
+ * `kind`:
56
+ *
57
+ * - `MEMBER` → `memberIds`
58
+ * - `POSITION` → `positionId`
59
+ * - `ORG_UNIT_MEMBER` → `orgUnitId` (+ optional `includeDescendants`)
60
+ * - `WEBHOOK` → `webhookUrl` (+ optional `webhookHeaders`); only valid for
61
+ * the notification directives, never as countersign / pre-approval signers.
62
+ */
63
+ export interface AdhocTargetOptions {
64
+ readonly includeDescendants?: boolean;
65
+ readonly kind: AdhocTargetKind;
66
+ readonly memberIds?: readonly string[];
67
+ readonly orgUnitId?: string;
68
+ readonly positionId?: string;
69
+ readonly webhookHeaders?: Readonly<Record<string, string>>;
70
+ readonly webhookUrl?: string;
71
+ }
72
+
73
+ /**
74
+ * An instance-scoped ad-hoc directive recorded by a stage approver. Directives
75
+ * never modify the workflow template — they only affect the single approval
76
+ * instance they were created on. `status` lifecycle: `PENDING` (waiting for
77
+ * its trigger) → `CONSUMED` (effect applied / notification sent) or
78
+ * `CANCELLED` (withdrawn, or dropped by a return / reject / cancel).
79
+ */
80
+ export interface AdhocDirectiveRecord {
81
+ readonly channels: readonly NotificationChannel[] | null;
82
+ readonly comment: string | null;
83
+ readonly consumedAt: string | null;
84
+ readonly createdAt: string;
85
+ readonly createdByMemberId: string;
86
+ readonly id: string;
87
+ readonly instanceId: string;
88
+ readonly onReject: AdhocPreApprovalRejectBehavior | null;
89
+ readonly originNodeId: string;
90
+ readonly originTaskId: string;
91
+ readonly status: AdhocDirectiveStatus;
92
+ readonly targetKind: AdhocTargetKind;
93
+ readonly targetValueJson: string;
94
+ readonly type: AdhocDirectiveType;
95
+ }
96
+
97
+ export interface ApprovalInstanceRecord {
98
+ readonly completedAt: string | null;
99
+ readonly formData: WorkflowFormData;
100
+ readonly formDataJson: string;
101
+ readonly formDefinitionSnapshot: FormDefinitionSnapshot;
102
+ readonly formDefinitionSnapshotJson: string;
103
+ readonly id: string;
104
+ readonly initiatorMemberId: string;
105
+ readonly startedAt: string;
106
+ readonly state: ApprovalInstanceState;
107
+ readonly templateId: string;
108
+ readonly templateVersionId: string;
109
+ readonly title: string;
110
+ readonly workflowSnapshot: WorkflowDefinition;
111
+ readonly workflowSnapshotJson: string;
112
+ }
113
+
114
+ export interface FormDefinitionSnapshot {
115
+ readonly formDefinitionVersionId?: string;
116
+ readonly schema?: FormDefinitionSchema;
117
+ readonly uiSchema?: FormUiSchema;
118
+ readonly version?: number;
119
+ }
120
+
121
+ export type WorkflowFormData = Readonly<
122
+ Record<string, FormFieldValue | undefined>
123
+ >;
124
+
125
+ export interface TaskRecord {
126
+ readonly adhocDirectiveId: string | null;
127
+ readonly adhocOriginTaskId: string | null;
128
+ readonly adhocType: AdhocDirectiveType | null;
129
+ readonly assigneeMemberId: string | null;
130
+ readonly assignmentType: TaskAssignmentType;
131
+ readonly candidateMemberIds: readonly string[];
132
+ readonly completedAt: string | null;
133
+ readonly createdAt: string;
134
+ readonly decisionPolicySnapshotJson: string;
135
+ readonly delegationChainJson: string;
136
+ readonly id: string;
137
+ readonly instanceId: string;
138
+ readonly isAdhoc: boolean;
139
+ readonly nodeId: string;
140
+ readonly openedAt: string | null;
141
+ readonly originalAssigneeMemberId: string | null;
142
+ readonly slaDueAt: string | null;
143
+ readonly status: TaskStatus;
144
+ readonly tokenId: string;
145
+ }
146
+
147
+ export interface TaskCandidateRecord {
148
+ readonly claimedAt: string | null;
149
+ readonly createdAt: string;
150
+ readonly decidedAt: string | null;
151
+ readonly delegationChainJson: string;
152
+ readonly id: string;
153
+ readonly memberId: string;
154
+ readonly originalMemberId: string;
155
+ readonly sourceType: string;
156
+ readonly status: string;
157
+ readonly taskId: string;
158
+ }
159
+
160
+ export interface WorkflowTokenRecord {
161
+ readonly consumedAt: string | null;
162
+ readonly createdAt: string;
163
+ readonly currentNodeId: string;
164
+ readonly id: string;
165
+ readonly instanceId: string;
166
+ readonly parentTokenId: string | null;
167
+ readonly status: 'ACTIVE' | 'CONSUMED' | 'WAITING';
168
+ }
169
+
170
+ export interface TaskDecisionRecord {
171
+ readonly action: TaskDecisionAction;
172
+ readonly comment: string | null;
173
+ readonly decidedAt: string;
174
+ readonly decidedByMemberId: string;
175
+ readonly id: string;
176
+ readonly returnToNodeId: string | null;
177
+ readonly signatureId: string | null;
178
+ readonly taskId: string;
179
+ readonly transferToMemberId: string | null;
180
+ }
181
+
182
+ export interface ActivityLogRecord {
183
+ readonly actorMemberId: string | null;
184
+ readonly createdAt: string;
185
+ readonly eventType: string;
186
+ readonly id: string;
187
+ readonly nodeId: string | null;
188
+ readonly payloadJson: string;
189
+ readonly taskId: string | null;
190
+ }
191
+
192
+ export interface MemberProfileRecord {
193
+ readonly email: string;
194
+ readonly memberId: string;
195
+ readonly name: string;
196
+ }
197
+
198
+ export interface MemberDirectoryPage {
199
+ readonly members: readonly MemberProfileRecord[];
200
+ readonly totalCount: number;
201
+ }
202
+
203
+ export type DelegationScopeType = 'ALL' | 'CONDITION_BASED' | 'TEMPLATE_LIST';
204
+
205
+ export type DelegationRuleStatus = 'ACTIVE' | 'EXPIRED' | 'REVOKED';
206
+
207
+ export type NotificationChannel = 'EMAIL' | 'IN_APP' | 'WEBHOOK';
208
+
209
+ export type NotificationDigestMode = 'DAILY' | 'INSTANT';
210
+
211
+ export type NotificationStatus = 'FAILED' | 'PENDING' | 'READ' | 'SENT';
212
+
213
+ export type NotificationType =
214
+ | 'INSTANCE_COMPLETED'
215
+ | 'SLA_OVERDUE'
216
+ | 'SLA_WARNING'
217
+ | 'TASK_ASSIGNED'
218
+ | 'TASK_TRANSFERRED';
219
+
220
+ export type NotificationResolution =
221
+ | 'APPROVED'
222
+ | 'REJECTED'
223
+ | 'RETURNED'
224
+ | 'SUPERSEDED'
225
+ | 'TRANSFERRED';
226
+
227
+ export interface NotificationRecord {
228
+ /**
229
+ * Whether the recipient can still act on this notification (an unresolved
230
+ * task assignment). Server-derived; drives the inline 同意/拒絕 actions.
231
+ */
232
+ readonly actionable: boolean;
233
+ readonly attemptCount: number;
234
+ readonly body: string;
235
+ readonly channel: NotificationChannel;
236
+ readonly createdAt: string;
237
+ readonly deliveredAt: string | null;
238
+ readonly deliveryError: string | null;
239
+ readonly deliveryTarget: string | null;
240
+ readonly id: string;
241
+ readonly instanceId: string | null;
242
+ readonly lastAttemptAt: string | null;
243
+ readonly nextRetryAt: string | null;
244
+ readonly payloadJson: string;
245
+ readonly readAt: string | null;
246
+ readonly recipientMemberId: string;
247
+ /** How an actionable notification was resolved; `null` while still open. */
248
+ readonly resolution: NotificationResolution | null;
249
+ readonly resolvedAt: string | null;
250
+ readonly sentAt: string | null;
251
+ readonly status: NotificationStatus;
252
+ readonly taskId: string | null;
253
+ readonly title: string;
254
+ readonly type: NotificationType;
255
+ }
256
+
257
+ export interface NotificationPreferenceRecord {
258
+ readonly emailDigestMode: NotificationDigestMode;
259
+ readonly emailEnabled: boolean;
260
+ readonly inAppEnabled: boolean;
261
+ readonly memberId: string;
262
+ readonly quietHoursEnd: string | null;
263
+ readonly quietHoursStart: string | null;
264
+ readonly updatedAt: string;
265
+ }
266
+
267
+ export interface DelegationRuleRecord {
268
+ readonly agentMemberId: string;
269
+ readonly createdAt: string;
270
+ readonly createdByMemberId: string | null;
271
+ readonly endAt: string | null;
272
+ readonly id: string;
273
+ readonly principalMemberId: string;
274
+ readonly priority: number;
275
+ readonly requiresConfirmation: boolean;
276
+ readonly revokedAt: string | null;
277
+ readonly revokedByMemberId: string | null;
278
+ readonly scopeConditionCel: string | null;
279
+ readonly scopeTemplateIds: readonly string[];
280
+ readonly scopeType: DelegationScopeType;
281
+ readonly startAt: string;
282
+ readonly status: DelegationRuleStatus;
283
+ readonly updatedAt: string;
284
+ }
285
+
286
+ export interface AttachmentRecord {
287
+ readonly checksumSha256: string;
288
+ readonly createdAt: string;
289
+ readonly filename: string;
290
+ readonly formFieldPath: string | null;
291
+ readonly id: string;
292
+ readonly instanceId: string | null;
293
+ readonly mimeType: string;
294
+ readonly sizeBytes: string;
295
+ readonly storageKey: string;
296
+ readonly storageProvider: string;
297
+ readonly taskId: string | null;
298
+ readonly uploaderMemberId: string;
299
+ }
300
+
301
+ export interface SignatureRecord {
302
+ readonly algorithm: string;
303
+ readonly id: string;
304
+ readonly instanceId: string;
305
+ readonly keyVersion: number;
306
+ readonly previousSignatureHash: string | null;
307
+ readonly signature: string;
308
+ readonly signedAt: string;
309
+ readonly signedPayloadHash: string;
310
+ readonly signedPayloadJson: string;
311
+ readonly signerMemberId: string;
312
+ readonly taskId: string | null;
313
+ readonly timestampTokenBase64: string | null;
314
+ }
315
+
316
+ export interface SignatureVerificationRecord {
317
+ readonly checkedCount: number;
318
+ readonly errors: readonly string[];
319
+ readonly instanceId: string;
320
+ readonly valid: boolean;
321
+ }
322
+
323
+ export interface ApprovalTemplateRecord {
324
+ readonly currentVersionId: string | null;
325
+ readonly id: string;
326
+ readonly name: string;
327
+ readonly updatedAt?: string;
328
+ }
329
+
330
+ export interface ApprovalTemplateVersionRecord {
331
+ readonly formDefinitionVersionId: string | null;
332
+ readonly id: string;
333
+ readonly status: 'ARCHIVED' | 'DRAFT' | 'PUBLISHED';
334
+ readonly version: number;
335
+ }
336
+
337
+ export interface LaunchContext {
338
+ readonly formVersion: {
339
+ readonly id: string;
340
+ readonly schema: FormDefinitionSchema;
341
+ readonly uiSchema: FormUiSchema;
342
+ readonly version: number;
343
+ };
344
+ readonly template: ApprovalTemplateRecord;
345
+ readonly templateVersion: ApprovalTemplateVersionRecord;
346
+ }
347
+
348
+ export interface LaunchableTemplateRecord {
349
+ readonly currentVersionId: string;
350
+ readonly formDefinitionVersionId: string;
351
+ readonly id: string;
352
+ readonly name: string;
353
+ readonly updatedAt: string;
354
+ readonly version: number;
355
+ }
356
+
357
+ export type ApprovalInstanceView = 'ALL' | 'CC' | 'SENT';
358
+
359
+ export interface ApprovalInstancesPageInput {
360
+ readonly page: number;
361
+ readonly pageSize: number;
362
+ readonly searchText: string | null;
363
+ readonly state: ApprovalInstanceState | null;
364
+ readonly templateId: string | null;
365
+ readonly view: ApprovalInstanceView;
366
+ }
367
+
368
+ export interface ApprovalInstancesPageResult {
369
+ readonly instances: readonly ApprovalInstanceRecord[];
370
+ readonly pageInfo: ApprovalInstancePageInfoRecord;
371
+ readonly totalCount: number;
372
+ }
373
+
374
+ export interface ApprovalInstancePageInfoRecord {
375
+ readonly hasNextPage: boolean;
376
+ readonly hasPreviousPage: boolean;
377
+ readonly page: number;
378
+ readonly pageSize: number;
379
+ readonly totalCount: number;
380
+ readonly totalPages: number;
381
+ }
382
+
383
+ export interface WorkflowDashboardSummaryRecord {
384
+ readonly activeInstanceCount: number;
385
+ readonly completedInstanceCount: number;
386
+ readonly overdueTaskCount: number;
387
+ readonly pendingTaskCount: number;
388
+ readonly rejectedInstanceCount: number;
389
+ readonly totalInstanceCount: number;
390
+ readonly unreadNotificationCount: number;
391
+ }
392
+
393
+ interface ApprovalInstancesQueryData {
394
+ readonly approvalInstances: readonly InstanceJsonRecord[];
395
+ }
396
+
397
+ interface ApprovalInstancePageInfoQueryData {
398
+ readonly approvalInstancePageInfo: ApprovalInstancePageInfoRecord;
399
+ }
400
+
401
+ interface WorkflowDashboardSummaryQueryData {
402
+ readonly workflowDashboardSummary: Omit<
403
+ WorkflowDashboardSummaryRecord,
404
+ 'unreadNotificationCount'
405
+ >;
406
+ }
407
+
408
+ interface ApprovalInstanceQueryData {
409
+ readonly approvalInstance: InstanceJsonRecord;
410
+ readonly activityLogs: readonly ActivityLogRecord[];
411
+ readonly tasks: readonly TaskRecord[];
412
+ readonly workflowTokens: readonly WorkflowTokenRecord[];
413
+ }
414
+
415
+ interface InboxTasksQueryData {
416
+ readonly inboxTasks: readonly TaskRecord[];
417
+ }
418
+
419
+ interface ApprovalHistoryTasksQueryData {
420
+ readonly approvalHistoryTasks: readonly TaskRecord[];
421
+ }
422
+
423
+ interface TaskDecisionsQueryData {
424
+ readonly taskDecisions: readonly TaskDecisionRecord[];
425
+ }
426
+
427
+ interface AttachmentsQueryData {
428
+ readonly attachments: readonly AttachmentRecord[];
429
+ }
430
+
431
+ interface AttachmentDownloadUrlQueryData {
432
+ readonly attachmentDownloadUrl: string;
433
+ }
434
+
435
+ interface AttachmentPreviewUrlQueryData {
436
+ readonly attachmentPreviewUrl: string;
437
+ }
438
+
439
+ interface SignaturesQueryData {
440
+ readonly signatures: readonly SignatureRecord[];
441
+ readonly verifySignatureChain: SignatureVerificationRecord;
442
+ }
443
+
444
+ interface UploadAttachmentMutationData {
445
+ readonly uploadAttachment: AttachmentRecord;
446
+ }
447
+
448
+ interface MembersQueryData {
449
+ readonly members: readonly MemberProfileRecord[];
450
+ }
451
+
452
+ interface SearchMembersQueryData {
453
+ readonly searchMembers: readonly MemberProfileRecord[];
454
+ }
455
+
456
+ interface MemberDirectoryPageQueryData {
457
+ readonly memberCount: number;
458
+ readonly searchMembers: readonly MemberProfileRecord[];
459
+ }
460
+
461
+ interface DelegationRulesQueryData {
462
+ readonly delegationRules: readonly DelegationRuleRecord[];
463
+ }
464
+
465
+ interface DelegationRulesPageQueryData extends DelegationRulesQueryData {
466
+ readonly delegationRuleCount: number;
467
+ }
468
+
469
+ interface NotificationsQueryData {
470
+ readonly notificationCount: number;
471
+ readonly notifications: readonly NotificationRecord[];
472
+ readonly unreadNotificationCount: number;
473
+ }
474
+
475
+ interface NotificationPreferenceQueryData {
476
+ readonly notificationPreference: NotificationPreferenceRecord;
477
+ }
478
+
479
+ interface CreateDelegationRuleMutationData {
480
+ readonly createDelegationRule: DelegationRuleRecord;
481
+ }
482
+
483
+ interface RevokeDelegationRuleMutationData {
484
+ readonly revokeDelegationRule: DelegationRuleRecord;
485
+ }
486
+
487
+ interface MarkNotificationReadMutationData {
488
+ readonly markNotificationRead: Pick<
489
+ NotificationRecord,
490
+ 'id' | 'readAt' | 'status'
491
+ >;
492
+ }
493
+
494
+ interface UpdateNotificationPreferenceMutationData {
495
+ readonly updateNotificationPreference: NotificationPreferenceRecord;
496
+ }
497
+
498
+ interface SubmitApprovalInstanceMutationData {
499
+ readonly submitApprovalInstance: Pick<ApprovalInstanceRecord, 'id'>;
500
+ }
501
+
502
+ interface DecideTaskMutationData {
503
+ readonly decideTask: TaskDecisionRecord;
504
+ }
505
+
506
+ interface CancelApprovalInstanceMutationData {
507
+ readonly cancelApprovalInstance: Pick<ApprovalInstanceRecord, 'id' | 'state'>;
508
+ }
509
+
510
+ interface ResubmitApprovalInstanceMutationData {
511
+ readonly resubmitApprovalInstance: Pick<
512
+ ApprovalInstanceRecord,
513
+ 'id' | 'state'
514
+ >;
515
+ }
516
+
517
+ interface LaunchTemplateQueryData {
518
+ readonly approvalTemplate: ApprovalTemplateRecord & {
519
+ readonly updatedAt: string;
520
+ };
521
+ readonly approvalTemplateVersions: readonly ApprovalTemplateVersionRecord[];
522
+ readonly launchableApprovalTemplates?: readonly (ApprovalTemplateRecord & {
523
+ readonly updatedAt: string;
524
+ })[];
525
+ }
526
+
527
+ interface LaunchTemplatesQueryData {
528
+ readonly approvalTemplates?: readonly (ApprovalTemplateRecord & {
529
+ readonly updatedAt: string;
530
+ })[];
531
+ readonly launchableApprovalTemplates?: readonly (ApprovalTemplateRecord & {
532
+ readonly updatedAt: string;
533
+ })[];
534
+ }
535
+
536
+ interface LaunchTemplateVersionsQueryData {
537
+ readonly approvalTemplateVersions: readonly ApprovalTemplateVersionRecord[];
538
+ }
539
+
540
+ interface LaunchFormVersionQueryData {
541
+ readonly formDefinitionVersion: {
542
+ readonly id: string;
543
+ readonly schemaJson: string;
544
+ readonly uiSchemaJson: string;
545
+ readonly version: number;
546
+ };
547
+ }
548
+
549
+ interface InstanceJsonRecord extends Omit<
550
+ ApprovalInstanceRecord,
551
+ 'formData' | 'formDefinitionSnapshot' | 'workflowSnapshot'
552
+ > {
553
+ readonly formDataJson: string;
554
+ readonly formDefinitionSnapshotJson: string;
555
+ readonly workflowSnapshotJson: string;
556
+ }
557
+
558
+ export async function listApprovalInstances(): Promise<
559
+ readonly ApprovalInstanceRecord[]
560
+ > {
561
+ const data = await requestGraphQl<ApprovalInstancesQueryData>(
562
+ `query ApprovalInstances {
563
+ approvalInstances {
564
+ completedAt
565
+ formDataJson
566
+ formDefinitionSnapshotJson
567
+ id
568
+ initiatorMemberId
569
+ startedAt
570
+ state
571
+ templateId
572
+ templateVersionId
573
+ title
574
+ workflowSnapshotJson
575
+ }
576
+ }`,
577
+ );
578
+
579
+ return data.approvalInstances.map(parseInstanceJson);
580
+ }
581
+
582
+ export async function listApprovalInstancesPage({
583
+ page,
584
+ pageSize,
585
+ searchText,
586
+ state,
587
+ templateId,
588
+ view,
589
+ }: ApprovalInstancesPageInput): Promise<ApprovalInstancesPageResult> {
590
+ const variables = {
591
+ page,
592
+ pageSize,
593
+ searchText: searchText?.trim() || null,
594
+ state: state ? [state] : null,
595
+ templateId: templateId?.trim() || null,
596
+ view,
597
+ };
598
+ const [pageData, pageInfoData] = await Promise.all([
599
+ requestGraphQl<ApprovalInstancesQueryData>(
600
+ `query ApprovalInstancesPage(
601
+ $view: ApprovalInstanceListView
602
+ $searchText: String
603
+ $state: [ApprovalInstanceState!]
604
+ $templateId: String
605
+ $page: Int
606
+ $pageSize: Int
607
+ ) {
608
+ approvalInstances(
609
+ view: $view
610
+ searchText: $searchText
611
+ state: $state
612
+ templateId: $templateId
613
+ page: $page
614
+ pageSize: $pageSize
615
+ ) {
616
+ completedAt
617
+ formDataJson
618
+ formDefinitionSnapshotJson
619
+ id
620
+ initiatorMemberId
621
+ startedAt
622
+ state
623
+ templateId
624
+ templateVersionId
625
+ title
626
+ workflowSnapshotJson
627
+ }
628
+ }`,
629
+ variables,
630
+ ),
631
+ requestGraphQl<ApprovalInstancePageInfoQueryData>(
632
+ `query ApprovalInstancePageInfo(
633
+ $view: ApprovalInstanceListView
634
+ $searchText: String
635
+ $state: [ApprovalInstanceState!]
636
+ $templateId: String
637
+ $page: Int
638
+ $pageSize: Int
639
+ ) {
640
+ approvalInstancePageInfo(
641
+ view: $view
642
+ searchText: $searchText
643
+ state: $state
644
+ templateId: $templateId
645
+ page: $page
646
+ pageSize: $pageSize
647
+ ) {
648
+ hasNextPage
649
+ hasPreviousPage
650
+ page
651
+ pageSize
652
+ totalCount
653
+ totalPages
654
+ }
655
+ }`,
656
+ variables,
657
+ ),
658
+ ]);
659
+ const pageInfo = pageInfoData.approvalInstancePageInfo;
660
+
661
+ return {
662
+ instances: pageData.approvalInstances.map(parseInstanceJson),
663
+ pageInfo,
664
+ totalCount: pageInfo.totalCount,
665
+ };
666
+ }
667
+
668
+ export async function readLaunchContext(
669
+ templateId: string,
670
+ ): Promise<LaunchContext> {
671
+ const templateData = await requestGraphQl<LaunchTemplateQueryData>(
672
+ `query LaunchTemplate($id: String!) {
673
+ approvalTemplate(id: $id) {
674
+ currentVersionId
675
+ id
676
+ name
677
+ }
678
+ approvalTemplateVersions(templateId: $id) {
679
+ formDefinitionVersionId
680
+ id
681
+ status
682
+ version
683
+ }
684
+ launchableApprovalTemplates {
685
+ currentVersionId
686
+ id
687
+ name
688
+ updatedAt
689
+ }
690
+ }`,
691
+ { id: templateId },
692
+ );
693
+ const templateVersion = templateData.approvalTemplateVersions.find(
694
+ (version) => version.id === templateData.approvalTemplate.currentVersionId,
695
+ );
696
+
697
+ if (!templateVersion || templateVersion.status !== 'PUBLISHED') {
698
+ throw new Error('模板尚未發布,無法發起簽核。');
699
+ }
700
+
701
+ if (!templateVersion.formDefinitionVersionId) {
702
+ throw new Error('模板尚未綁定表單版本,無法發起簽核。');
703
+ }
704
+
705
+ const launchableApprovalTemplates =
706
+ templateData.launchableApprovalTemplates ?? [templateData.approvalTemplate];
707
+
708
+ if (
709
+ !launchableApprovalTemplates.some((template) => template.id === templateId)
710
+ ) {
711
+ throw new Error('目前登入者沒有此模板的發起權限。');
712
+ }
713
+
714
+ const formData = await requestGraphQl<LaunchFormVersionQueryData>(
715
+ `query LaunchFormVersion($id: String!) {
716
+ formDefinitionVersion(id: $id) {
717
+ id
718
+ schemaJson
719
+ uiSchemaJson
720
+ version
721
+ }
722
+ }`,
723
+ { id: templateVersion.formDefinitionVersionId },
724
+ );
725
+
726
+ return {
727
+ formVersion: {
728
+ id: formData.formDefinitionVersion.id,
729
+ schema: JSON.parse(
730
+ formData.formDefinitionVersion.schemaJson,
731
+ ) as FormDefinitionSchema,
732
+ uiSchema: JSON.parse(
733
+ formData.formDefinitionVersion.uiSchemaJson,
734
+ ) as FormUiSchema,
735
+ version: formData.formDefinitionVersion.version,
736
+ },
737
+ template: templateData.approvalTemplate,
738
+ templateVersion,
739
+ };
740
+ }
741
+
742
+ export async function listLaunchableTemplates(): Promise<
743
+ readonly LaunchableTemplateRecord[]
744
+ > {
745
+ const templateData = await requestGraphQl<LaunchTemplatesQueryData>(
746
+ `query ApprovalTemplates {
747
+ launchableApprovalTemplates {
748
+ currentVersionId
749
+ id
750
+ name
751
+ updatedAt
752
+ }
753
+ }`,
754
+ );
755
+ const launchableApprovalTemplates =
756
+ templateData.launchableApprovalTemplates ??
757
+ templateData.approvalTemplates ??
758
+ [];
759
+ const versionLists = await Promise.all(
760
+ launchableApprovalTemplates.map((template) =>
761
+ requestGraphQl<LaunchTemplateVersionsQueryData>(
762
+ `query LaunchTemplateVersions($templateId: String!) {
763
+ approvalTemplateVersions(templateId: $templateId) {
764
+ formDefinitionVersionId
765
+ id
766
+ status
767
+ version
768
+ }
769
+ }`,
770
+ { templateId: template.id },
771
+ ),
772
+ ),
773
+ );
774
+
775
+ return launchableApprovalTemplates.flatMap((template, index) => {
776
+ const currentVersion = versionLists[index]?.approvalTemplateVersions.find(
777
+ (version) => version.id === template.currentVersionId,
778
+ );
779
+
780
+ return currentVersion?.status === 'PUBLISHED' &&
781
+ currentVersion.formDefinitionVersionId
782
+ ? [
783
+ {
784
+ currentVersionId: currentVersion.id,
785
+ formDefinitionVersionId: currentVersion.formDefinitionVersionId,
786
+ id: template.id,
787
+ name: template.name,
788
+ updatedAt: template.updatedAt,
789
+ version: currentVersion.version,
790
+ },
791
+ ]
792
+ : [];
793
+ });
794
+ }
795
+
796
+ export async function readApprovalInstance(instanceId: string): Promise<{
797
+ readonly activityLogs: readonly ActivityLogRecord[];
798
+ readonly instance: ApprovalInstanceRecord;
799
+ readonly tasks: readonly TaskRecord[];
800
+ readonly workflowTokens: readonly WorkflowTokenRecord[];
801
+ }> {
802
+ const data = await requestGraphQl<ApprovalInstanceQueryData>(
803
+ `query ApprovalInstance($id: String!) {
804
+ approvalInstance(id: $id) {
805
+ completedAt
806
+ formDataJson
807
+ formDefinitionSnapshotJson
808
+ id
809
+ initiatorMemberId
810
+ startedAt
811
+ state
812
+ templateId
813
+ templateVersionId
814
+ title
815
+ workflowSnapshotJson
816
+ }
817
+ tasks(instanceId: $id) {
818
+ adhocDirectiveId
819
+ adhocOriginTaskId
820
+ adhocType
821
+ assigneeMemberId
822
+ assignmentType
823
+ candidateMemberIds
824
+ completedAt
825
+ createdAt
826
+ decisionPolicySnapshotJson
827
+ delegationChainJson
828
+ id
829
+ instanceId
830
+ isAdhoc
831
+ nodeId
832
+ openedAt
833
+ originalAssigneeMemberId
834
+ slaDueAt
835
+ status
836
+ tokenId
837
+ }
838
+ workflowTokens(instanceId: $id) {
839
+ consumedAt
840
+ createdAt
841
+ currentNodeId
842
+ id
843
+ instanceId
844
+ parentTokenId
845
+ status
846
+ }
847
+ activityLogs(instanceId: $id) {
848
+ actorMemberId
849
+ createdAt
850
+ eventType
851
+ id
852
+ nodeId
853
+ payloadJson
854
+ taskId
855
+ }
856
+ }`,
857
+ { id: instanceId },
858
+ );
859
+
860
+ return {
861
+ activityLogs: data.activityLogs,
862
+ instance: parseInstanceJson(data.approvalInstance),
863
+ tasks: normalizeTaskRecords(data.tasks),
864
+ workflowTokens: data.workflowTokens,
865
+ };
866
+ }
867
+
868
+ export async function listInboxTasks(
869
+ assigneeMemberId: string,
870
+ ): Promise<readonly TaskRecord[]> {
871
+ const data = await requestGraphQl<InboxTasksQueryData>(
872
+ `query InboxTasks($assigneeMemberId: String!) {
873
+ inboxTasks(assigneeMemberId: $assigneeMemberId) {
874
+ adhocDirectiveId
875
+ adhocOriginTaskId
876
+ adhocType
877
+ assigneeMemberId
878
+ assignmentType
879
+ candidateMemberIds
880
+ completedAt
881
+ createdAt
882
+ decisionPolicySnapshotJson
883
+ delegationChainJson
884
+ id
885
+ instanceId
886
+ isAdhoc
887
+ nodeId
888
+ openedAt
889
+ originalAssigneeMemberId
890
+ slaDueAt
891
+ status
892
+ tokenId
893
+ }
894
+ }`,
895
+ { assigneeMemberId },
896
+ );
897
+
898
+ return normalizeTaskRecords(data.inboxTasks);
899
+ }
900
+
901
+ export async function listApprovalHistoryTasks(
902
+ assigneeMemberId: string,
903
+ ): Promise<readonly TaskRecord[]> {
904
+ const data = await requestGraphQl<ApprovalHistoryTasksQueryData>(
905
+ `query ApprovalHistoryTasks($assigneeMemberId: String!) {
906
+ approvalHistoryTasks(assigneeMemberId: $assigneeMemberId) {
907
+ adhocDirectiveId
908
+ adhocOriginTaskId
909
+ adhocType
910
+ assigneeMemberId
911
+ assignmentType
912
+ candidateMemberIds
913
+ completedAt
914
+ createdAt
915
+ decisionPolicySnapshotJson
916
+ delegationChainJson
917
+ id
918
+ instanceId
919
+ isAdhoc
920
+ nodeId
921
+ openedAt
922
+ originalAssigneeMemberId
923
+ slaDueAt
924
+ status
925
+ tokenId
926
+ }
927
+ }`,
928
+ { assigneeMemberId },
929
+ );
930
+
931
+ return normalizeTaskRecords(data.approvalHistoryTasks);
932
+ }
933
+
934
+ export async function readWorkflowDashboardSummary({
935
+ currentMemberId,
936
+ from,
937
+ to,
938
+ }: {
939
+ readonly currentMemberId: string;
940
+ readonly from: string | null;
941
+ readonly to: string | null;
942
+ }): Promise<WorkflowDashboardSummaryRecord> {
943
+ const [summaryData, notificationResult] = await Promise.all([
944
+ requestGraphQl<WorkflowDashboardSummaryQueryData>(
945
+ `query WorkflowDashboardSummary($from: DateTime, $to: DateTime) {
946
+ workflowDashboardSummary(from: $from, to: $to) {
947
+ activeInstanceCount
948
+ completedInstanceCount
949
+ overdueTaskCount
950
+ pendingTaskCount
951
+ rejectedInstanceCount
952
+ totalInstanceCount
953
+ }
954
+ }`,
955
+ { from, to },
956
+ ),
957
+ listNotifications({
958
+ includeRead: true,
959
+ page: 1,
960
+ pageSize: 1,
961
+ recipientMemberId: currentMemberId,
962
+ }),
963
+ ]);
964
+
965
+ return {
966
+ ...summaryData.workflowDashboardSummary,
967
+ unreadNotificationCount: notificationResult.unreadCount,
968
+ };
969
+ }
970
+
971
+ function normalizeTaskRecords(
972
+ tasks: readonly TaskRecord[],
973
+ ): readonly TaskRecord[] {
974
+ return tasks.map((task) => ({
975
+ ...task,
976
+ adhocDirectiveId: task.adhocDirectiveId ?? null,
977
+ adhocOriginTaskId: task.adhocOriginTaskId ?? null,
978
+ adhocType: task.adhocType ?? null,
979
+ assignmentType: task.assignmentType ?? 'DIRECT_MEMBER',
980
+ candidateMemberIds: task.candidateMemberIds ?? [],
981
+ decisionPolicySnapshotJson:
982
+ task.decisionPolicySnapshotJson ?? JSON.stringify({ type: 'SINGLE' }),
983
+ isAdhoc: task.isAdhoc ?? false,
984
+ }));
985
+ }
986
+
987
+ export async function listTaskDecisions(
988
+ taskId: string,
989
+ ): Promise<readonly TaskDecisionRecord[]> {
990
+ const data = await requestGraphQl<TaskDecisionsQueryData>(
991
+ `query TaskDecisions($taskId: String!) {
992
+ taskDecisions(taskId: $taskId) {
993
+ action
994
+ comment
995
+ decidedAt
996
+ decidedByMemberId
997
+ id
998
+ returnToNodeId
999
+ signatureId
1000
+ taskId
1001
+ transferToMemberId
1002
+ }
1003
+ }`,
1004
+ { taskId },
1005
+ );
1006
+
1007
+ return data.taskDecisions;
1008
+ }
1009
+
1010
+ export async function listAttachments(
1011
+ instanceId: string,
1012
+ ): Promise<readonly AttachmentRecord[]> {
1013
+ const data = await requestGraphQl<AttachmentsQueryData>(
1014
+ `query InstanceAttachments($instanceId: String!) {
1015
+ attachments(instanceId: $instanceId) {
1016
+ checksumSha256
1017
+ createdAt
1018
+ filename
1019
+ formFieldPath
1020
+ id
1021
+ instanceId
1022
+ mimeType
1023
+ sizeBytes
1024
+ storageKey
1025
+ storageProvider
1026
+ taskId
1027
+ uploaderMemberId
1028
+ }
1029
+ }`,
1030
+ { instanceId },
1031
+ );
1032
+
1033
+ return data.attachments ?? [];
1034
+ }
1035
+
1036
+ export async function uploadAttachment({
1037
+ file,
1038
+ formFieldPath,
1039
+ }: {
1040
+ readonly file: File;
1041
+ readonly formFieldPath: string;
1042
+ }): Promise<AttachmentRecord> {
1043
+ const contentBase64 = await readFileBase64(file);
1044
+ const checksumSha256 = await hashFileSha256(file);
1045
+ const data = await requestGraphQl<UploadAttachmentMutationData>(
1046
+ `mutation UploadAttachment($input: UploadAttachmentInput!) {
1047
+ uploadAttachment(input: $input) {
1048
+ checksumSha256
1049
+ createdAt
1050
+ filename
1051
+ formFieldPath
1052
+ id
1053
+ instanceId
1054
+ mimeType
1055
+ sizeBytes
1056
+ storageKey
1057
+ storageProvider
1058
+ taskId
1059
+ uploaderMemberId
1060
+ }
1061
+ }`,
1062
+ {
1063
+ input: {
1064
+ checksumSha256,
1065
+ contentBase64,
1066
+ filename: file.name,
1067
+ formFieldPath,
1068
+ instanceId: null,
1069
+ mimeType: file.type || 'application/octet-stream',
1070
+ sizeBytes: file.size,
1071
+ taskId: null,
1072
+ },
1073
+ },
1074
+ );
1075
+
1076
+ return data.uploadAttachment;
1077
+ }
1078
+
1079
+ export async function readAttachmentDownloadUrl({
1080
+ id,
1081
+ }: {
1082
+ readonly id: string;
1083
+ }): Promise<string> {
1084
+ const data = await requestGraphQl<AttachmentDownloadUrlQueryData>(
1085
+ `query AttachmentDownloadUrl($id: String!) {
1086
+ attachmentDownloadUrl(id: $id)
1087
+ }`,
1088
+ { id },
1089
+ );
1090
+
1091
+ return data.attachmentDownloadUrl;
1092
+ }
1093
+
1094
+ export async function readAttachmentPreviewUrl({
1095
+ id,
1096
+ }: {
1097
+ readonly id: string;
1098
+ }): Promise<string> {
1099
+ const data = await requestGraphQl<AttachmentPreviewUrlQueryData>(
1100
+ `query AttachmentPreviewUrl($id: String!) {
1101
+ attachmentPreviewUrl(id: $id)
1102
+ }`,
1103
+ { id },
1104
+ );
1105
+
1106
+ return data.attachmentPreviewUrl;
1107
+ }
1108
+
1109
+ export async function readInstanceSignatures(instanceId: string): Promise<{
1110
+ readonly signatures: readonly SignatureRecord[];
1111
+ readonly verification: SignatureVerificationRecord;
1112
+ }> {
1113
+ const data = await requestGraphQl<SignaturesQueryData>(
1114
+ `query InstanceSignatures($instanceId: String!) {
1115
+ signatures(instanceId: $instanceId) {
1116
+ algorithm
1117
+ id
1118
+ instanceId
1119
+ keyVersion
1120
+ previousSignatureHash
1121
+ signature
1122
+ signedAt
1123
+ signedPayloadHash
1124
+ signedPayloadJson
1125
+ signerMemberId
1126
+ taskId
1127
+ timestampTokenBase64
1128
+ }
1129
+ verifySignatureChain(instanceId: $instanceId) {
1130
+ checkedCount
1131
+ errors
1132
+ instanceId
1133
+ valid
1134
+ }
1135
+ }`,
1136
+ { instanceId },
1137
+ );
1138
+
1139
+ return {
1140
+ signatures: data.signatures ?? [],
1141
+ verification: data.verifySignatureChain ?? {
1142
+ checkedCount: 0,
1143
+ errors: [],
1144
+ instanceId,
1145
+ valid: true,
1146
+ },
1147
+ };
1148
+ }
1149
+
1150
+ export async function resolveMemberProfiles(
1151
+ memberIds: readonly string[],
1152
+ ): Promise<readonly MemberProfileRecord[]> {
1153
+ if (memberIds.length === 0) {
1154
+ return [];
1155
+ }
1156
+
1157
+ const data = await requestGraphQl<MembersQueryData>(
1158
+ `query InstanceMembers($memberIds: [String!]!) {
1159
+ members(memberIds: $memberIds) {
1160
+ email
1161
+ memberId
1162
+ name
1163
+ }
1164
+ }`,
1165
+ { memberIds },
1166
+ );
1167
+
1168
+ return data.members;
1169
+ }
1170
+
1171
+ export async function searchMembers(
1172
+ searchText: string,
1173
+ ): Promise<readonly MemberProfileRecord[]> {
1174
+ const data = await requestGraphQl<SearchMembersQueryData>(
1175
+ `query SearchMembers($searchText: String!) {
1176
+ searchMembers(searchText: $searchText) {
1177
+ email
1178
+ memberId
1179
+ name
1180
+ }
1181
+ }`,
1182
+ { searchText },
1183
+ );
1184
+
1185
+ return data.searchMembers;
1186
+ }
1187
+
1188
+ export async function listMemberDirectoryPage({
1189
+ page,
1190
+ pageSize,
1191
+ searchText = '',
1192
+ }: {
1193
+ readonly page: number;
1194
+ readonly pageSize: number;
1195
+ readonly searchText?: string;
1196
+ }): Promise<MemberDirectoryPage> {
1197
+ const data = await requestGraphQl<MemberDirectoryPageQueryData>(
1198
+ `query MemberDirectoryPage($page: Int, $pageSize: Int, $searchText: String!) {
1199
+ searchMembers(page: $page, pageSize: $pageSize, searchText: $searchText) {
1200
+ email
1201
+ memberId
1202
+ name
1203
+ }
1204
+ memberCount(searchText: $searchText)
1205
+ }`,
1206
+ { page, pageSize, searchText },
1207
+ );
1208
+
1209
+ return {
1210
+ members: data.searchMembers,
1211
+ totalCount: data.memberCount,
1212
+ };
1213
+ }
1214
+
1215
+ export async function listDelegationRules({
1216
+ includeInactive = true,
1217
+ principalMemberId = null,
1218
+ }: {
1219
+ readonly includeInactive?: boolean;
1220
+ readonly principalMemberId?: string | null;
1221
+ } = {}): Promise<readonly DelegationRuleRecord[]> {
1222
+ const data = await requestGraphQl<DelegationRulesQueryData>(
1223
+ `query DelegationRules($includeInactive: Boolean, $principalMemberId: String) {
1224
+ delegationRules(
1225
+ includeInactive: $includeInactive
1226
+ principalMemberId: $principalMemberId
1227
+ ) {
1228
+ agentMemberId
1229
+ createdAt
1230
+ createdByMemberId
1231
+ endAt
1232
+ id
1233
+ principalMemberId
1234
+ priority
1235
+ requiresConfirmation
1236
+ revokedAt
1237
+ revokedByMemberId
1238
+ scopeConditionCel
1239
+ scopeTemplateIds
1240
+ scopeType
1241
+ startAt
1242
+ status
1243
+ updatedAt
1244
+ }
1245
+ }`,
1246
+ { includeInactive, principalMemberId },
1247
+ );
1248
+
1249
+ return data.delegationRules;
1250
+ }
1251
+
1252
+ export async function listDelegationRulesPage({
1253
+ agentMemberId = null,
1254
+ includeInactive = true,
1255
+ page = 1,
1256
+ pageSize = 10,
1257
+ principalMemberId = null,
1258
+ scopeType = null,
1259
+ status = null,
1260
+ }: {
1261
+ readonly agentMemberId?: string | null;
1262
+ readonly includeInactive?: boolean;
1263
+ readonly page?: number;
1264
+ readonly pageSize?: number;
1265
+ readonly principalMemberId?: string | null;
1266
+ readonly scopeType?: DelegationScopeType | null;
1267
+ readonly status?: DelegationRuleStatus | null;
1268
+ } = {}): Promise<{
1269
+ readonly rules: readonly DelegationRuleRecord[];
1270
+ readonly totalCount: number;
1271
+ }> {
1272
+ const data = await requestGraphQl<DelegationRulesPageQueryData>(
1273
+ `query DelegationRulesPage(
1274
+ $includeInactive: Boolean
1275
+ $page: Int
1276
+ $pageSize: Int
1277
+ $principalMemberId: String
1278
+ $agentMemberId: String
1279
+ $scopeType: DelegationScopeType
1280
+ $status: DelegationRuleStatus
1281
+ ) {
1282
+ delegationRules(
1283
+ agentMemberId: $agentMemberId
1284
+ includeInactive: $includeInactive
1285
+ page: $page
1286
+ pageSize: $pageSize
1287
+ principalMemberId: $principalMemberId
1288
+ scopeType: $scopeType
1289
+ status: $status
1290
+ ) {
1291
+ agentMemberId
1292
+ createdAt
1293
+ createdByMemberId
1294
+ endAt
1295
+ id
1296
+ principalMemberId
1297
+ priority
1298
+ requiresConfirmation
1299
+ revokedAt
1300
+ revokedByMemberId
1301
+ scopeConditionCel
1302
+ scopeTemplateIds
1303
+ scopeType
1304
+ startAt
1305
+ status
1306
+ updatedAt
1307
+ }
1308
+ delegationRuleCount(
1309
+ agentMemberId: $agentMemberId
1310
+ includeInactive: $includeInactive
1311
+ principalMemberId: $principalMemberId
1312
+ scopeType: $scopeType
1313
+ status: $status
1314
+ )
1315
+ }`,
1316
+ {
1317
+ agentMemberId,
1318
+ includeInactive,
1319
+ page,
1320
+ pageSize,
1321
+ principalMemberId,
1322
+ scopeType,
1323
+ status,
1324
+ },
1325
+ );
1326
+
1327
+ return {
1328
+ rules: data.delegationRules,
1329
+ totalCount: data.delegationRuleCount,
1330
+ };
1331
+ }
1332
+
1333
+ export async function createDelegationRule(input: {
1334
+ readonly agentMemberId: string;
1335
+ readonly createdByMemberId: string | null;
1336
+ readonly endAt: string | null;
1337
+ readonly principalMemberId: string;
1338
+ readonly priority: number;
1339
+ readonly requiresConfirmation: boolean;
1340
+ readonly scopeConditionCel: string | null;
1341
+ readonly scopeTemplateIds: readonly string[];
1342
+ readonly scopeType: DelegationScopeType;
1343
+ readonly startAt: string | null;
1344
+ }): Promise<DelegationRuleRecord> {
1345
+ const data = await requestGraphQl<CreateDelegationRuleMutationData>(
1346
+ `mutation CreateDelegationRule($input: CreateDelegationRuleInput!) {
1347
+ createDelegationRule(input: $input) {
1348
+ agentMemberId
1349
+ createdAt
1350
+ createdByMemberId
1351
+ endAt
1352
+ id
1353
+ principalMemberId
1354
+ priority
1355
+ requiresConfirmation
1356
+ revokedAt
1357
+ revokedByMemberId
1358
+ scopeConditionCel
1359
+ scopeTemplateIds
1360
+ scopeType
1361
+ startAt
1362
+ status
1363
+ updatedAt
1364
+ }
1365
+ }`,
1366
+ { input },
1367
+ );
1368
+
1369
+ return data.createDelegationRule;
1370
+ }
1371
+
1372
+ export async function revokeDelegationRule({
1373
+ id,
1374
+ revokedByMemberId,
1375
+ }: {
1376
+ readonly id: string;
1377
+ readonly revokedByMemberId: string | null;
1378
+ }): Promise<DelegationRuleRecord> {
1379
+ const data = await requestGraphQl<RevokeDelegationRuleMutationData>(
1380
+ `mutation RevokeDelegationRule($id: String!, $revokedByMemberId: String) {
1381
+ revokeDelegationRule(id: $id, revokedByMemberId: $revokedByMemberId) {
1382
+ agentMemberId
1383
+ createdAt
1384
+ createdByMemberId
1385
+ endAt
1386
+ id
1387
+ principalMemberId
1388
+ priority
1389
+ requiresConfirmation
1390
+ revokedAt
1391
+ revokedByMemberId
1392
+ scopeConditionCel
1393
+ scopeTemplateIds
1394
+ scopeType
1395
+ startAt
1396
+ status
1397
+ updatedAt
1398
+ }
1399
+ }`,
1400
+ { id, revokedByMemberId },
1401
+ );
1402
+
1403
+ return data.revokeDelegationRule;
1404
+ }
1405
+
1406
+ export async function listNotifications({
1407
+ includeRead = true,
1408
+ page = 1,
1409
+ pageSize = 10,
1410
+ recipientMemberId,
1411
+ }: {
1412
+ readonly includeRead?: boolean;
1413
+ readonly page?: number;
1414
+ readonly pageSize?: number;
1415
+ readonly recipientMemberId: string;
1416
+ }): Promise<{
1417
+ readonly notifications: readonly NotificationRecord[];
1418
+ readonly totalCount: number;
1419
+ readonly unreadCount: number;
1420
+ }> {
1421
+ const data = await requestGraphQl<NotificationsQueryData>(
1422
+ `query Notifications(
1423
+ $recipientMemberId: String!
1424
+ $includeRead: Boolean
1425
+ $page: Int
1426
+ $pageSize: Int
1427
+ ) {
1428
+ notifications(
1429
+ includeRead: $includeRead
1430
+ page: $page
1431
+ pageSize: $pageSize
1432
+ recipientMemberId: $recipientMemberId
1433
+ ) {
1434
+ actionable
1435
+ attemptCount
1436
+ body
1437
+ channel
1438
+ createdAt
1439
+ deliveredAt
1440
+ deliveryError
1441
+ deliveryTarget
1442
+ id
1443
+ instanceId
1444
+ lastAttemptAt
1445
+ nextRetryAt
1446
+ payloadJson
1447
+ readAt
1448
+ recipientMemberId
1449
+ resolution
1450
+ resolvedAt
1451
+ sentAt
1452
+ status
1453
+ taskId
1454
+ title
1455
+ type
1456
+ }
1457
+ notificationCount(
1458
+ includeRead: $includeRead
1459
+ recipientMemberId: $recipientMemberId
1460
+ )
1461
+ unreadNotificationCount(recipientMemberId: $recipientMemberId)
1462
+ }`,
1463
+ { includeRead, page, pageSize, recipientMemberId },
1464
+ );
1465
+
1466
+ return {
1467
+ notifications: data.notifications,
1468
+ totalCount: data.notificationCount,
1469
+ unreadCount: data.unreadNotificationCount,
1470
+ };
1471
+ }
1472
+
1473
+ export async function readUnreadNotificationCount(
1474
+ recipientMemberId: string,
1475
+ ): Promise<number> {
1476
+ const data = await requestGraphQl<{
1477
+ readonly unreadNotificationCount: number;
1478
+ }>(
1479
+ `query UnreadNotificationCount($recipientMemberId: String!) {
1480
+ unreadNotificationCount(recipientMemberId: $recipientMemberId)
1481
+ }`,
1482
+ { recipientMemberId },
1483
+ );
1484
+
1485
+ return data.unreadNotificationCount;
1486
+ }
1487
+
1488
+ export async function markNotificationRead({
1489
+ id,
1490
+ readerMemberId,
1491
+ }: {
1492
+ readonly id: string;
1493
+ readonly readerMemberId: string;
1494
+ }): Promise<Pick<NotificationRecord, 'id' | 'readAt' | 'status'>> {
1495
+ const data = await requestGraphQl<MarkNotificationReadMutationData>(
1496
+ `mutation MarkNotificationRead($id: String!, $readerMemberId: String) {
1497
+ markNotificationRead(id: $id, readerMemberId: $readerMemberId) {
1498
+ id
1499
+ readAt
1500
+ status
1501
+ }
1502
+ }`,
1503
+ { id, readerMemberId },
1504
+ );
1505
+
1506
+ return data.markNotificationRead;
1507
+ }
1508
+
1509
+ export async function markAllNotificationsRead({
1510
+ recipientMemberId,
1511
+ }: {
1512
+ readonly recipientMemberId: string;
1513
+ }): Promise<number> {
1514
+ const data = await requestGraphQl<{
1515
+ readonly markAllNotificationsRead: number;
1516
+ }>(
1517
+ `mutation MarkAllNotificationsRead($recipientMemberId: String) {
1518
+ markAllNotificationsRead(recipientMemberId: $recipientMemberId)
1519
+ }`,
1520
+ { recipientMemberId },
1521
+ );
1522
+
1523
+ return data.markAllNotificationsRead;
1524
+ }
1525
+
1526
+ export async function readNotificationPreference(
1527
+ memberId: string,
1528
+ ): Promise<NotificationPreferenceRecord> {
1529
+ const data = await requestGraphQl<NotificationPreferenceQueryData>(
1530
+ `query NotificationPreference($memberId: String!) {
1531
+ notificationPreference(memberId: $memberId) {
1532
+ emailDigestMode
1533
+ emailEnabled
1534
+ inAppEnabled
1535
+ memberId
1536
+ quietHoursEnd
1537
+ quietHoursStart
1538
+ updatedAt
1539
+ }
1540
+ }`,
1541
+ { memberId },
1542
+ );
1543
+
1544
+ return data.notificationPreference;
1545
+ }
1546
+
1547
+ export async function updateNotificationPreference(input: {
1548
+ readonly emailDigestMode: NotificationDigestMode;
1549
+ readonly emailEnabled: boolean;
1550
+ readonly inAppEnabled: boolean;
1551
+ readonly memberId: string;
1552
+ readonly quietHoursEnd: string | null;
1553
+ readonly quietHoursStart: string | null;
1554
+ }): Promise<NotificationPreferenceRecord> {
1555
+ const data = await requestGraphQl<UpdateNotificationPreferenceMutationData>(
1556
+ `mutation UpdateNotificationPreference($input: UpdateNotificationPreferenceInput!) {
1557
+ updateNotificationPreference(input: $input) {
1558
+ emailDigestMode
1559
+ emailEnabled
1560
+ inAppEnabled
1561
+ memberId
1562
+ quietHoursEnd
1563
+ quietHoursStart
1564
+ updatedAt
1565
+ }
1566
+ }`,
1567
+ { input },
1568
+ );
1569
+
1570
+ return data.updateNotificationPreference;
1571
+ }
1572
+
1573
+ export async function submitApprovalInstance({
1574
+ formData,
1575
+ initiatorMemberId,
1576
+ templateId,
1577
+ title,
1578
+ }: {
1579
+ readonly formData: Readonly<Record<string, unknown>>;
1580
+ readonly initiatorMemberId: string;
1581
+ readonly templateId: string;
1582
+ readonly title: string | null;
1583
+ }): Promise<string> {
1584
+ const data = await requestGraphQl<SubmitApprovalInstanceMutationData>(
1585
+ `mutation SubmitApprovalInstance($input: SubmitApprovalInstanceInput!) {
1586
+ submitApprovalInstance(input: $input) {
1587
+ id
1588
+ }
1589
+ }`,
1590
+ {
1591
+ input: {
1592
+ formDataJson: JSON.stringify(formData),
1593
+ initiatorMemberId,
1594
+ initiatorMetadataSnapshotJson: null,
1595
+ templateId,
1596
+ title,
1597
+ },
1598
+ },
1599
+ );
1600
+
1601
+ return data.submitApprovalInstance.id;
1602
+ }
1603
+
1604
+ export async function decideTask({
1605
+ action,
1606
+ comment,
1607
+ decidedByMemberId,
1608
+ returnToNodeId = null,
1609
+ taskId,
1610
+ transferToMemberId = null,
1611
+ }: {
1612
+ readonly action: TaskDecisionAction;
1613
+ readonly comment: string | null;
1614
+ readonly decidedByMemberId: string;
1615
+ readonly returnToNodeId?: string | null;
1616
+ readonly taskId: string;
1617
+ readonly transferToMemberId?: string | null;
1618
+ }): Promise<TaskDecisionRecord> {
1619
+ const data = await requestGraphQl<DecideTaskMutationData>(
1620
+ `mutation DecideTask($input: DecideTaskInput!) {
1621
+ decideTask(input: $input) {
1622
+ action
1623
+ comment
1624
+ decidedAt
1625
+ decidedByMemberId
1626
+ id
1627
+ returnToNodeId
1628
+ signatureId
1629
+ taskId
1630
+ transferToMemberId
1631
+ }
1632
+ }`,
1633
+ {
1634
+ input: {
1635
+ action,
1636
+ comment,
1637
+ decidedByMemberId,
1638
+ returnToNodeId,
1639
+ taskId,
1640
+ transferToMemberId,
1641
+ },
1642
+ },
1643
+ );
1644
+
1645
+ return data.decideTask;
1646
+ }
1647
+
1648
+ export async function cancelApprovalInstance({
1649
+ cancelledByMemberId,
1650
+ comment,
1651
+ instanceId,
1652
+ }: {
1653
+ readonly cancelledByMemberId: string;
1654
+ readonly comment: string | null;
1655
+ readonly instanceId: string;
1656
+ }): Promise<void> {
1657
+ await requestGraphQl<CancelApprovalInstanceMutationData>(
1658
+ `mutation CancelApprovalInstance($input: CancelApprovalInstanceInput!) {
1659
+ cancelApprovalInstance(input: $input) {
1660
+ id
1661
+ state
1662
+ }
1663
+ }`,
1664
+ {
1665
+ input: {
1666
+ cancelledByMemberId,
1667
+ comment,
1668
+ instanceId,
1669
+ },
1670
+ },
1671
+ );
1672
+ }
1673
+
1674
+ export async function resubmitApprovalInstance({
1675
+ formData,
1676
+ initiatorMemberId,
1677
+ instanceId,
1678
+ title,
1679
+ }: {
1680
+ readonly formData: Readonly<Record<string, unknown>>;
1681
+ readonly initiatorMemberId: string;
1682
+ readonly instanceId: string;
1683
+ readonly title: string | null;
1684
+ }): Promise<void> {
1685
+ await requestGraphQl<ResubmitApprovalInstanceMutationData>(
1686
+ `mutation ResubmitApprovalInstance($input: ResubmitApprovalInstanceInput!) {
1687
+ resubmitApprovalInstance(input: $input) {
1688
+ id
1689
+ state
1690
+ }
1691
+ }`,
1692
+ {
1693
+ input: {
1694
+ formDataJson: JSON.stringify(formData),
1695
+ initiatorMemberId,
1696
+ instanceId,
1697
+ title,
1698
+ },
1699
+ },
1700
+ );
1701
+ }
1702
+
1703
+ const ADHOC_DIRECTIVE_FIELDS = `
1704
+ channels
1705
+ comment
1706
+ consumedAt
1707
+ createdAt
1708
+ createdByMemberId
1709
+ id
1710
+ instanceId
1711
+ onReject
1712
+ originNodeId
1713
+ originTaskId
1714
+ status
1715
+ targetKind
1716
+ targetValueJson
1717
+ type
1718
+ `;
1719
+
1720
+ const ADHOC_TASK_FIELDS = `
1721
+ adhocDirectiveId
1722
+ adhocOriginTaskId
1723
+ adhocType
1724
+ assigneeMemberId
1725
+ assignmentType
1726
+ candidateMemberIds
1727
+ completedAt
1728
+ createdAt
1729
+ decisionPolicySnapshotJson
1730
+ delegationChainJson
1731
+ id
1732
+ instanceId
1733
+ isAdhoc
1734
+ nodeId
1735
+ openedAt
1736
+ originalAssigneeMemberId
1737
+ slaDueAt
1738
+ status
1739
+ tokenId
1740
+ `;
1741
+
1742
+ interface RequestAdhocCountersignMutationData {
1743
+ readonly requestAdhocCountersign: AdhocDirectiveRecord;
1744
+ }
1745
+
1746
+ interface RequestAdhocPreApprovalMutationData {
1747
+ readonly requestAdhocPreApproval: TaskRecord;
1748
+ }
1749
+
1750
+ interface ConfigureAdhocStageNotificationMutationData {
1751
+ readonly configureAdhocStageNotification: AdhocDirectiveRecord;
1752
+ }
1753
+
1754
+ interface ConfigureAdhocCompletionNotificationMutationData {
1755
+ readonly configureAdhocCompletionNotification: AdhocDirectiveRecord;
1756
+ }
1757
+
1758
+ interface CancelAdhocDirectiveMutationData {
1759
+ readonly cancelAdhocDirective: AdhocDirectiveRecord;
1760
+ }
1761
+
1762
+ interface AdhocDirectivesQueryData {
1763
+ readonly adhocDirectives: readonly AdhocDirectiveRecord[];
1764
+ }
1765
+
1766
+ function buildAdhocTargetInput(
1767
+ target: AdhocTargetOptions,
1768
+ ): Readonly<Record<string, unknown>> {
1769
+ return {
1770
+ includeDescendants: target.includeDescendants ?? null,
1771
+ kind: target.kind,
1772
+ memberIds: target.memberIds ? [...target.memberIds] : null,
1773
+ orgUnitId: target.orgUnitId ?? null,
1774
+ positionId: target.positionId ?? null,
1775
+ webhookHeadersJson: target.webhookHeaders
1776
+ ? JSON.stringify(target.webhookHeaders)
1777
+ : null,
1778
+ webhookUrl: target.webhookUrl ?? null,
1779
+ };
1780
+ }
1781
+
1782
+ /**
1783
+ * Ad-hoc countersign (臨時會簽): asks the given target to co-sign the NEXT
1784
+ * stage of this instance. When the next user task is created, a parallel
1785
+ * ad-hoc task is spawned for the target, and the flow only advances past
1786
+ * that stage once BOTH the original task and every countersign task are
1787
+ * approved. A countersigner's rejection rejects the whole instance.
1788
+ *
1789
+ * Only the current task's assignee/candidate may call this, and only on
1790
+ * nodes whose template sets `allowAddSigner: true`. Returns the recorded
1791
+ * directive (`PENDING` until the next user task consumes it; withdrawable
1792
+ * via {@link cancelAdhocDirective} while pending). Never alters the
1793
+ * workflow template.
1794
+ */
1795
+ export async function requestAdhocCountersign({
1796
+ comment = null,
1797
+ target,
1798
+ taskId,
1799
+ }: {
1800
+ readonly comment?: string | null;
1801
+ readonly target: AdhocTargetOptions;
1802
+ readonly taskId: string;
1803
+ }): Promise<AdhocDirectiveRecord> {
1804
+ const data = await requestGraphQl<RequestAdhocCountersignMutationData>(
1805
+ `mutation RequestAdhocCountersign($taskId: ID!, $target: AdhocTargetInput!, $comment: String) {
1806
+ requestAdhocCountersign(taskId: $taskId, target: $target, comment: $comment) {
1807
+ ${ADHOC_DIRECTIVE_FIELDS}
1808
+ }
1809
+ }`,
1810
+ { comment, target: buildAdhocTargetInput(target), taskId },
1811
+ );
1812
+
1813
+ return data.requestAdhocCountersign;
1814
+ }
1815
+
1816
+ /**
1817
+ * Ad-hoc pre-approval (臨時加簽): immediately spawns a blocking ad-hoc task
1818
+ * for the given target on the CURRENT stage. Even after the original
1819
+ * approver approves, the flow stays on this stage until the pre-approver
1820
+ * also approves. `onReject` decides what happens when the pre-approver
1821
+ * rejects: `REJECT_INSTANCE` rejects the whole instance;
1822
+ * `RETURN_TO_ORIGIN` hands the decision back to the original approver
1823
+ * (re-opening their task if they had already approved).
1824
+ *
1825
+ * Only the current task's assignee/candidate may call this, and only on
1826
+ * nodes whose template sets `allowAddSigner: true`. Returns the spawned
1827
+ * ad-hoc task. Never alters the workflow template.
1828
+ */
1829
+ export async function requestAdhocPreApproval({
1830
+ comment = null,
1831
+ onReject,
1832
+ target,
1833
+ taskId,
1834
+ }: {
1835
+ readonly comment?: string | null;
1836
+ readonly onReject: AdhocPreApprovalRejectBehavior;
1837
+ readonly target: AdhocTargetOptions;
1838
+ readonly taskId: string;
1839
+ }): Promise<TaskRecord> {
1840
+ const data = await requestGraphQl<RequestAdhocPreApprovalMutationData>(
1841
+ `mutation RequestAdhocPreApproval($taskId: ID!, $target: AdhocTargetInput!, $onReject: AdhocPreApprovalRejectBehavior!, $comment: String) {
1842
+ requestAdhocPreApproval(taskId: $taskId, target: $target, onReject: $onReject, comment: $comment) {
1843
+ ${ADHOC_TASK_FIELDS}
1844
+ }
1845
+ }`,
1846
+ {
1847
+ comment,
1848
+ onReject,
1849
+ target: buildAdhocTargetInput(target),
1850
+ taskId,
1851
+ },
1852
+ );
1853
+
1854
+ return data.requestAdhocPreApproval;
1855
+ }
1856
+
1857
+ /**
1858
+ * Ad-hoc stage notification (臨時階段通知): notifies the target once the
1859
+ * CURRENT stage ends — regardless of outcome (approved, rejected, or
1860
+ * returned). Targets may be members, a position, an org unit, or a webhook
1861
+ * URL. `channels` defaults to in-app notification.
1862
+ *
1863
+ * Available to the current task's assignee/candidate on any user-task node
1864
+ * (no `allowAddSigner` requirement). Returns the recorded directive
1865
+ * (withdrawable via {@link cancelAdhocDirective} while pending). Never
1866
+ * alters the workflow template.
1867
+ */
1868
+ export async function configureAdhocStageNotification({
1869
+ channels = null,
1870
+ target,
1871
+ taskId,
1872
+ }: {
1873
+ readonly channels?: readonly NotificationChannel[] | null;
1874
+ readonly target: AdhocTargetOptions;
1875
+ readonly taskId: string;
1876
+ }): Promise<AdhocDirectiveRecord> {
1877
+ const data =
1878
+ await requestGraphQl<ConfigureAdhocStageNotificationMutationData>(
1879
+ `mutation ConfigureAdhocStageNotification($taskId: ID!, $input: AdhocNotificationInput!) {
1880
+ configureAdhocStageNotification(taskId: $taskId, input: $input) {
1881
+ ${ADHOC_DIRECTIVE_FIELDS}
1882
+ }
1883
+ }`,
1884
+ {
1885
+ input: {
1886
+ channels: channels ? [...channels] : null,
1887
+ target: buildAdhocTargetInput(target),
1888
+ },
1889
+ taskId,
1890
+ },
1891
+ );
1892
+
1893
+ return data.configureAdhocStageNotification;
1894
+ }
1895
+
1896
+ /**
1897
+ * Ad-hoc completion notification (臨時完成通知): notifies the target once
1898
+ * the WHOLE instance reaches a terminal state (`APPROVED`, `REJECTED`, or
1899
+ * `CANCELLED`). Targets may be members, a position, an org unit, or a
1900
+ * webhook URL. `channels` defaults to in-app notification.
1901
+ *
1902
+ * Available to the current task's assignee/candidate on any user-task node
1903
+ * (no `allowAddSigner` requirement). Returns the recorded directive
1904
+ * (withdrawable via {@link cancelAdhocDirective} while pending). Never
1905
+ * alters the workflow template.
1906
+ */
1907
+ export async function configureAdhocCompletionNotification({
1908
+ channels = null,
1909
+ target,
1910
+ taskId,
1911
+ }: {
1912
+ readonly channels?: readonly NotificationChannel[] | null;
1913
+ readonly target: AdhocTargetOptions;
1914
+ readonly taskId: string;
1915
+ }): Promise<AdhocDirectiveRecord> {
1916
+ const data =
1917
+ await requestGraphQl<ConfigureAdhocCompletionNotificationMutationData>(
1918
+ `mutation ConfigureAdhocCompletionNotification($taskId: ID!, $input: AdhocNotificationInput!) {
1919
+ configureAdhocCompletionNotification(taskId: $taskId, input: $input) {
1920
+ ${ADHOC_DIRECTIVE_FIELDS}
1921
+ }
1922
+ }`,
1923
+ {
1924
+ input: {
1925
+ channels: channels ? [...channels] : null,
1926
+ target: buildAdhocTargetInput(target),
1927
+ },
1928
+ taskId,
1929
+ },
1930
+ );
1931
+
1932
+ return data.configureAdhocCompletionNotification;
1933
+ }
1934
+
1935
+ /**
1936
+ * Withdraws a still-`PENDING` ad-hoc directive (countersign / stage-notify /
1937
+ * completion-notify) before it takes effect. Only the directive's creator
1938
+ * may withdraw it; pre-approval directives are consumed immediately on
1939
+ * creation and therefore cannot be withdrawn.
1940
+ */
1941
+ export async function cancelAdhocDirective(
1942
+ directiveId: string,
1943
+ ): Promise<AdhocDirectiveRecord> {
1944
+ const data = await requestGraphQl<CancelAdhocDirectiveMutationData>(
1945
+ `mutation CancelAdhocDirective($directiveId: ID!) {
1946
+ cancelAdhocDirective(directiveId: $directiveId) {
1947
+ ${ADHOC_DIRECTIVE_FIELDS}
1948
+ }
1949
+ }`,
1950
+ { directiveId },
1951
+ );
1952
+
1953
+ return data.cancelAdhocDirective;
1954
+ }
1955
+
1956
+ /**
1957
+ * Lists every ad-hoc directive recorded on one approval instance, ordered by
1958
+ * creation time. Use the `status` field to find directives that are still
1959
+ * pending (withdrawable) versus already consumed or cancelled.
1960
+ */
1961
+ export async function listAdhocDirectives(
1962
+ instanceId: string,
1963
+ ): Promise<readonly AdhocDirectiveRecord[]> {
1964
+ const data = await requestGraphQl<AdhocDirectivesQueryData>(
1965
+ `query AdhocDirectives($instanceId: String!) {
1966
+ adhocDirectives(instanceId: $instanceId) {
1967
+ ${ADHOC_DIRECTIVE_FIELDS}
1968
+ }
1969
+ }`,
1970
+ { instanceId },
1971
+ );
1972
+
1973
+ // Tolerate hosts (and test mocks) whose GraphQL layer does not answer the
1974
+ // adhocDirectives query yet.
1975
+ return data.adhocDirectives ?? [];
1976
+ }
1977
+
1978
+ export function readApprovalInstanceCaseTitle(
1979
+ instance: ApprovalInstanceRecord,
1980
+ ): string {
1981
+ return readFormDataCaseTitle({
1982
+ fallbackTitle: instance.title || instance.id,
1983
+ formData: instance.formData,
1984
+ schema: instance.formDefinitionSnapshot.schema ?? null,
1985
+ uiSchema: instance.formDefinitionSnapshot.uiSchema ?? null,
1986
+ });
1987
+ }
1988
+
1989
+ export function readFormDataCaseTitle({
1990
+ fallbackTitle,
1991
+ formData,
1992
+ schema,
1993
+ uiSchema,
1994
+ }: {
1995
+ readonly fallbackTitle: string;
1996
+ readonly formData: WorkflowFormData;
1997
+ readonly schema: FormDefinitionSchema | null;
1998
+ readonly uiSchema: FormUiSchema | null;
1999
+ }): string {
2000
+ const firstField = readFirstCaseTitleField(schema, uiSchema);
2001
+
2002
+ if (!firstField) {
2003
+ return fallbackTitle;
2004
+ }
2005
+
2006
+ const valueLabel = readFieldValueLabel(
2007
+ firstField,
2008
+ formData[firstField.fieldKey],
2009
+ );
2010
+
2011
+ return valueLabel ? `${firstField.label}:${valueLabel}` : fallbackTitle;
2012
+ }
2013
+
2014
+ function parseInstanceJson(record: InstanceJsonRecord): ApprovalInstanceRecord {
2015
+ return {
2016
+ ...record,
2017
+ formData: JSON.parse(record.formDataJson) as WorkflowFormData,
2018
+ formDefinitionSnapshot: JSON.parse(
2019
+ record.formDefinitionSnapshotJson,
2020
+ ) as FormDefinitionSnapshot,
2021
+ workflowSnapshot: JSON.parse(
2022
+ record.workflowSnapshotJson,
2023
+ ) as WorkflowDefinition,
2024
+ };
2025
+ }
2026
+
2027
+ async function readFileBase64(file: File): Promise<string> {
2028
+ const buffer = await file.arrayBuffer();
2029
+ const bytes = new Uint8Array(buffer);
2030
+ const chunkSize = 0x8000;
2031
+ const chunks = Array.from(
2032
+ { length: Math.ceil(bytes.length / chunkSize) },
2033
+ (_, index): string =>
2034
+ String.fromCharCode(
2035
+ ...bytes.slice(index * chunkSize, (index + 1) * chunkSize),
2036
+ ),
2037
+ );
2038
+
2039
+ return btoa(chunks.join(''));
2040
+ }
2041
+
2042
+ async function hashFileSha256(file: File): Promise<string> {
2043
+ const digest = await crypto.subtle.digest(
2044
+ 'SHA-256',
2045
+ await file.arrayBuffer(),
2046
+ );
2047
+ const bytes = Array.from(new Uint8Array(digest));
2048
+
2049
+ return bytes.map((byte) => byte.toString(16).padStart(2, '0')).join('');
2050
+ }
2051
+
2052
+ function readFirstCaseTitleField(
2053
+ schema: FormDefinitionSchema | null,
2054
+ uiSchema: FormUiSchema | null,
2055
+ ): FormFieldDefinition | null {
2056
+ if (!schema?.fields.length) {
2057
+ return null;
2058
+ }
2059
+
2060
+ const fieldsByKey = new Map(
2061
+ schema.fields.map((field) => [field.fieldKey, field]),
2062
+ );
2063
+ const firstLayoutField = uiSchema?.layout
2064
+ .map((layoutItem) => fieldsByKey.get(layoutItem.fieldKey) ?? null)
2065
+ .find((field): field is FormFieldDefinition => Boolean(field));
2066
+
2067
+ return firstLayoutField ?? schema.fields[0] ?? null;
2068
+ }
2069
+
2070
+ function readFieldValueLabel(
2071
+ field: FormFieldDefinition,
2072
+ value: FormFieldValue | undefined,
2073
+ ): string | null {
2074
+ if (typeof value === 'undefined' || value === null) {
2075
+ return null;
2076
+ }
2077
+
2078
+ if (Array.isArray(value)) {
2079
+ return readArrayFieldValueLabel(field, value);
2080
+ }
2081
+
2082
+ if (field.type === 'boolean') {
2083
+ return value === true ? '是' : '否';
2084
+ }
2085
+
2086
+ if (isSelectFieldDefinition(field) && typeof value === 'string') {
2087
+ return readSelectOptionLabel(field, value);
2088
+ }
2089
+
2090
+ const valueLabel = String(value).trim();
2091
+
2092
+ return valueLabel || null;
2093
+ }
2094
+
2095
+ function readArrayFieldValueLabel(
2096
+ field: FormFieldDefinition,
2097
+ value: readonly string[],
2098
+ ): string | null {
2099
+ const labels = isSelectFieldDefinition(field)
2100
+ ? value.map((item) => readSelectOptionLabel(field, item))
2101
+ : value;
2102
+ const label = labels.filter((item) => item.trim()).join('、');
2103
+
2104
+ return label || null;
2105
+ }
2106
+
2107
+ function readSelectOptionLabel(
2108
+ field: SelectFieldDefinition,
2109
+ value: string,
2110
+ ): string {
2111
+ return (
2112
+ field.options.find((option) => option.value === value)?.label ?? value
2113
+ ).trim();
2114
+ }
2115
+
2116
+ function isSelectFieldDefinition(
2117
+ field: FormFieldDefinition,
2118
+ ): field is SelectFieldDefinition {
2119
+ return (
2120
+ field.type === 'select' ||
2121
+ field.type === 'radio' ||
2122
+ field.type === 'checkbox'
2123
+ );
2124
+ }