@rytass/bpm-core-nestjs-module 0.21.4 → 0.22.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.
- package/CHANGELOG.md +23 -0
- package/README.md +9 -1
- package/package.json +2 -2
- package/src/lib/form-data-source/form-data-source.service.js +4 -12
- package/src/lib/form-data-source/form-data-source.service.js.map +1 -1
- package/src/lib/identity/identity-options.d.ts +1 -1
- package/src/lib/identity/identity.queries.d.ts +0 -2
- package/src/lib/identity/identity.queries.js +0 -11
- package/src/lib/identity/identity.queries.js.map +1 -1
- package/src/lib/workflow-engine/initiator-custom-fields-resolver.token.d.ts +31 -11
- package/src/lib/workflow-engine/initiator-custom-fields-resolver.token.js.map +1 -1
- package/src/lib/workflow-engine/workflow-engine.queries.d.ts +4 -2
- package/src/lib/workflow-engine/workflow-engine.queries.js +49 -6
- package/src/lib/workflow-engine/workflow-engine.queries.js.map +1 -1
- package/src/lib/workflow-engine/workflow-engine.service.d.ts +79 -4
- package/src/lib/workflow-engine/workflow-engine.service.js +223 -68
- package/src/lib/workflow-engine/workflow-engine.service.js.map +1 -1
|
@@ -210,6 +210,8 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
210
210
|
this.formDataSourceValueResolver = formDataSourceValueResolver;
|
|
211
211
|
this.webhookDeliveryService = webhookDeliveryService;
|
|
212
212
|
this.logger = new common_1.Logger(WorkflowEngineService_1.name);
|
|
213
|
+
/** In-flight inbox reads by member; see `readSharedOpenInboxTasks`. */
|
|
214
|
+
this.openInboxReadsInFlight = new Map();
|
|
213
215
|
this.approvalInstanceFieldFilterCompiler = new approval_instance_field_filter_compiler_1.ApprovalInstanceFieldFilterCompiler();
|
|
214
216
|
this.approvalInstanceFilterableFieldPolicy = new approval_instance_filterable_field_policy_1.ApprovalInstanceFilterableFieldPolicy();
|
|
215
217
|
/** Webhooks each open engine transaction will send once it has committed. */
|
|
@@ -2500,37 +2502,86 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
2500
2502
|
memberId,
|
|
2501
2503
|
submission: null,
|
|
2502
2504
|
});
|
|
2503
|
-
return templates.filter((template) =>
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
}
|
|
2505
|
+
return templates.filter((template) => this.isInitiatorPolicyMet(template, template.currentVersionId
|
|
2506
|
+
? (versionById.get(template.currentVersionId) ?? null)
|
|
2507
|
+
: null, initiatorMetadataSnapshot, 'launchable templates'));
|
|
2508
|
+
}
|
|
2509
|
+
/**
|
|
2510
|
+
* The published version whose form `memberId` may read data-source options
|
|
2511
|
+
* for when starting `templateId`, or `null` when they may not.
|
|
2512
|
+
*
|
|
2513
|
+
* It applies the same checks as {@link listLaunchableApprovalTemplates} to
|
|
2514
|
+
* that one template, but asks the host's initiator `customFields` resolver
|
|
2515
|
+
* with `purpose: 'FORM_OPTIONS'` and the template and version ids. A host can
|
|
2516
|
+
* therefore open a template's options to members who may not submit it
|
|
2517
|
+
* themselves — a template only the host's backend submits — without the
|
|
2518
|
+
* template appearing in their launchable list. The snapshot is used for this
|
|
2519
|
+
* judgement only and is never stored. A resolver failure judges against
|
|
2520
|
+
* `customFields: {}`, as the list does, so a policy that reads
|
|
2521
|
+
* `initiator.customFields` then fails closed.
|
|
2522
|
+
*/
|
|
2523
|
+
async readFormOptionsTemplateVersion(templateId, memberId, authContext) {
|
|
2524
|
+
const template = await this.approvalTemplateRepository.findOne({
|
|
2525
|
+
where: { id: templateId, isActive: true },
|
|
2526
|
+
});
|
|
2527
|
+
if (!template?.currentVersionId) {
|
|
2528
|
+
return null;
|
|
2529
|
+
}
|
|
2530
|
+
const currentVersion = await this.approvalTemplateVersionRepository.findOne({
|
|
2531
|
+
where: {
|
|
2532
|
+
id: template.currentVersionId,
|
|
2533
|
+
status: template_enums_1.ApprovalTemplateVersionStatusEnum.PUBLISHED,
|
|
2534
|
+
},
|
|
2535
|
+
});
|
|
2536
|
+
if (!currentVersion?.formDefinitionVersionId) {
|
|
2537
|
+
return null;
|
|
2538
|
+
}
|
|
2539
|
+
const initiatorMetadataSnapshot = await this.buildInitiatorMetadataSnapshot({
|
|
2540
|
+
authContext: authContext ?? createWorkflowAuthContext(memberId),
|
|
2541
|
+
formOptions: {
|
|
2542
|
+
templateId: template.id,
|
|
2543
|
+
templateVersionId: currentVersion.id,
|
|
2544
|
+
},
|
|
2545
|
+
memberId,
|
|
2546
|
+
submission: null,
|
|
2533
2547
|
});
|
|
2548
|
+
return this.isInitiatorPolicyMet(template, currentVersion, initiatorMetadataSnapshot, 'form options')
|
|
2549
|
+
? currentVersion
|
|
2550
|
+
: null;
|
|
2551
|
+
}
|
|
2552
|
+
/**
|
|
2553
|
+
* Whether `template`'s current published version lets the member described
|
|
2554
|
+
* by `initiatorMetadataSnapshot` start it: the launchable list and the form
|
|
2555
|
+
* options check share this, so the two cannot drift apart. `scope` only
|
|
2556
|
+
* names the caller in the log.
|
|
2557
|
+
*/
|
|
2558
|
+
isInitiatorPolicyMet(template, currentVersion, initiatorMetadataSnapshot, scope) {
|
|
2559
|
+
if (!currentVersion?.formDefinitionVersionId) {
|
|
2560
|
+
return false;
|
|
2561
|
+
}
|
|
2562
|
+
// A policy in a dialect this version does not know cannot be evaluated,
|
|
2563
|
+
// so the template is not launchable (fail closed), but only that one:
|
|
2564
|
+
// publishing does not check the field yet (Phase 23 P2), and one such
|
|
2565
|
+
// version must not break the list for every member.
|
|
2566
|
+
if (hasExpressionText(currentVersion.initiatorPolicyCel) &&
|
|
2567
|
+
!isReadableExpressionDialect(currentVersion.workflowDefinition)) {
|
|
2568
|
+
this.logger.warn(`template=${template.id} version=${currentVersion.id} left out of ${scope}: unknown expression dialect`);
|
|
2569
|
+
return false;
|
|
2570
|
+
}
|
|
2571
|
+
// Each template is its own operation, so one slow template cannot use
|
|
2572
|
+
// up the allowance of the rest of the list. A policy that throws (for
|
|
2573
|
+
// example a missing `initiator.customFields.*` key, because the resolver
|
|
2574
|
+
// did not return it, or failed and the list fell back to `{}`) is also
|
|
2575
|
+
// only that template's problem: it is left out, and the error code is
|
|
2576
|
+
// logged, never the values.
|
|
2577
|
+
try {
|
|
2578
|
+
return (0, expression_budget_1.runExpressionOperation)(() => this.conditionService.evaluateBoolean(currentVersion.initiatorPolicyCel, buildInitiatorPolicyContext(initiatorMetadataSnapshot), 'initiatorPolicyCel', readExpressionDialectOptions(currentVersion.initiatorPolicyCel, currentVersion.workflowDefinition)));
|
|
2579
|
+
}
|
|
2580
|
+
catch (error) {
|
|
2581
|
+
const reason = error instanceof expression_errors_1.ExpressionRuntimeError ? error.code : 'UNEXPECTED';
|
|
2582
|
+
this.logger.warn(`template=${template.id} version=${currentVersion.id} left out of ${scope}: initiatorPolicyCel failed (${reason})`);
|
|
2583
|
+
return false;
|
|
2584
|
+
}
|
|
2534
2585
|
}
|
|
2535
2586
|
async listWorkflowTokens(instanceId, scope) {
|
|
2536
2587
|
await this.getApprovalInstance(instanceId, scope);
|
|
@@ -2546,8 +2597,43 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
2546
2597
|
where: { instanceId },
|
|
2547
2598
|
}));
|
|
2548
2599
|
}
|
|
2549
|
-
|
|
2550
|
-
|
|
2600
|
+
/**
|
|
2601
|
+
* The member's open tasks, newest first. Without `page` / `pageSize` every
|
|
2602
|
+
* one is returned, as before paging existed. The page is cut from the same
|
|
2603
|
+
* {@link readOpenInboxTasks} result the dashboard counters read, so the list,
|
|
2604
|
+
* {@link countInboxTasks} and the counters cannot disagree; only the page is
|
|
2605
|
+
* given its candidate summaries.
|
|
2606
|
+
*/
|
|
2607
|
+
async listInboxTasks(assigneeMemberId, options = {}) {
|
|
2608
|
+
const openTasks = await this.readSharedOpenInboxTasks(assigneeMemberId);
|
|
2609
|
+
if (!isTaskListPaged(options)) {
|
|
2610
|
+
return this.attachTaskCandidateSummaries(openTasks);
|
|
2611
|
+
}
|
|
2612
|
+
const offset = readPageOffset(options.page, options.pageSize);
|
|
2613
|
+
return this.attachTaskCandidateSummaries(openTasks.slice(offset, offset + normalizePageSize(options.pageSize)));
|
|
2614
|
+
}
|
|
2615
|
+
/** How many tasks {@link listInboxTasks} pages through. */
|
|
2616
|
+
async countInboxTasks(assigneeMemberId) {
|
|
2617
|
+
return (await this.readSharedOpenInboxTasks(assigneeMemberId)).length;
|
|
2618
|
+
}
|
|
2619
|
+
/**
|
|
2620
|
+
* {@link readOpenInboxTasks} for the inbox list and its count, joining a
|
|
2621
|
+
* read for the same member that is still in flight. The client asks for
|
|
2622
|
+
* `inboxTasks` and `inboxTaskCount` in one request and GraphQL resolves the
|
|
2623
|
+
* two side by side, so this turns two identical reads into one. An entry
|
|
2624
|
+
* lives only until its read settles: nothing is cached across requests, and
|
|
2625
|
+
* a read that started earlier is never served once it has finished.
|
|
2626
|
+
*/
|
|
2627
|
+
readSharedOpenInboxTasks(assigneeMemberId) {
|
|
2628
|
+
const inFlight = this.openInboxReadsInFlight.get(assigneeMemberId);
|
|
2629
|
+
if (inFlight) {
|
|
2630
|
+
return inFlight;
|
|
2631
|
+
}
|
|
2632
|
+
const read = this.readOpenInboxTasks(assigneeMemberId).finally(() => {
|
|
2633
|
+
this.openInboxReadsInFlight.delete(assigneeMemberId);
|
|
2634
|
+
});
|
|
2635
|
+
this.openInboxReadsInFlight.set(assigneeMemberId, read);
|
|
2636
|
+
return read;
|
|
2551
2637
|
}
|
|
2552
2638
|
/**
|
|
2553
2639
|
* The open tasks that are this member's own to act on — the single
|
|
@@ -2609,33 +2695,86 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
2609
2695
|
const settledTaskIds = new Set(settledCandidateRows.map((candidate) => candidate.taskId));
|
|
2610
2696
|
return openTasks.filter((task) => !settledTaskIds.has(task.id));
|
|
2611
2697
|
}
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2698
|
+
/**
|
|
2699
|
+
* The tasks this member has acted on, most recently acted on first: tasks
|
|
2700
|
+
* assigned to them that are COMPLETED, and tasks on which their own
|
|
2701
|
+
* candidate row is COMPLETED. The latter needs no status on the task: a
|
|
2702
|
+
* countersign still waiting on somebody else already carries their vote, and
|
|
2703
|
+
* the inbox has stopped showing it to them.
|
|
2704
|
+
*
|
|
2705
|
+
* "Acted on" is their candidate row's `decided_at`, else the task's
|
|
2706
|
+
* `completed_at`, else its `created_at`; ties fall back to the task id so no
|
|
2707
|
+
* row moves between pages. Paged in SQL. Without `page` / `pageSize` the
|
|
2708
|
+
* latest {@link DEFAULT_APPROVAL_HISTORY_TASK_LIMIT} are returned, as before
|
|
2709
|
+
* paging existed. `instanceState` keeps only tasks of instances in that
|
|
2710
|
+
* state (RUNNING for "acted on, not finished").
|
|
2711
|
+
*/
|
|
2712
|
+
async listApprovalHistoryTasks(assigneeMemberId, options = {}) {
|
|
2713
|
+
const paged = isTaskListPaged(options);
|
|
2714
|
+
const tasks = await this.createApprovalHistoryTaskQuery(assigneeMemberId, options.instanceState)
|
|
2715
|
+
.orderBy(this.readApprovalHistoryActedAtExpression(), 'DESC')
|
|
2716
|
+
.addOrderBy('"task"."id"', 'DESC')
|
|
2717
|
+
.offset(paged ? readPageOffset(options.page, options.pageSize) : 0)
|
|
2718
|
+
.limit(paged
|
|
2719
|
+
? normalizePageSize(options.pageSize)
|
|
2720
|
+
: DEFAULT_APPROVAL_HISTORY_TASK_LIMIT)
|
|
2721
|
+
.getMany();
|
|
2722
|
+
return this.attachTaskCandidateSummaries(tasks);
|
|
2723
|
+
}
|
|
2724
|
+
/** How many tasks {@link listApprovalHistoryTasks} pages through. */
|
|
2725
|
+
async countApprovalHistoryTasks(assigneeMemberId, options = {}) {
|
|
2726
|
+
return this.createApprovalHistoryTaskQuery(assigneeMemberId, options.instanceState).getCount();
|
|
2727
|
+
}
|
|
2728
|
+
/** The rows of {@link listApprovalHistoryTasks}, unordered and unpaged. */
|
|
2729
|
+
createApprovalHistoryTaskQuery(assigneeMemberId, instanceState) {
|
|
2730
|
+
const candidateTable = this.readQualifiedTableName(this.taskCandidateRepository);
|
|
2731
|
+
const queryBuilder = this.taskRepository
|
|
2732
|
+
.createQueryBuilder('task')
|
|
2733
|
+
.andWhere(`
|
|
2734
|
+
(
|
|
2735
|
+
(
|
|
2736
|
+
"task"."assignee_member_id" = :historyMemberId
|
|
2737
|
+
AND "task"."status" = :historyTaskStatus
|
|
2738
|
+
)
|
|
2739
|
+
OR EXISTS (
|
|
2740
|
+
SELECT 1
|
|
2741
|
+
FROM ${candidateTable} "historyCandidate"
|
|
2742
|
+
WHERE "historyCandidate"."task_id" = "task"."id"
|
|
2743
|
+
AND "historyCandidate"."member_id" = :historyMemberId
|
|
2744
|
+
AND "historyCandidate"."status" = :historyCandidateStatus
|
|
2745
|
+
)
|
|
2746
|
+
)
|
|
2747
|
+
`, {
|
|
2748
|
+
historyCandidateStatus: workflow_engine_enums_1.TaskCandidateStatusEnum.COMPLETED,
|
|
2749
|
+
historyMemberId: assigneeMemberId,
|
|
2750
|
+
historyTaskStatus: workflow_engine_enums_1.TaskStatusEnum.COMPLETED,
|
|
2623
2751
|
});
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2752
|
+
if (instanceState) {
|
|
2753
|
+
queryBuilder.andWhere(`
|
|
2754
|
+
EXISTS (
|
|
2755
|
+
SELECT 1
|
|
2756
|
+
FROM ${this.readQualifiedTableName(this.approvalInstanceRepository)} "historyInstance"
|
|
2757
|
+
WHERE "historyInstance"."id" = "task"."instance_id"
|
|
2758
|
+
AND "historyInstance"."state" = :historyInstanceState
|
|
2759
|
+
)
|
|
2760
|
+
`, { historyInstanceState: instanceState });
|
|
2761
|
+
}
|
|
2762
|
+
return queryBuilder;
|
|
2763
|
+
}
|
|
2764
|
+
/** When the member acted on a history task; see {@link listApprovalHistoryTasks}. */
|
|
2765
|
+
readApprovalHistoryActedAtExpression() {
|
|
2766
|
+
const candidateTable = this.readQualifiedTableName(this.taskCandidateRepository);
|
|
2767
|
+
return `COALESCE(
|
|
2768
|
+
(
|
|
2769
|
+
SELECT MAX("actedCandidate"."decided_at")
|
|
2770
|
+
FROM ${candidateTable} "actedCandidate"
|
|
2771
|
+
WHERE "actedCandidate"."task_id" = "task"."id"
|
|
2772
|
+
AND "actedCandidate"."member_id" = :historyMemberId
|
|
2773
|
+
AND "actedCandidate"."status" = :historyCandidateStatus
|
|
2774
|
+
),
|
|
2775
|
+
"task"."completed_at",
|
|
2776
|
+
"task"."created_at"
|
|
2777
|
+
)`;
|
|
2639
2778
|
}
|
|
2640
2779
|
async listTaskDecisions(taskId, scope) {
|
|
2641
2780
|
await this.assertTaskReadable(taskId, scope);
|
|
@@ -2793,26 +2932,30 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
2793
2932
|
* case on data the host never gave. It runs before the engine
|
|
2794
2933
|
* transaction, so a failure leaves nothing behind and a slow lookup holds
|
|
2795
2934
|
* no lock.
|
|
2796
|
-
* - The list
|
|
2797
|
-
* failing lookup must not take the whole list
|
|
2935
|
+
* - The list, and the form options check, carry on with `customFields: {}`
|
|
2936
|
+
* and log a warning: one failing lookup must not take the whole list
|
|
2937
|
+
* away. That fails closed: a
|
|
2798
2938
|
* policy that reads `initiator.customFields` then throws on the missing key
|
|
2799
2939
|
* and the list's per-template catch leaves that template out, while a
|
|
2800
2940
|
* template whose policy does not read it is listed as usual. Submitting is
|
|
2801
2941
|
* still judged against the resolver's real answer.
|
|
2802
2942
|
*/
|
|
2803
|
-
async buildInitiatorMetadataSnapshot({ authContext, memberId, submission, }) {
|
|
2943
|
+
async buildInitiatorMetadataSnapshot({ authContext, formOptions = null, memberId, submission, }) {
|
|
2804
2944
|
const defaultSnapshot = await this.readDefaultInitiatorMetadataSnapshot(memberId);
|
|
2805
2945
|
if (!this.initiatorCustomFieldsResolver) {
|
|
2806
2946
|
return defaultSnapshot;
|
|
2807
2947
|
}
|
|
2948
|
+
// The template the resolver is asked about: the one being submitted, or
|
|
2949
|
+
// the one whose options are read. The list asks about none.
|
|
2950
|
+
const template = submission ?? formOptions;
|
|
2808
2951
|
const input = {
|
|
2809
2952
|
authContext,
|
|
2810
2953
|
defaultSnapshot: cloneJsonObject(defaultSnapshot),
|
|
2811
2954
|
formData: submission ? cloneJsonObject(submission.formData) : null,
|
|
2812
2955
|
memberId,
|
|
2813
|
-
purpose:
|
|
2814
|
-
templateId:
|
|
2815
|
-
templateVersionId:
|
|
2956
|
+
purpose: readInitiatorCustomFieldsPurpose({ formOptions, submission }),
|
|
2957
|
+
templateId: template?.templateId ?? null,
|
|
2958
|
+
templateVersionId: template?.templateVersionId ?? null,
|
|
2816
2959
|
};
|
|
2817
2960
|
if (submission) {
|
|
2818
2961
|
return {
|
|
@@ -2827,7 +2970,9 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
2827
2970
|
};
|
|
2828
2971
|
}
|
|
2829
2972
|
catch (error) {
|
|
2830
|
-
this.logger.warn(
|
|
2973
|
+
this.logger.warn(`${formOptions
|
|
2974
|
+
? `Form options for template=${formOptions.templateId}`
|
|
2975
|
+
: 'Launchable templates'} judged with customFields {}: the initiator customFields resolver failed (${error instanceof common_1.HttpException ? error.getStatus() : 'UNEXPECTED'})`);
|
|
2831
2976
|
return defaultSnapshot;
|
|
2832
2977
|
}
|
|
2833
2978
|
}
|
|
@@ -2848,9 +2993,9 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
2848
2993
|
* unhandled.
|
|
2849
2994
|
*/
|
|
2850
2995
|
async resolveInitiatorCustomFields(resolver, input) {
|
|
2851
|
-
const subject = input.purpose === '
|
|
2852
|
-
?
|
|
2853
|
-
: '
|
|
2996
|
+
const subject = input.purpose === 'LAUNCHABLE_TEMPLATES'
|
|
2997
|
+
? 'launchable templates'
|
|
2998
|
+
: `${input.purpose === 'FORM_OPTIONS' ? 'form options ' : ''}template=${input.templateId} version=${input.templateVersionId}`;
|
|
2854
2999
|
const outcome = await settleWithin(new Promise((resolve) => {
|
|
2855
3000
|
resolve(resolver.resolveCustomFields(input));
|
|
2856
3001
|
}), INITIATOR_CUSTOM_FIELDS_RESOLVER_TIMEOUT_MS, (late) => {
|
|
@@ -5635,6 +5780,13 @@ function readDefaultInitiatorMetadataSnapshot(memberId) {
|
|
|
5635
5780
|
function cloneJsonObject(value) {
|
|
5636
5781
|
return JSON.parse(JSON.stringify(value));
|
|
5637
5782
|
}
|
|
5783
|
+
/** Which caller is building an initiator snapshot, as the resolver is told. */
|
|
5784
|
+
function readInitiatorCustomFieldsPurpose({ formOptions, submission, }) {
|
|
5785
|
+
if (submission) {
|
|
5786
|
+
return 'SUBMISSION';
|
|
5787
|
+
}
|
|
5788
|
+
return formOptions ? 'FORM_OPTIONS' : 'LAUNCHABLE_TEMPLATES';
|
|
5789
|
+
}
|
|
5638
5790
|
/**
|
|
5639
5791
|
* What a host's initiator `customFields` resolver returned, as the object the
|
|
5640
5792
|
* snapshot keeps: a plain object that survives JSON, which is also how it is
|
|
@@ -6376,6 +6528,9 @@ function readPageOffset(page, pageSize) {
|
|
|
6376
6528
|
const normalizedPageSize = normalizePageSize(pageSize);
|
|
6377
6529
|
return (normalizePage(page) - 1) * normalizedPageSize;
|
|
6378
6530
|
}
|
|
6531
|
+
function isTaskListPaged(options) {
|
|
6532
|
+
return options.page !== undefined || options.pageSize !== undefined;
|
|
6533
|
+
}
|
|
6379
6534
|
function shouldPaginateList(options) {
|
|
6380
6535
|
return (typeof options.page !== 'undefined' ||
|
|
6381
6536
|
typeof options.pageSize !== 'undefined');
|