@rytass/bpm-core-nestjs-module 0.19.0 → 0.20.1
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 +55 -0
- package/package.json +3 -2
- package/src/index.d.ts +1 -0
- package/src/index.js +1 -0
- package/src/index.js.map +1 -1
- package/src/lib/bpm/bpm-root-options.d.ts +13 -0
- package/src/lib/bpm/bpm-root-options.js.map +1 -1
- package/src/lib/bpm/bpm-root.module.d.ts +12 -0
- package/src/lib/bpm/bpm-root.module.js +5 -0
- package/src/lib/bpm/bpm-root.module.js.map +1 -1
- package/src/lib/condition/adapters/cel-legacy.adapter.d.ts +62 -0
- package/src/lib/condition/adapters/cel-legacy.adapter.js +141 -0
- package/src/lib/condition/adapters/cel-legacy.adapter.js.map +1 -0
- package/src/lib/condition/adapters/cel-v2.adapter.d.ts +76 -0
- package/src/lib/condition/adapters/cel-v2.adapter.js +371 -0
- package/src/lib/condition/adapters/cel-v2.adapter.js.map +1 -0
- package/src/lib/condition/condition.service.d.ts +90 -5
- package/src/lib/condition/condition.service.js +287 -71
- package/src/lib/condition/condition.service.js.map +1 -1
- package/src/lib/condition/expression-ast.d.ts +176 -0
- package/src/lib/condition/expression-ast.js +1254 -0
- package/src/lib/condition/expression-ast.js.map +1 -0
- package/src/lib/condition/expression-budget.d.ts +44 -0
- package/src/lib/condition/expression-budget.js +61 -0
- package/src/lib/condition/expression-budget.js.map +1 -0
- package/src/lib/condition/expression-dialect.d.ts +29 -0
- package/src/lib/condition/expression-dialect.js +44 -0
- package/src/lib/condition/expression-dialect.js.map +1 -0
- package/src/lib/condition/expression-differential.corpus.d.ts +34 -0
- package/src/lib/condition/expression-differential.corpus.js +995 -0
- package/src/lib/condition/expression-differential.corpus.js.map +1 -0
- package/src/lib/condition/expression-differential.d.ts +33 -0
- package/src/lib/condition/expression-differential.js +48 -0
- package/src/lib/condition/expression-differential.js.map +1 -0
- package/src/lib/condition/expression-engine-failure.d.ts +15 -0
- package/src/lib/condition/expression-engine-failure.js +22 -0
- package/src/lib/condition/expression-engine-failure.js.map +1 -0
- package/src/lib/condition/expression-errors.d.ts +40 -0
- package/src/lib/condition/expression-errors.js +121 -0
- package/src/lib/condition/expression-errors.js.map +1 -0
- package/src/lib/condition/expression-function-invocation.d.ts +50 -0
- package/src/lib/condition/expression-function-invocation.js +290 -0
- package/src/lib/condition/expression-function-invocation.js.map +1 -0
- package/src/lib/condition/expression-result.walker.d.ts +18 -0
- package/src/lib/condition/expression-result.walker.js +152 -0
- package/src/lib/condition/expression-result.walker.js.map +1 -0
- package/src/lib/condition/expression-runtime.d.ts +70 -0
- package/src/lib/condition/expression-runtime.js +134 -0
- package/src/lib/condition/expression-runtime.js.map +1 -0
- package/src/lib/condition/index.d.ts +4 -0
- package/src/lib/condition/index.js +8 -0
- package/src/lib/condition/index.js.map +1 -1
- package/src/lib/delegation/delegation-rule.entity.d.ts +7 -0
- package/src/lib/delegation/delegation-rule.entity.js +10 -0
- package/src/lib/delegation/delegation-rule.entity.js.map +1 -1
- package/src/lib/delegation/delegation.service.d.ts +7 -1
- package/src/lib/delegation/delegation.service.js +22 -5
- package/src/lib/delegation/delegation.service.js.map +1 -1
- package/src/lib/delegation/dto/delegation-rule.input.d.ts +2 -0
- package/src/lib/delegation/dto/delegation-rule.input.js +10 -0
- package/src/lib/delegation/dto/delegation-rule.input.js.map +1 -1
- package/src/lib/expression-function/expression-function.built-ins.d.ts +9 -0
- package/src/lib/expression-function/expression-function.built-ins.js +117 -0
- package/src/lib/expression-function/expression-function.built-ins.js.map +1 -0
- package/src/lib/expression-function/expression-function.module.d.ts +19 -0
- package/src/lib/expression-function/expression-function.module.js +34 -0
- package/src/lib/expression-function/expression-function.module.js.map +1 -0
- package/src/lib/expression-function/expression-function.provider.d.ts +17 -0
- package/src/lib/expression-function/expression-function.provider.js +39 -0
- package/src/lib/expression-function/expression-function.provider.js.map +1 -0
- package/src/lib/expression-function/expression-function.registry.d.ts +36 -0
- package/src/lib/expression-function/expression-function.registry.js +231 -0
- package/src/lib/expression-function/expression-function.registry.js.map +1 -0
- package/src/lib/expression-function/expression-function.types.d.ts +30 -0
- package/src/lib/expression-function/expression-function.types.js +8 -0
- package/src/lib/expression-function/expression-function.types.js.map +1 -0
- package/src/lib/expression-function/index.d.ts +5 -0
- package/src/lib/expression-function/index.js +9 -0
- package/src/lib/expression-function/index.js.map +1 -0
- package/src/lib/form/form-schema.validator.js +53 -6
- package/src/lib/form/form-schema.validator.js.map +1 -1
- package/src/lib/form-data-source/form-data-source-value-resolver.service.d.ts +12 -1
- package/src/lib/form-data-source/form-data-source-value-resolver.service.js +45 -26
- package/src/lib/form-data-source/form-data-source-value-resolver.service.js.map +1 -1
- package/src/lib/form-data-source/form-data-source.expression.d.ts +95 -0
- package/src/lib/form-data-source/form-data-source.expression.js +267 -0
- package/src/lib/form-data-source/form-data-source.expression.js.map +1 -0
- package/src/lib/form-data-source/form-data-source.module.js +4 -0
- package/src/lib/form-data-source/form-data-source.module.js.map +1 -1
- package/src/lib/form-data-source/form-data-source.service.d.ts +22 -3
- package/src/lib/form-data-source/form-data-source.service.js +78 -18
- package/src/lib/form-data-source/form-data-source.service.js.map +1 -1
- package/src/lib/form-data-source/form-data-source.validation.d.ts +47 -4
- package/src/lib/form-data-source/form-data-source.validation.js +151 -23
- package/src/lib/form-data-source/form-data-source.validation.js.map +1 -1
- package/src/lib/migrations/0000000030000-delegation-scope-condition-dialect.d.ts +22 -0
- package/src/lib/migrations/0000000030000-delegation-scope-condition-dialect.js +76 -0
- package/src/lib/migrations/0000000030000-delegation-scope-condition-dialect.js.map +1 -0
- package/src/lib/migrations/index.d.ts +1 -0
- package/src/lib/migrations/index.js +3 -0
- package/src/lib/migrations/index.js.map +1 -1
- package/src/lib/notification/notification.service.d.ts +1 -1
- package/src/lib/notification/notification.service.js +7 -3
- package/src/lib/notification/notification.service.js.map +1 -1
- package/src/lib/template/dto/expression-designer.dto.d.ts +107 -0
- package/src/lib/template/dto/expression-designer.dto.js +415 -0
- package/src/lib/template/dto/expression-designer.dto.js.map +1 -0
- package/src/lib/template/expression-designer.queries.d.ts +16 -0
- package/src/lib/template/expression-designer.queries.js +66 -0
- package/src/lib/template/expression-designer.queries.js.map +1 -0
- package/src/lib/template/expression-designer.service.d.ts +72 -0
- package/src/lib/template/expression-designer.service.js +543 -0
- package/src/lib/template/expression-designer.service.js.map +1 -0
- package/src/lib/template/index.d.ts +2 -0
- package/src/lib/template/index.js +2 -0
- package/src/lib/template/index.js.map +1 -1
- package/src/lib/template/template.module.js +4 -0
- package/src/lib/template/template.module.js.map +1 -1
- package/src/lib/template/template.service.d.ts +37 -1
- package/src/lib/template/template.service.js +317 -29
- package/src/lib/template/template.service.js.map +1 -1
- package/src/lib/template/workflow-definition.validator.d.ts +7 -0
- package/src/lib/template/workflow-definition.validator.js +26 -2
- package/src/lib/template/workflow-definition.validator.js.map +1 -1
- package/src/lib/workflow-engine/time-budget.d.ts +13 -0
- package/src/lib/workflow-engine/time-budget.js +58 -0
- package/src/lib/workflow-engine/time-budget.js.map +1 -0
- package/src/lib/workflow-engine/workflow-condition-evaluator.js +12 -12
- package/src/lib/workflow-engine/workflow-condition-evaluator.js.map +1 -1
- package/src/lib/workflow-engine/workflow-engine.service.d.ts +24 -0
- package/src/lib/workflow-engine/workflow-engine.service.js +311 -88
- package/src/lib/workflow-engine/workflow-engine.service.js.map +1 -1
- package/src/lib/workflow-engine/workflow-form-value.d.ts +11 -0
- package/src/lib/workflow-engine/workflow-form-value.js +33 -0
- package/src/lib/workflow-engine/workflow-form-value.js.map +1 -0
- package/src/lib/workflow-engine/workflow-service-task-dispatcher.token.js +4 -0
- package/src/lib/workflow-engine/workflow-service-task-dispatcher.token.js.map +1 -1
- package/src/lib/workflow-webhook/workflow-webhook-delivery.service.d.ts +3 -1
- package/src/lib/workflow-webhook/workflow-webhook-delivery.service.js +11 -2
- package/src/lib/workflow-webhook/workflow-webhook-delivery.service.js.map +1 -1
- package/src/lib/workflow-webhook/workflow-webhook-enqueue.d.ts +8 -1
- package/src/lib/workflow-webhook/workflow-webhook-enqueue.js +70 -27
- package/src/lib/workflow-webhook/workflow-webhook-enqueue.js.map +1 -1
- package/src/lib/workflow-webhook/workflow-webhook-expression.d.ts +57 -0
- package/src/lib/workflow-webhook/workflow-webhook-expression.js +123 -0
- package/src/lib/workflow-webhook/workflow-webhook-expression.js.map +1 -0
- package/src/lib/workflow-webhook/workflow-webhook.module.js +4 -0
- package/src/lib/workflow-webhook/workflow-webhook.module.js.map +1 -1
- package/src/lib/workflow-webhook/workflow-webhook.validator.d.ts +7 -0
- package/src/lib/workflow-webhook/workflow-webhook.validator.js +29 -6
- package/src/lib/workflow-webhook/workflow-webhook.validator.js.map +1 -1
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { ModuleRef } from '@nestjs/core';
|
|
2
|
+
import { ExpressionUsagePoint } from '@rytass/bpm-core-shared';
|
|
3
|
+
import { WorkflowDefinition } from '@rytass/bpm-core-shared/workflow';
|
|
2
4
|
import { EntityManager, Repository } from 'typeorm';
|
|
3
5
|
import { ConditionService } from '../condition/condition.service';
|
|
6
|
+
import { BPMExpressionFunctionRegistry } from '../expression-function/expression-function.types';
|
|
4
7
|
import { FormDefinitionVersionEntity } from '../form/form-definition-version.entity';
|
|
5
8
|
import { FormService } from '../form/form.service';
|
|
6
9
|
import { ComposeApprovalTemplateWithFormInput } from './dto/compose-approval-template.input';
|
|
@@ -32,8 +35,9 @@ export declare class TemplateService {
|
|
|
32
35
|
private readonly conditionService;
|
|
33
36
|
private readonly formService;
|
|
34
37
|
private readonly moduleRef;
|
|
38
|
+
private readonly expressionFunctionRegistry?;
|
|
35
39
|
private readonly logger;
|
|
36
|
-
constructor(templateRepository: Repository<ApprovalTemplateEntity>, templateCategoryRepository: Repository<ApprovalTemplateCategoryEntity>, templateVersionRepository: Repository<ApprovalTemplateVersionEntity>, formDefinitionVersionRepository: Repository<FormDefinitionVersionEntity>, conditionService: ConditionService, formService: FormService, moduleRef: ModuleRef);
|
|
40
|
+
constructor(templateRepository: Repository<ApprovalTemplateEntity>, templateCategoryRepository: Repository<ApprovalTemplateCategoryEntity>, templateVersionRepository: Repository<ApprovalTemplateVersionEntity>, formDefinitionVersionRepository: Repository<FormDefinitionVersionEntity>, conditionService: ConditionService, formService: FormService, moduleRef: ModuleRef, expressionFunctionRegistry?: (BPMExpressionFunctionRegistry | null) | undefined);
|
|
37
41
|
createApprovalTemplate(input: CreateApprovalTemplateInput, manager?: EntityManager): Promise<ApprovalTemplateEntity>;
|
|
38
42
|
private createApprovalTemplateWithManager;
|
|
39
43
|
/**
|
|
@@ -87,6 +91,21 @@ export declare class TemplateService {
|
|
|
87
91
|
private notifyTemplateObserver;
|
|
88
92
|
private readHostObserver;
|
|
89
93
|
private validatePublishableVersion;
|
|
94
|
+
/** The cel-legacy publish lint, unchanged (docs/24 §1). */
|
|
95
|
+
private lintCelLegacyExpressions;
|
|
96
|
+
/**
|
|
97
|
+
* The cel-v2 publish lint (ADR 22 §3.4, §5): every expression on its AST
|
|
98
|
+
* with its use point's roots, the bound form's fields and types and the
|
|
99
|
+
* function registry, plus the `SET_FORM_FIELD` write targets. Warnings do
|
|
100
|
+
* not refuse a publish; the designer reads them through
|
|
101
|
+
* `ConditionService.lintExpressionIssues`.
|
|
102
|
+
*/
|
|
103
|
+
private lintCelV2Expressions;
|
|
104
|
+
/**
|
|
105
|
+
* The functions a version calls that the registry no longer has, as lint
|
|
106
|
+
* lines. Only cel-v2 can call registered functions at all.
|
|
107
|
+
*/
|
|
108
|
+
private lintRegisteredFunctions;
|
|
90
109
|
/**
|
|
91
110
|
* Publish rules that need the webhook endpoint catalog (ADR 18 §4).
|
|
92
111
|
*
|
|
@@ -110,4 +129,21 @@ export declare class TemplateService {
|
|
|
110
129
|
private readNextVersionNumber;
|
|
111
130
|
private parseWorkflowDefinitionOrThrow;
|
|
112
131
|
}
|
|
132
|
+
export interface ExpressionSource {
|
|
133
|
+
readonly expression: string | null | undefined;
|
|
134
|
+
readonly label: string;
|
|
135
|
+
}
|
|
136
|
+
export interface UsagePointExpression extends ExpressionSource {
|
|
137
|
+
/** The top-level field a `SET_FORM_FIELD` value is written to. */
|
|
138
|
+
readonly targetFieldKey: string | null;
|
|
139
|
+
readonly usagePoint: ExpressionUsagePoint;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Every expression of a version with its use point (docs/24 §2.3). Unlike
|
|
143
|
+
* the cel-legacy readers this includes the NOTIFY recipients expression,
|
|
144
|
+
* which is evaluated like an approver expression. A draft is not
|
|
145
|
+
* shape-checked, so every read is guarded. Also read by the designer's
|
|
146
|
+
* expression analysis and dialect upgrade check (Phase 23 P4).
|
|
147
|
+
*/
|
|
148
|
+
export declare function readUsagePointExpressions(definition: WorkflowDefinition, initiatorPolicyCel: string | null): readonly UsagePointExpression[];
|
|
113
149
|
export {};
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
var TemplateService_1;
|
|
3
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
4
|
exports.TemplateService = void 0;
|
|
5
|
+
exports.readUsagePointExpressions = readUsagePointExpressions;
|
|
5
6
|
const tslib_1 = require("tslib");
|
|
6
7
|
const common_1 = require("@nestjs/common");
|
|
7
8
|
const core_1 = require("@nestjs/core");
|
|
@@ -11,6 +12,9 @@ const form_1 = require("@rytass/bpm-core-shared/form");
|
|
|
11
12
|
const workflow_graph_1 = require("@rytass/bpm-core-shared/workflow-graph");
|
|
12
13
|
const typeorm_2 = require("typeorm");
|
|
13
14
|
const condition_service_1 = require("../condition/condition.service");
|
|
15
|
+
const expression_dialect_1 = require("../condition/expression-dialect");
|
|
16
|
+
const expression_ast_1 = require("../condition/expression-ast");
|
|
17
|
+
const expression_function_types_1 = require("../expression-function/expression-function.types");
|
|
14
18
|
const form_table_reference_1 = require("../form/form-table-reference");
|
|
15
19
|
const form_definition_entity_1 = require("../form/form-definition.entity");
|
|
16
20
|
const form_definition_version_entity_1 = require("../form/form-definition-version.entity");
|
|
@@ -43,7 +47,7 @@ const WORKFLOW_EXPRESSION_ROOT_IDENTIFIERS = [
|
|
|
43
47
|
'instance',
|
|
44
48
|
];
|
|
45
49
|
let TemplateService = TemplateService_1 = class TemplateService {
|
|
46
|
-
constructor(templateRepository, templateCategoryRepository, templateVersionRepository, formDefinitionVersionRepository, conditionService, formService, moduleRef) {
|
|
50
|
+
constructor(templateRepository, templateCategoryRepository, templateVersionRepository, formDefinitionVersionRepository, conditionService, formService, moduleRef, expressionFunctionRegistry) {
|
|
47
51
|
this.templateRepository = templateRepository;
|
|
48
52
|
this.templateCategoryRepository = templateCategoryRepository;
|
|
49
53
|
this.templateVersionRepository = templateVersionRepository;
|
|
@@ -51,6 +55,7 @@ let TemplateService = TemplateService_1 = class TemplateService {
|
|
|
51
55
|
this.conditionService = conditionService;
|
|
52
56
|
this.formService = formService;
|
|
53
57
|
this.moduleRef = moduleRef;
|
|
58
|
+
this.expressionFunctionRegistry = expressionFunctionRegistry;
|
|
54
59
|
this.logger = new common_1.Logger(TemplateService_1.name);
|
|
55
60
|
}
|
|
56
61
|
async createApprovalTemplate(input, manager) {
|
|
@@ -89,7 +94,7 @@ let TemplateService = TemplateService_1 = class TemplateService {
|
|
|
89
94
|
status: template_enums_1.ApprovalTemplateVersionStatusEnum.DRAFT,
|
|
90
95
|
templateId: template.id,
|
|
91
96
|
version: 1,
|
|
92
|
-
workflowDefinition: workflow_definition_validator_1.
|
|
97
|
+
workflowDefinition: workflow_definition_validator_1.NEW_TEMPLATE_WORKFLOW_DEFINITION,
|
|
93
98
|
}));
|
|
94
99
|
// As in `updateApprovalTemplate`: `categoryId` is no longer written by
|
|
95
100
|
// `save()`, so the in-memory entity would come back without it even though
|
|
@@ -477,6 +482,14 @@ let TemplateService = TemplateService_1 = class TemplateService {
|
|
|
477
482
|
if (target.status === template_enums_1.ApprovalTemplateVersionStatusEnum.DRAFT) {
|
|
478
483
|
throw new common_1.ConflictException('Draft approval template versions cannot be rollback targets');
|
|
479
484
|
}
|
|
485
|
+
// A rollback is a recovery path: the version passed the lint of its day
|
|
486
|
+
// and is not linted again, since a rule tightened since could refuse it
|
|
487
|
+
// (ADR 22 §5, docs/24 §1). The one check is that every function it calls
|
|
488
|
+
// is still registered; without it every case would fail on evaluation.
|
|
489
|
+
const unregisteredFunctionErrors = this.lintRegisteredFunctions(target.workflowDefinition, target.initiatorPolicyCel);
|
|
490
|
+
if (unregisteredFunctionErrors.length) {
|
|
491
|
+
throw new common_1.BadRequestException(unregisteredFunctionErrors.join('; '));
|
|
492
|
+
}
|
|
480
493
|
const rolledBack = await this.templateRepository.manager.transaction(async (manager) => {
|
|
481
494
|
const templateRepository = manager.getRepository(approval_template_entity_1.ApprovalTemplateEntity);
|
|
482
495
|
const versionRepository = manager.getRepository(approval_template_version_entity_1.ApprovalTemplateVersionEntity);
|
|
@@ -562,24 +575,22 @@ let TemplateService = TemplateService_1 = class TemplateService {
|
|
|
562
575
|
throw new common_1.BadRequestException('Approval template can only bind a published form definition version');
|
|
563
576
|
}
|
|
564
577
|
const workflowResult = (0, workflow_definition_validator_1.lintWorkflowDefinition)(version.workflowDefinition);
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
];
|
|
582
|
-
const tableReferenceErrors = lintWorkflowTableReferences(version.workflowDefinition, version.initiatorPolicyCel, formVersion.schema);
|
|
578
|
+
// An unknown dialect is refused by `lintWorkflowDefinition`; its
|
|
579
|
+
// expressions are not parsed with a guessed engine.
|
|
580
|
+
const dialect = readPublishDialect(version.workflowDefinition);
|
|
581
|
+
const fieldReader = createExpressionFieldReader(dialect ?? expression_dialect_1.DEFAULT_EXPRESSION_DIALECT);
|
|
582
|
+
const conditionErrors = dialect === 'cel-v2'
|
|
583
|
+
? this.lintCelV2Expressions(version, formVersion.schema)
|
|
584
|
+
: dialect === null
|
|
585
|
+
? []
|
|
586
|
+
: this.lintCelLegacyExpressions(version);
|
|
587
|
+
// ADR 22 §5.5 names no dialect, and a write path is not CEL: its reserved
|
|
588
|
+
// segments are refused in either. That it names a declared field is
|
|
589
|
+
// checked for cel-v2 only (`lintCelV2Expressions`).
|
|
590
|
+
const setFormFieldPathErrors = dialect === null
|
|
591
|
+
? []
|
|
592
|
+
: lintSetFormFieldReservedSegments(version.workflowDefinition);
|
|
593
|
+
const tableReferenceErrors = lintWorkflowTableReferences(version.workflowDefinition, version.initiatorPolicyCel, formVersion.schema, fieldReader);
|
|
583
594
|
const memberReferenceErrors = lintWorkflowMemberReferences(version.workflowDefinition, formVersion.schema);
|
|
584
595
|
const orgUnitReferenceErrors = lintWorkflowOrgUnitReferences(version.workflowDefinition, formVersion.schema);
|
|
585
596
|
// The graph checks read a sound graph (the publish lint refuses cycles
|
|
@@ -589,11 +600,12 @@ let TemplateService = TemplateService_1 = class TemplateService {
|
|
|
589
600
|
? lintWorkflowNodeActorSources(version.workflowDefinition)
|
|
590
601
|
: [];
|
|
591
602
|
const approverFilledFieldErrors = (0, bpm_core_shared_1.readFormFieldAccessIssues)(version.workflowDefinition, formVersion.schema).map((issue) => issue.message);
|
|
592
|
-
const approverFilledReadErrors = lintApproverFilledFieldReads(version.workflowDefinition);
|
|
603
|
+
const approverFilledReadErrors = lintApproverFilledFieldReads(version.workflowDefinition, fieldReader);
|
|
593
604
|
const webhookErrors = await this.lintWorkflowWebhooks(version.workflowDefinition, formVersion.schema);
|
|
594
605
|
const errors = [
|
|
595
606
|
...workflowResult.errors,
|
|
596
607
|
...conditionErrors,
|
|
608
|
+
...setFormFieldPathErrors,
|
|
597
609
|
...tableReferenceErrors,
|
|
598
610
|
...memberReferenceErrors,
|
|
599
611
|
...orgUnitReferenceErrors,
|
|
@@ -606,6 +618,69 @@ let TemplateService = TemplateService_1 = class TemplateService {
|
|
|
606
618
|
throw new common_1.BadRequestException(errors.join('; '));
|
|
607
619
|
}
|
|
608
620
|
}
|
|
621
|
+
/** The cel-legacy publish lint, unchanged (docs/24 §1). */
|
|
622
|
+
lintCelLegacyExpressions(version) {
|
|
623
|
+
return [
|
|
624
|
+
...this.conditionService.lintExpressions([
|
|
625
|
+
{
|
|
626
|
+
expression: version.initiatorPolicyCel,
|
|
627
|
+
label: 'initiatorPolicyCel',
|
|
628
|
+
},
|
|
629
|
+
], { allowedRootIdentifiers: INITIATOR_POLICY_ROOT_IDENTIFIERS }),
|
|
630
|
+
...this.conditionService.lintExpressions(readNodeConditionExpressions(version.workflowDefinition), { allowedRootIdentifiers: WORKFLOW_EXPRESSION_ROOT_IDENTIFIERS }),
|
|
631
|
+
// Edge conditions also see the decision just made on the node they
|
|
632
|
+
// leave (workflow-engine `evaluateWorkflowEdge` and its dry-run twin).
|
|
633
|
+
...this.conditionService.lintExpressions(readEdgeConditionExpressions(version.workflowDefinition), {
|
|
634
|
+
allowedRootIdentifiers: [
|
|
635
|
+
...WORKFLOW_EXPRESSION_ROOT_IDENTIFIERS,
|
|
636
|
+
'lastDecision',
|
|
637
|
+
],
|
|
638
|
+
}),
|
|
639
|
+
];
|
|
640
|
+
}
|
|
641
|
+
/**
|
|
642
|
+
* The cel-v2 publish lint (ADR 22 §3.4, §5): every expression on its AST
|
|
643
|
+
* with its use point's roots, the bound form's fields and types and the
|
|
644
|
+
* function registry, plus the `SET_FORM_FIELD` write targets. Warnings do
|
|
645
|
+
* not refuse a publish; the designer reads them through
|
|
646
|
+
* `ConditionService.lintExpressionIssues`.
|
|
647
|
+
*/
|
|
648
|
+
lintCelV2Expressions(version, formSchema) {
|
|
649
|
+
const fields = new Map((formSchema?.fields ?? []).map((entry) => [entry.fieldKey, entry]));
|
|
650
|
+
return [
|
|
651
|
+
...readUsagePointExpressions(version.workflowDefinition, version.initiatorPolicyCel).flatMap(({ expression, label, targetFieldKey, usagePoint }) => this.conditionService.lintExpression(expression, label, {
|
|
652
|
+
dialect: 'cel-v2',
|
|
653
|
+
formSchema,
|
|
654
|
+
functionRegistry: this.expressionFunctionRegistry ?? null,
|
|
655
|
+
targetField: targetFieldKey === null ? null : (fields.get(targetFieldKey) ?? null),
|
|
656
|
+
usagePoint,
|
|
657
|
+
})),
|
|
658
|
+
...lintSetFormFieldDeclaredFields(version.workflowDefinition, formSchema),
|
|
659
|
+
];
|
|
660
|
+
}
|
|
661
|
+
/**
|
|
662
|
+
* The functions a version calls that the registry no longer has, as lint
|
|
663
|
+
* lines. Only cel-v2 can call registered functions at all.
|
|
664
|
+
*/
|
|
665
|
+
lintRegisteredFunctions(definition, initiatorPolicyCel) {
|
|
666
|
+
if (readPublishDialect(definition) !== 'cel-v2') {
|
|
667
|
+
return [];
|
|
668
|
+
}
|
|
669
|
+
const registry = this.expressionFunctionRegistry ?? null;
|
|
670
|
+
return readUsagePointExpressions(definition, initiatorPolicyCel).flatMap(({ expression, label }) => {
|
|
671
|
+
const names = typeof expression === 'string'
|
|
672
|
+
? ((0, expression_ast_1.readExpressionHostFunctionNames)(expression.trim(), 'cel-v2') ?? [])
|
|
673
|
+
: [];
|
|
674
|
+
// Same name rule and registry isolation as the publish lint.
|
|
675
|
+
const missing = (0, expression_ast_1.readUnavailableExpressionFunctions)(names, registry);
|
|
676
|
+
return missing.map((name) => (0, bpm_core_shared_1.formatExpressionLintIssue)({
|
|
677
|
+
code: 'EXPR_FUNCTION_NOT_REGISTERED',
|
|
678
|
+
label,
|
|
679
|
+
severity: 'ERROR',
|
|
680
|
+
subject: name,
|
|
681
|
+
}));
|
|
682
|
+
});
|
|
683
|
+
}
|
|
609
684
|
/**
|
|
610
685
|
* Publish rules that need the webhook endpoint catalog (ADR 18 §4).
|
|
611
686
|
*
|
|
@@ -624,6 +699,9 @@ let TemplateService = TemplateService_1 = class TemplateService {
|
|
|
624
699
|
}
|
|
625
700
|
})();
|
|
626
701
|
return (0, workflow_webhook_validator_1.lintWorkflowWebhookTargets)({
|
|
702
|
+
// The injected service: the NOTIFY `EXPRESSION` lint sees the same
|
|
703
|
+
// function registry the engine evaluates with (docs/23 P4/P5 交接).
|
|
704
|
+
conditionService: this.conditionService,
|
|
627
705
|
definition,
|
|
628
706
|
formSchema,
|
|
629
707
|
hasEndpointSources: Boolean(webhookService?.hasEndpointSources()),
|
|
@@ -737,13 +815,15 @@ exports.TemplateService = TemplateService = TemplateService_1 = tslib_1.__decora
|
|
|
737
815
|
tslib_1.__param(1, (0, typeorm_1.InjectRepository)(approval_template_category_entity_1.ApprovalTemplateCategoryEntity)),
|
|
738
816
|
tslib_1.__param(2, (0, typeorm_1.InjectRepository)(approval_template_version_entity_1.ApprovalTemplateVersionEntity)),
|
|
739
817
|
tslib_1.__param(3, (0, typeorm_1.InjectRepository)(form_definition_version_entity_1.FormDefinitionVersionEntity)),
|
|
818
|
+
tslib_1.__param(7, (0, common_1.Optional)()),
|
|
819
|
+
tslib_1.__param(7, (0, common_1.Inject)(expression_function_types_1.BPM_EXPRESSION_FUNCTION_REGISTRY)),
|
|
740
820
|
tslib_1.__metadata("design:paramtypes", [typeorm_2.Repository,
|
|
741
821
|
typeorm_2.Repository,
|
|
742
822
|
typeorm_2.Repository,
|
|
743
823
|
typeorm_2.Repository,
|
|
744
824
|
condition_service_1.ConditionService,
|
|
745
825
|
form_service_1.FormService,
|
|
746
|
-
core_1.ModuleRef])
|
|
826
|
+
core_1.ModuleRef, Object])
|
|
747
827
|
], TemplateService);
|
|
748
828
|
function parseOptionalJsonObject(value) {
|
|
749
829
|
if (!value) {
|
|
@@ -835,14 +915,14 @@ function createApprovalTemplateCategoryWhere(options) {
|
|
|
835
915
|
* a `conditionFieldKey` of `items.qty` simply resolves to `undefined` at
|
|
836
916
|
* runtime. Both would fail silently rather than loudly.
|
|
837
917
|
*/
|
|
838
|
-
function lintWorkflowTableReferences(definition, initiatorPolicyCel, schema) {
|
|
918
|
+
function lintWorkflowTableReferences(definition, initiatorPolicyCel, schema, fieldReader) {
|
|
839
919
|
const tableFieldKeys = (0, form_table_reference_1.readTableFieldKeys)(schema);
|
|
840
920
|
if (!tableFieldKeys.size) {
|
|
841
921
|
return [];
|
|
842
922
|
}
|
|
843
923
|
const expressionErrors = readConditionExpressions(definition, initiatorPolicyCel).flatMap(({ expression, label }) => typeof expression === 'string'
|
|
844
924
|
? [...tableFieldKeys]
|
|
845
|
-
.filter((tableKey) =>
|
|
925
|
+
.filter((tableKey) => fieldReader.readsTableInternals(expression, tableKey))
|
|
846
926
|
.map((tableKey) => `${label} must not reference table field internals: ${tableKey}`)
|
|
847
927
|
: []);
|
|
848
928
|
const structuredErrors = definition.edges
|
|
@@ -997,7 +1077,7 @@ function lintWorkflowNodeActorSources(definition) {
|
|
|
997
1077
|
* The definition may hold a cycle (the cycle lint reports it in the same
|
|
998
1078
|
* pass), which the visited set in `readReachableWorkflowNodeIds` copes with.
|
|
999
1079
|
*/
|
|
1000
|
-
function lintApproverFilledFieldReads(definition) {
|
|
1080
|
+
function lintApproverFilledFieldReads(definition, fieldReader) {
|
|
1001
1081
|
const fieldKeys = [...(0, bpm_core_shared_1.readApproverFilledFieldKeys)(definition)];
|
|
1002
1082
|
if (!fieldKeys.length) {
|
|
1003
1083
|
return [];
|
|
@@ -1026,11 +1106,11 @@ function lintApproverFilledFieldReads(definition) {
|
|
|
1026
1106
|
const nodeLabels = nodeSources
|
|
1027
1107
|
.filter(({ expression, nodeId }) => nodeIdsReadingEarly.has(nodeId) &&
|
|
1028
1108
|
typeof expression === 'string' &&
|
|
1029
|
-
|
|
1109
|
+
fieldReader.readsField(expression, fieldKey))
|
|
1030
1110
|
.map(({ label }) => label);
|
|
1031
1111
|
const edgeLabels = definition.edges
|
|
1032
1112
|
.filter((edge) => earlyNodeIds.has(edge.source))
|
|
1033
|
-
.flatMap((edge) => readEdgeFieldReadLabels(edge, fieldKey));
|
|
1113
|
+
.flatMap((edge) => readEdgeFieldReadLabels(edge, fieldKey, fieldReader));
|
|
1034
1114
|
return [...nodeLabels, ...edgeLabels].map((label) => workflow_graph_1.WORKFLOW_LINT_MESSAGES.approverFilledFieldReadEarly(label, fieldKey));
|
|
1035
1115
|
});
|
|
1036
1116
|
}
|
|
@@ -1040,12 +1120,13 @@ function hasEntryCondition(node) {
|
|
|
1040
1120
|
}
|
|
1041
1121
|
// The label of the one line an edge earns for `fieldKey`: the CEL condition
|
|
1042
1122
|
// wins over the structured condition, so an edge carrying both reads once.
|
|
1043
|
-
function readEdgeFieldReadLabels(edge, fieldKey) {
|
|
1123
|
+
function readEdgeFieldReadLabels(edge, fieldKey, fieldReader) {
|
|
1044
1124
|
// A draft's edge may carry no `data` at all.
|
|
1045
1125
|
const data = edge.data;
|
|
1046
1126
|
const condition = data?.condition;
|
|
1047
1127
|
const conditionFieldKey = data?.conditionFieldKey;
|
|
1048
|
-
if (typeof condition === 'string' &&
|
|
1128
|
+
if (typeof condition === 'string' &&
|
|
1129
|
+
fieldReader.readsField(condition, fieldKey)) {
|
|
1049
1130
|
return [workflow_graph_1.WORKFLOW_EXPRESSION_LABELS.edgeCondition(edge.id)];
|
|
1050
1131
|
}
|
|
1051
1132
|
return typeof conditionFieldKey === 'string' &&
|
|
@@ -1143,6 +1224,213 @@ function readNodeExpressions(node) {
|
|
|
1143
1224
|
}
|
|
1144
1225
|
return [];
|
|
1145
1226
|
}
|
|
1227
|
+
/**
|
|
1228
|
+
* The dialect a definition is published with: the default when it names
|
|
1229
|
+
* none, `null` when it names one this version does not know (refused by
|
|
1230
|
+
* `lintWorkflowDefinition`).
|
|
1231
|
+
*/
|
|
1232
|
+
function readPublishDialect(definition) {
|
|
1233
|
+
const value = definition?.expressionDialect;
|
|
1234
|
+
if (value === undefined || value === null) {
|
|
1235
|
+
return expression_dialect_1.DEFAULT_EXPRESSION_DIALECT;
|
|
1236
|
+
}
|
|
1237
|
+
return (0, expression_dialect_1.isExpressionDialect)(value) ? value : null;
|
|
1238
|
+
}
|
|
1239
|
+
const TEXTUAL_FIELD_READER = {
|
|
1240
|
+
readsField: form_table_reference_1.referencesFormField,
|
|
1241
|
+
readsTableInternals: form_table_reference_1.referencesTableInternals,
|
|
1242
|
+
};
|
|
1243
|
+
function createExpressionFieldReader(dialect) {
|
|
1244
|
+
if (dialect !== 'cel-v2') {
|
|
1245
|
+
return TEXTUAL_FIELD_READER;
|
|
1246
|
+
}
|
|
1247
|
+
const cache = new Map();
|
|
1248
|
+
const read = (expression) => {
|
|
1249
|
+
if (!cache.has(expression)) {
|
|
1250
|
+
const result = (0, expression_ast_1.readExpressionReferences)(expression.trim(), 'cel-v2');
|
|
1251
|
+
cache.set(expression, result.status === 'OK' ? result.references : null);
|
|
1252
|
+
}
|
|
1253
|
+
return cache.get(expression) ?? null;
|
|
1254
|
+
};
|
|
1255
|
+
return {
|
|
1256
|
+
readsField: (expression, fieldKey) => {
|
|
1257
|
+
const references = read(expression);
|
|
1258
|
+
return references
|
|
1259
|
+
? (0, expression_ast_1.readsFormField)(references, fieldKey)
|
|
1260
|
+
: (0, form_table_reference_1.referencesFormField)(expression, fieldKey);
|
|
1261
|
+
},
|
|
1262
|
+
readsTableInternals: (expression, tableKey) => {
|
|
1263
|
+
const references = read(expression);
|
|
1264
|
+
return references
|
|
1265
|
+
? (0, expression_ast_1.readsTableInternals)(references, tableKey)
|
|
1266
|
+
: (0, form_table_reference_1.referencesTableInternals)(expression, tableKey);
|
|
1267
|
+
},
|
|
1268
|
+
};
|
|
1269
|
+
}
|
|
1270
|
+
/**
|
|
1271
|
+
* Every expression of a version with its use point (docs/24 §2.3). Unlike
|
|
1272
|
+
* the cel-legacy readers this includes the NOTIFY recipients expression,
|
|
1273
|
+
* which is evaluated like an approver expression. A draft is not
|
|
1274
|
+
* shape-checked, so every read is guarded. Also read by the designer's
|
|
1275
|
+
* expression analysis and dialect upgrade check (Phase 23 P4).
|
|
1276
|
+
*/
|
|
1277
|
+
function readUsagePointExpressions(definition, initiatorPolicyCel) {
|
|
1278
|
+
const nodes = Array.isArray(definition?.nodes)
|
|
1279
|
+
? definition.nodes
|
|
1280
|
+
: [];
|
|
1281
|
+
const edges = Array.isArray(definition?.edges)
|
|
1282
|
+
? definition.edges
|
|
1283
|
+
: [];
|
|
1284
|
+
const entry = (expression, label, usagePoint, targetFieldKey = null) => ({ expression, label, targetFieldKey, usagePoint });
|
|
1285
|
+
return [
|
|
1286
|
+
entry(initiatorPolicyCel, 'initiatorPolicyCel', 'INITIATOR_POLICY'),
|
|
1287
|
+
...nodes.flatMap((node) => {
|
|
1288
|
+
const data = node?.data;
|
|
1289
|
+
if (!isRecord(data) || typeof node.id !== 'string') {
|
|
1290
|
+
return [];
|
|
1291
|
+
}
|
|
1292
|
+
const entryCondition = entry(readOptionalString(data['entryCondition']), workflow_graph_1.WORKFLOW_EXPRESSION_LABELS.nodeEntryCondition(node.id), 'ENTRY_CONDITION');
|
|
1293
|
+
if (node.type === 'userTask') {
|
|
1294
|
+
const resolver = data['approverResolver'];
|
|
1295
|
+
return [
|
|
1296
|
+
entryCondition,
|
|
1297
|
+
...(isRecord(resolver) && resolver['type'] === 'EXPRESSION'
|
|
1298
|
+
? [
|
|
1299
|
+
entry(readOptionalString(resolver['expression']), workflow_graph_1.WORKFLOW_EXPRESSION_LABELS.nodeApproverResolverExpression(node.id), 'APPROVER_RESOLVER'),
|
|
1300
|
+
]
|
|
1301
|
+
: []),
|
|
1302
|
+
];
|
|
1303
|
+
}
|
|
1304
|
+
if (node.type !== 'serviceTask') {
|
|
1305
|
+
return [];
|
|
1306
|
+
}
|
|
1307
|
+
const action = data['action'];
|
|
1308
|
+
if (!isRecord(action)) {
|
|
1309
|
+
return [entryCondition];
|
|
1310
|
+
}
|
|
1311
|
+
if (action['type'] === 'WEBHOOK') {
|
|
1312
|
+
return [
|
|
1313
|
+
entryCondition,
|
|
1314
|
+
entry(readOptionalString(action['payload']), workflow_graph_1.WORKFLOW_EXPRESSION_LABELS.nodeActionPayload(node.id), 'WEBHOOK_PAYLOAD'),
|
|
1315
|
+
];
|
|
1316
|
+
}
|
|
1317
|
+
if (action['type'] === 'SET_FORM_FIELD') {
|
|
1318
|
+
const fieldPath = action['fieldPath'];
|
|
1319
|
+
return [
|
|
1320
|
+
entryCondition,
|
|
1321
|
+
entry(readOptionalString(action['value']), workflow_graph_1.WORKFLOW_EXPRESSION_LABELS.nodeActionValue(node.id), 'SERVICE_FORM_FIELD_VALUE', typeof fieldPath === 'string' ? readWholeFieldTarget(fieldPath) : null),
|
|
1322
|
+
];
|
|
1323
|
+
}
|
|
1324
|
+
const recipients = action['recipients'];
|
|
1325
|
+
return action['type'] === 'NOTIFY' &&
|
|
1326
|
+
isRecord(recipients) &&
|
|
1327
|
+
recipients['type'] === 'EXPRESSION'
|
|
1328
|
+
? [
|
|
1329
|
+
entryCondition,
|
|
1330
|
+
entry(readOptionalString(recipients['expression']), workflow_graph_1.WORKFLOW_EXPRESSION_LABELS.nodeActionRecipientsExpression(node.id), 'APPROVER_RESOLVER'),
|
|
1331
|
+
]
|
|
1332
|
+
: [entryCondition];
|
|
1333
|
+
}),
|
|
1334
|
+
...edges.flatMap((edge) => {
|
|
1335
|
+
const data = edge?.data;
|
|
1336
|
+
return typeof edge?.id === 'string' && isRecord(data)
|
|
1337
|
+
? [
|
|
1338
|
+
entry(readOptionalString(data['condition']), workflow_graph_1.WORKFLOW_EXPRESSION_LABELS.edgeCondition(edge.id), 'FLOW_CONDITION'),
|
|
1339
|
+
]
|
|
1340
|
+
: [];
|
|
1341
|
+
}),
|
|
1342
|
+
];
|
|
1343
|
+
}
|
|
1344
|
+
/**
|
|
1345
|
+
* The field a `SET_FORM_FIELD` path writes as a whole: `amount`, `form.amount`.
|
|
1346
|
+
* A deeper path (`meta.code`) writes inside a field, whose declared type then
|
|
1347
|
+
* says nothing about the value, so it has no target.
|
|
1348
|
+
*/
|
|
1349
|
+
function readWholeFieldTarget(fieldPath) {
|
|
1350
|
+
const segments = fieldPath
|
|
1351
|
+
.split('.')
|
|
1352
|
+
.map((segment) => segment.trim())
|
|
1353
|
+
.filter(Boolean);
|
|
1354
|
+
const fieldSegments = segments[0] === 'form' || segments[0] === 'formData'
|
|
1355
|
+
? segments.slice(1)
|
|
1356
|
+
: segments;
|
|
1357
|
+
return fieldSegments.length === 1 && !/[[\]]/u.test(fieldSegments[0])
|
|
1358
|
+
? fieldSegments[0]
|
|
1359
|
+
: null;
|
|
1360
|
+
}
|
|
1361
|
+
function readOptionalString(value) {
|
|
1362
|
+
return typeof value === 'string' || value === null ? value : undefined;
|
|
1363
|
+
}
|
|
1364
|
+
/**
|
|
1365
|
+
* Every non-blank `SET_FORM_FIELD` write path in a definition. A blank path is
|
|
1366
|
+
* reported by the structural lint already.
|
|
1367
|
+
*/
|
|
1368
|
+
function readSetFormFieldPaths(definition) {
|
|
1369
|
+
const nodes = Array.isArray(definition?.nodes)
|
|
1370
|
+
? definition.nodes
|
|
1371
|
+
: [];
|
|
1372
|
+
return nodes.flatMap((node) => {
|
|
1373
|
+
const data = node?.data;
|
|
1374
|
+
const action = isRecord(data) ? data['action'] : undefined;
|
|
1375
|
+
const fieldPath = isRecord(action) && action['type'] === 'SET_FORM_FIELD'
|
|
1376
|
+
? action['fieldPath']
|
|
1377
|
+
: undefined;
|
|
1378
|
+
if (typeof fieldPath !== 'string' || !fieldPath.trim()) {
|
|
1379
|
+
return [];
|
|
1380
|
+
}
|
|
1381
|
+
return [
|
|
1382
|
+
{
|
|
1383
|
+
fieldPath,
|
|
1384
|
+
label: `workflow.nodes.${node.id}.action.fieldPath`,
|
|
1385
|
+
reserved: [
|
|
1386
|
+
...new Set(fieldPath
|
|
1387
|
+
.split(/[.[\]"']/u)
|
|
1388
|
+
.map((segment) => segment.trim())
|
|
1389
|
+
.filter((segment) => expression_ast_1.RESERVED_PATH_SEGMENTS.has(segment))),
|
|
1390
|
+
],
|
|
1391
|
+
},
|
|
1392
|
+
];
|
|
1393
|
+
});
|
|
1394
|
+
}
|
|
1395
|
+
/**
|
|
1396
|
+
* ADR 22 §5.5, the half that holds in both dialects: a `SET_FORM_FIELD` path
|
|
1397
|
+
* has no `__proto__`, `constructor` or `prototype` segment.
|
|
1398
|
+
*/
|
|
1399
|
+
function lintSetFormFieldReservedSegments(definition) {
|
|
1400
|
+
return readSetFormFieldPaths(definition).flatMap(({ label, reserved }) => reserved.map((segment) => (0, bpm_core_shared_1.formatExpressionLintIssue)({
|
|
1401
|
+
code: 'EXPR_RESERVED_PATH_SEGMENT',
|
|
1402
|
+
label,
|
|
1403
|
+
severity: 'ERROR',
|
|
1404
|
+
subject: segment,
|
|
1405
|
+
})));
|
|
1406
|
+
}
|
|
1407
|
+
/**
|
|
1408
|
+
* ADR 22 §3.4 / §5.5 for cel-v2 definitions: a `SET_FORM_FIELD` writes a
|
|
1409
|
+
* field the bound form declares. A path with a reserved segment is reported
|
|
1410
|
+
* by `lintSetFormFieldReservedSegments` instead. cel-legacy does not get this
|
|
1411
|
+
* half yet: a fork of an older version may write a field its form dropped.
|
|
1412
|
+
*/
|
|
1413
|
+
function lintSetFormFieldDeclaredFields(definition, schema) {
|
|
1414
|
+
if (!schema) {
|
|
1415
|
+
return [];
|
|
1416
|
+
}
|
|
1417
|
+
const fieldKeys = new Set((schema.fields ?? []).map((entry) => entry.fieldKey));
|
|
1418
|
+
return readSetFormFieldPaths(definition)
|
|
1419
|
+
.filter(({ reserved }) => reserved.length === 0)
|
|
1420
|
+
.flatMap(({ fieldPath, label }) => {
|
|
1421
|
+
const fieldKey = (0, bpm_core_shared_1.readFormFieldPathTopLevelKey)(fieldPath);
|
|
1422
|
+
return fieldKeys.has(fieldKey)
|
|
1423
|
+
? []
|
|
1424
|
+
: [
|
|
1425
|
+
(0, bpm_core_shared_1.formatExpressionLintIssue)({
|
|
1426
|
+
code: 'EXPR_FIELD_UNKNOWN',
|
|
1427
|
+
label,
|
|
1428
|
+
severity: 'ERROR',
|
|
1429
|
+
subject: fieldKey,
|
|
1430
|
+
}),
|
|
1431
|
+
];
|
|
1432
|
+
});
|
|
1433
|
+
}
|
|
1146
1434
|
function isRecord(value) {
|
|
1147
1435
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
1148
1436
|
}
|