@uipath/maestro-builder-sdk 6.63.0 → 6.65.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/dist/api-index.md +22 -21
- package/dist/api-members.md +82 -81
- package/dist/case/case-expr-check.d.ts +2 -1
- package/dist/case/case-expr-check.js +4 -2
- package/dist/case/case-sdk.js +11 -10
- package/dist/case/load.js +4 -3
- package/dist/case/serialize.js +52 -50
- package/dist/cli/commands/flow.js +4 -2
- package/dist/compile-cli.js +55 -45
- package/dist/core/event-filters.d.ts +20 -0
- package/dist/core/event-filters.js +27 -0
- package/dist/core/refusals.d.ts +88 -1
- package/dist/core/refusals.js +94 -7
- package/dist/decompile-cli.js +2 -2
- package/dist/decompile.js +36 -11
- package/dist/flow-node-type.d.ts +9 -0
- package/dist/flow-sdk.d.ts +24 -3
- package/dist/flow-sdk.js +1 -1
- package/dist/nodes/agent/flow/check.js +4 -4
- package/dist/nodes/connector/flow/node-type.js +36 -1
- package/dist/serialize.d.ts +24 -0
- package/dist/serialize.js +114 -4
- package/package.json +2 -2
package/dist/case/load.js
CHANGED
|
@@ -3,6 +3,7 @@ import { pathToFileURL } from 'node:url';
|
|
|
3
3
|
import { resolve } from 'node:path';
|
|
4
4
|
import { existsSync, statSync } from 'node:fs';
|
|
5
5
|
import { useThisSdkForAuthoredSource } from '../sdk-resolve.js';
|
|
6
|
+
import { CompileRefusal } from '../core/compile-refusal.js';
|
|
6
7
|
/**
|
|
7
8
|
* Resolve a user argument (a `.ts` path or a BaseName) to a case source file.
|
|
8
9
|
*
|
|
@@ -19,16 +20,16 @@ export function resolveCaseFile(arg) {
|
|
|
19
20
|
if (existsSync(resolved) && statSync(resolved).isFile())
|
|
20
21
|
return resolved;
|
|
21
22
|
}
|
|
22
|
-
throw new
|
|
23
|
+
throw new CompileRefusal('CASE_SOURCE_NOT_FOUND', `case source not found: tried ${candidates.join(', ')}`);
|
|
23
24
|
}
|
|
24
25
|
export async function loadBuiltCase(file) {
|
|
25
26
|
useThisSdkForAuthoredSource();
|
|
26
27
|
const mod = await import(pathToFileURL(file).href);
|
|
27
28
|
const exp = mod.default ?? mod.casePlan ?? mod.case;
|
|
28
29
|
if (!exp)
|
|
29
|
-
throw new
|
|
30
|
+
throw new CompileRefusal('CASE_NO_DEFAULT_EXPORT', `${file} has no default export. Add: export default casePlan('id')...build();`);
|
|
30
31
|
const built = typeof exp.build === 'function' ? exp.build() : exp;
|
|
31
32
|
if (!built || !Array.isArray(built.stages))
|
|
32
|
-
throw new
|
|
33
|
+
throw new CompileRefusal('CASE_EXPORT_NOT_BUILT', `${file} default export is not a built case. Did you call .build()?`);
|
|
33
34
|
return built;
|
|
34
35
|
}
|
package/dist/case/serialize.js
CHANGED
|
@@ -19,16 +19,17 @@ import { bindingLabelRefusal } from '../core/binding-messages.js';
|
|
|
19
19
|
import { resolveEventWhere } from '../core/lookups.js';
|
|
20
20
|
import { resolveCaseSubscription } from './subscription.js';
|
|
21
21
|
import { dedupeConditionNames } from './rule-names.js';
|
|
22
|
-
import { externalDescriptorRefusal, externalHybridRefusal, externalServiceType, isExternalExecutionMode } from './external.js';
|
|
22
|
+
import { EXTERNAL_TASK_DESCRIPTOR_CONTRACT, EXTERNAL_TASK_HYBRID_UNSUPPORTED, externalDescriptorRefusal, externalHybridRefusal, externalServiceType, isExternalExecutionMode, } from './external.js';
|
|
23
23
|
import { CASE_FORMAT_PROFILE, CASE_SCHEMA_VERSIONS } from './format-profile.js';
|
|
24
24
|
import { isResourceLabelRefusal, resolveResourceRef, resourceIdentity } from './resource-identity.js';
|
|
25
25
|
import { ownEntry } from '../core/own-entry.js';
|
|
26
26
|
import { resolveSchemaVersion } from '../schema-version.js';
|
|
27
27
|
import { caseJsonPreservation, caseJsonPreservationErrors, } from './preserve.js';
|
|
28
28
|
import { businessEventProblems } from './business-event-shape.js';
|
|
29
|
-
import {
|
|
29
|
+
import { readEventFilterLeaf, eventFilterTreeLeaf } from '../core/event-filters.js';
|
|
30
30
|
import { responseJsonSchema } from '../core/connector-response-schema.js';
|
|
31
31
|
import { ruleShapeProblems } from './rule-options.js';
|
|
32
|
+
import { CompileRefusal } from '../core/compile-refusal.js';
|
|
32
33
|
/**
|
|
33
34
|
* Condition `displayName` allocator.
|
|
34
35
|
*
|
|
@@ -135,7 +136,7 @@ export function serialize(built, opts = {}) {
|
|
|
135
136
|
built.stages.forEach((stage, si) => {
|
|
136
137
|
const sid = ids.stageId(`stage:${stage.label}`);
|
|
137
138
|
if (resolver.stageIdByLabel.has(stage.label))
|
|
138
|
-
throw new
|
|
139
|
+
throw new CompileRefusal('DUP_STAGE', `duplicate stage label "${stage.label}".`);
|
|
139
140
|
resolver.stageIdByLabel.set(stage.label, sid);
|
|
140
141
|
stageIds.push(sid);
|
|
141
142
|
taskIds.push([]);
|
|
@@ -143,7 +144,7 @@ export function serialize(built, opts = {}) {
|
|
|
143
144
|
taskIds[si].push([]);
|
|
144
145
|
lane.forEach((task) => {
|
|
145
146
|
if (seenTaskNames.has(task.name))
|
|
146
|
-
throw new
|
|
147
|
+
throw new CompileRefusal('DUP_TASK', `duplicate task displayName "${task.name}" — task names must be unique across the case.`);
|
|
147
148
|
seenTaskNames.add(task.name);
|
|
148
149
|
const tid = ids.taskId(`task:${stage.label}/${task.name}`);
|
|
149
150
|
taskIds[si][li].push(tid);
|
|
@@ -187,13 +188,13 @@ export function serialize(built, opts = {}) {
|
|
|
187
188
|
const selectNextStage = built.stages.flatMap((stage) => stage.exit)
|
|
188
189
|
.filter((exit) => exit.selectNextStage !== undefined);
|
|
189
190
|
if (selectNextStage.length > 1)
|
|
190
|
-
throw new
|
|
191
|
+
throw new CompileRefusal('SELECT_NEXT_STAGE_MULTIPLE', 'select-next-stage can be configured on at most one stage exit per case.');
|
|
191
192
|
if (selectNextStage[0]?.selectNextStage) {
|
|
192
193
|
const { objectName, connector } = selectNextStage[0].selectNextStage;
|
|
193
194
|
if (selectNextStage[0].type !== 'wait-for-user')
|
|
194
|
-
throw new
|
|
195
|
+
throw new CompileRefusal('SELECT_NEXT_STAGE_EXIT_TYPE', 'select-next-stage requires an exit with type "wait-for-user".');
|
|
195
196
|
if (!objectName.trim())
|
|
196
|
-
throw new
|
|
197
|
+
throw new CompileRefusal('SELECT_NEXT_STAGE_OBJECT_NAME', 'select-next-stage objectName must be non-empty.');
|
|
197
198
|
const context = [];
|
|
198
199
|
if (connector) {
|
|
199
200
|
context.push(io('connectorKey', 'string', { value: connector.connectorKey }));
|
|
@@ -234,7 +235,7 @@ export function serialize(built, opts = {}) {
|
|
|
234
235
|
if (built.businessEvents?.length) {
|
|
235
236
|
const [problem] = businessEventProblems(built.businessEvents);
|
|
236
237
|
if (problem)
|
|
237
|
-
throw new
|
|
238
|
+
throw new CompileRefusal(problem.code, problem.message);
|
|
238
239
|
metadata.businessEvents = built.businessEvents.map(emitBusinessEvent);
|
|
239
240
|
}
|
|
240
241
|
// Variables: declared vars + the readable companions that REASSIGNED task
|
|
@@ -347,11 +348,11 @@ function applyAuthorLayout(result, authored) {
|
|
|
347
348
|
const nodes = result.nodes;
|
|
348
349
|
const layoutValue = result.layout;
|
|
349
350
|
if (layoutValue !== undefined && (!layoutValue || typeof layoutValue !== 'object' || Array.isArray(layoutValue)))
|
|
350
|
-
throw new Error('case layout: preserved layout must be an object before authored entries can be applied.');
|
|
351
|
+
throw new Error('internal SDK error: case layout: preserved layout must be an object before authored entries can be applied.');
|
|
351
352
|
const existingLayout = (layoutValue ?? {});
|
|
352
353
|
const existingNodes = existingLayout.nodes;
|
|
353
354
|
if (existingNodes !== undefined && (!existingNodes || typeof existingNodes !== 'object' || Array.isArray(existingNodes)))
|
|
354
|
-
throw new Error('case layout: preserved layout.nodes must be an object before authored entries can be applied.');
|
|
355
|
+
throw new Error('internal SDK error: case layout: preserved layout.nodes must be an object before authored entries can be applied.');
|
|
355
356
|
const layoutNodes = { ...(existingNodes ?? {}) };
|
|
356
357
|
const layout = { ...existingLayout, nodes: layoutNodes };
|
|
357
358
|
for (const entry of entries) {
|
|
@@ -363,16 +364,16 @@ function applyAuthorLayout(result, authored) {
|
|
|
363
364
|
return trigger && label === entry.name;
|
|
364
365
|
});
|
|
365
366
|
if (matches.length !== 1)
|
|
366
|
-
throw new
|
|
367
|
+
throw new CompileRefusal('CASE_LAYOUT_NODE_MATCH', `case layout: ${entry.kind} "${entry.name}" matches ${matches.length} emitted nodes; expected exactly 1.`);
|
|
367
368
|
const nodeId = matches[0].id;
|
|
368
369
|
if (typeof nodeId !== 'string' || !nodeId)
|
|
369
|
-
throw new Error(`case layout: ${entry.kind} "${entry.name}" has no emitted node id.`);
|
|
370
|
+
throw new Error(`internal SDK error: case layout: ${entry.kind} "${entry.name}" has no emitted node id.`);
|
|
370
371
|
const unknown = Object.keys(entry.layout).filter((field) => !CASE_NODE_LAYOUT_FIELDS.includes(field));
|
|
371
372
|
if (unknown.length)
|
|
372
|
-
throw new
|
|
373
|
+
throw new CompileRefusal('CASE_LAYOUT_UNKNOWN_FIELD', `case layout: ${entry.kind} "${entry.name}" has unsupported field "${unknown[0]}".`);
|
|
373
374
|
const previous = layoutNodes[nodeId];
|
|
374
375
|
if (previous !== undefined && (!previous || typeof previous !== 'object' || Array.isArray(previous)))
|
|
375
|
-
throw new Error(`case layout: preserved entry for ${entry.kind} "${entry.name}" must be an object.`);
|
|
376
|
+
throw new Error(`internal SDK error: case layout: preserved entry for ${entry.kind} "${entry.name}" must be an object.`);
|
|
376
377
|
const merged = { ...(previous ?? {}) };
|
|
377
378
|
for (const field of CASE_NODE_LAYOUT_FIELDS)
|
|
378
379
|
if (entry.layout[field] !== undefined)
|
|
@@ -394,7 +395,7 @@ export function applyPreservation(result, preservation, options = {}) {
|
|
|
394
395
|
return result;
|
|
395
396
|
const errors = caseJsonPreservationErrors(preservation);
|
|
396
397
|
if (errors.length)
|
|
397
|
-
throw new
|
|
398
|
+
throw new CompileRefusal('PRESERVATION_COLLISION', `invalid Case JSON preservation payload: ${errors.join('; ')}`);
|
|
398
399
|
applyPreservedIdentities(result, preservation);
|
|
399
400
|
Object.assign(result, preservation.topLevel ?? {});
|
|
400
401
|
Object.assign(result.metadata, preservation.metadata ?? {});
|
|
@@ -402,7 +403,7 @@ export function applyPreservation(result, preservation, options = {}) {
|
|
|
402
403
|
if (caseAppConfig && preservation.caseAppConfigFields) {
|
|
403
404
|
for (const [field, value] of Object.entries(preservation.caseAppConfigFields.fields ?? {})) {
|
|
404
405
|
if (!options.overwrite && Object.prototype.hasOwnProperty.call(caseAppConfig, field))
|
|
405
|
-
throw new
|
|
406
|
+
throw new CompileRefusal('PRESERVATION_FIELD_COLLISION', `preserved caseAppConfig field "${field}" collides with serializer-owned output.`);
|
|
406
407
|
caseAppConfig[field] = value;
|
|
407
408
|
}
|
|
408
409
|
for (const entry of preservation.caseAppConfigFields.sections ?? []) {
|
|
@@ -411,7 +412,7 @@ export function applyPreservation(result, preservation, options = {}) {
|
|
|
411
412
|
continue;
|
|
412
413
|
for (const [field, value] of Object.entries(entry.fields)) {
|
|
413
414
|
if (!options.overwrite && Object.prototype.hasOwnProperty.call(section, field))
|
|
414
|
-
throw new
|
|
415
|
+
throw new CompileRefusal('PRESERVATION_FIELD_COLLISION', `preserved Case App section "${entry.id}" field "${field}" collides with serializer-owned output.`);
|
|
415
416
|
section[field] = value;
|
|
416
417
|
}
|
|
417
418
|
}
|
|
@@ -431,10 +432,10 @@ export function applyPreservation(result, preservation, options = {}) {
|
|
|
431
432
|
if (!row && options.overwrite)
|
|
432
433
|
continue;
|
|
433
434
|
if (!row)
|
|
434
|
-
throw new
|
|
435
|
+
throw new CompileRefusal('PRESERVATION_TARGET_MISSING', `preserved variable "${name}" was not regenerated in variables.${lane}.`);
|
|
435
436
|
for (const [field, value] of Object.entries(fields)) {
|
|
436
437
|
if (!options.overwrite && Object.prototype.hasOwnProperty.call(row, field))
|
|
437
|
-
throw new
|
|
438
|
+
throw new CompileRefusal('PRESERVATION_FIELD_COLLISION', `preserved variable "${name}" field "${field}" collides with serializer-owned output.`);
|
|
438
439
|
row[field] = value;
|
|
439
440
|
}
|
|
440
441
|
}
|
|
@@ -446,7 +447,7 @@ export function applyPreservation(result, preservation, options = {}) {
|
|
|
446
447
|
if (!row && options.overwrite)
|
|
447
448
|
continue;
|
|
448
449
|
if (!row)
|
|
449
|
-
throw new
|
|
450
|
+
throw new CompileRefusal('PRESERVATION_TARGET_MISSING', `legacy variable default "${name}" was not regenerated in variables.${lane}.`);
|
|
450
451
|
const encoded = JSON.stringify(value);
|
|
451
452
|
// The legacy row exists to carry the NON-STRING spelling of a value the
|
|
452
453
|
// serializer now writes as a string, so it is only applicable while the two
|
|
@@ -457,7 +458,7 @@ export function applyPreservation(result, preservation, options = {}) {
|
|
|
457
458
|
if (row.default !== encoded) {
|
|
458
459
|
if (options.overwrite)
|
|
459
460
|
continue;
|
|
460
|
-
throw new
|
|
461
|
+
throw new CompileRefusal('PRESERVATION_LEGACY_DEFAULT_MISMATCH', `legacy variable default "${name}" does not match the serializer-owned default.`);
|
|
461
462
|
}
|
|
462
463
|
row.default = value;
|
|
463
464
|
}
|
|
@@ -485,7 +486,7 @@ export function applyPreservation(result, preservation, options = {}) {
|
|
|
485
486
|
if (task[field] === value)
|
|
486
487
|
continue;
|
|
487
488
|
if (!options.overwrite)
|
|
488
|
-
throw new
|
|
489
|
+
throw new CompileRefusal('PRESERVATION_FIELD_COLLISION', `preserved task "${entry.stage}/${entry.task}" field "${field}" collides with serializer-owned output.`);
|
|
489
490
|
}
|
|
490
491
|
task[field] = value;
|
|
491
492
|
}
|
|
@@ -509,7 +510,7 @@ export function applyPreservation(result, preservation, options = {}) {
|
|
|
509
510
|
if (!trigger && options.overwrite)
|
|
510
511
|
continue;
|
|
511
512
|
if (!trigger)
|
|
512
|
-
throw new
|
|
513
|
+
throw new CompileRefusal('PRESERVATION_TARGET_MISSING', `trigger ${index + 1} was not regenerated for preserved data fields.`);
|
|
513
514
|
Object.assign(trigger.data, fields);
|
|
514
515
|
}
|
|
515
516
|
if (preservation.nodes?.length) {
|
|
@@ -721,7 +722,7 @@ function emitVariables(vars, ids, triggerNodeByRef) {
|
|
|
721
722
|
return { default: vd.default };
|
|
722
723
|
const value = JSON.stringify(vd.default);
|
|
723
724
|
if (value === undefined)
|
|
724
|
-
throw new
|
|
725
|
+
throw new CompileRefusal('CASE_VAR_DEFAULT_NOT_JSON', `Case variable "${vd.name}" has a default that cannot be JSON encoded.`);
|
|
725
726
|
return { default: value };
|
|
726
727
|
};
|
|
727
728
|
const readableCompanion = (vd) => ({
|
|
@@ -748,7 +749,7 @@ function emitVariables(vars, ids, triggerNodeByRef) {
|
|
|
748
749
|
// to the trigger node, so it does not also take a root companion.
|
|
749
750
|
const node = triggerNodeByRef.get(vd.sourceTrigger);
|
|
750
751
|
if (!node)
|
|
751
|
-
throw new
|
|
752
|
+
throw new CompileRefusal('CASE_IN_ARG_TRIGGER_NOT_ADDED', `In-arg "${vd.name}" is bound via { from } to a trigger that was not added with .trigger(...).`);
|
|
752
753
|
entry.default = vd.default === undefined ? '' : encodedDefault(vd).default;
|
|
753
754
|
entry.elementId = node.id;
|
|
754
755
|
inputs.push(entry);
|
|
@@ -1038,7 +1039,7 @@ function withOutputExtracts(data, task, ids, resolver) {
|
|
|
1038
1039
|
const own = Array.isArray(data.outputs) ? data.outputs : [];
|
|
1039
1040
|
const elementId = own.find((row) => typeof row.elementId === 'string')?.elementId;
|
|
1040
1041
|
if (!elementId)
|
|
1041
|
-
throw new
|
|
1042
|
+
throw new CompileRefusal('TASK_OUTPUTS_NO_RESPONSE', `task "${task.name}": output extracts need the task's resolved response output.`);
|
|
1042
1043
|
data.outputs = [
|
|
1043
1044
|
...own,
|
|
1044
1045
|
...task.outputs.map((o, i) => emitTaskOutput(o, elementId, ids, `${elementId}:out:${i}:${o.var}`, resolver)),
|
|
@@ -1076,7 +1077,7 @@ function isResolvedSubscription(spec) {
|
|
|
1076
1077
|
* by the archived v1 artifacts. Placeholder bags never enter this path. */
|
|
1077
1078
|
function resolvedSubscriptionBag(kind, spec, resolver, elementId, where) {
|
|
1078
1079
|
if (!resolver.library)
|
|
1079
|
-
throw new
|
|
1080
|
+
throw new CompileRefusal('LIBRARY_REQUIRED', `${kind === 'trigger' ? 'event trigger' : 'wait-for-connector'} needs the connector library — `
|
|
1080
1081
|
+ `pass { library } to serialize or use uip maestro case compile --library.`);
|
|
1081
1082
|
const rc = resolveCaseSubscription(resolver.library, kind, spec, { where, bindings: resolver.bindings });
|
|
1082
1083
|
const op = rc.entry.operation ?? {};
|
|
@@ -1121,12 +1122,13 @@ function subscriptionMetadata(rc, op, filters, elementId) {
|
|
|
1121
1122
|
filters: filters.map((entry, index) => {
|
|
1122
1123
|
// Same authored shape as a flow's event filter, same designer grammar (see
|
|
1123
1124
|
// `event-filters.ts`); a malformed leaf is refused rather than dropped.
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1125
|
+
// The code `check` gives the same leaf (`core/event-checks.ts`).
|
|
1126
|
+
const judged = readEventFilterLeaf(entry);
|
|
1127
|
+
if (judged.code !== undefined)
|
|
1128
|
+
throw new CompileRefusal(judged.code, judged.problem !== undefined
|
|
1129
|
+
? `event filter ${index} on \`${judged.read?.field}\`: ${judged.problem}`
|
|
1130
|
+
: `event filter ${index} must carry a field and exactly one operator — got ${JSON.stringify(entry)}.`);
|
|
1131
|
+
const { read } = judged;
|
|
1130
1132
|
return {
|
|
1131
1133
|
...eventFilterTreeLeaf(read),
|
|
1132
1134
|
uiId: stableUuid(`${elementId}:${read.field}:${String(read.value)}:${index}`),
|
|
@@ -1393,14 +1395,14 @@ function referenceBindings(kind, authored, resolver, ids, taskName) {
|
|
|
1393
1395
|
// literal value, so the task binds exactly as the literal form does.
|
|
1394
1396
|
const ref = resolveResourceRef(authored, resolver.bindings, `Task "${taskName}"`);
|
|
1395
1397
|
if (isResourceLabelRefusal(ref))
|
|
1396
|
-
throw new
|
|
1398
|
+
throw new CompileRefusal(ref.code, ref.message);
|
|
1397
1399
|
const binding = kind === 'business-rule'
|
|
1398
1400
|
? { resource: BUSINESS_RULE_RESOURCE }
|
|
1399
1401
|
: kind === 'action'
|
|
1400
1402
|
? { resource: ACTION_APP_RESOURCE }
|
|
1401
1403
|
: ownEntry(REFERENCE_BINDING, kind);
|
|
1402
1404
|
if (!binding)
|
|
1403
|
-
throw new Error(`task kind "${kind}" is not a published-resource task.`);
|
|
1405
|
+
throw new Error(`internal SDK error: task kind "${kind}" is not a published-resource task.`);
|
|
1404
1406
|
// An in-solution resource (`folder: 'solution_folder'`) keeps the sentinel
|
|
1405
1407
|
// in its key and binds an empty folderPath — see resource-identity.ts.
|
|
1406
1408
|
const { resourceKey, folderPath } = resourceIdentity(ref.name, ref.folderPath);
|
|
@@ -1459,7 +1461,7 @@ function businessRuleTaskData(task, resolver, elementId, ids) {
|
|
|
1459
1461
|
*/
|
|
1460
1462
|
function connectorTaskData(spec, resolver, elementId, where, executionType) {
|
|
1461
1463
|
if (!resolver.library)
|
|
1462
|
-
throw new
|
|
1464
|
+
throw new CompileRefusal('LIBRARY_REQUIRED', `connector task needs the connector library — pass { library } to serialize or use uip maestro case compile --library.`);
|
|
1463
1465
|
const rc = resolver.library.resolve(spec.key, spec.action, spec.version, spec.object);
|
|
1464
1466
|
const op = rc.entry.operation ?? {};
|
|
1465
1467
|
const context = [
|
|
@@ -1527,17 +1529,17 @@ function connectorTaskData(spec, resolver, elementId, where, executionType) {
|
|
|
1527
1529
|
*/
|
|
1528
1530
|
function externalTaskData(kind, spec, resolver, elementId, where) {
|
|
1529
1531
|
if (!resolver.library)
|
|
1530
|
-
throw new
|
|
1532
|
+
throw new CompileRefusal('LIBRARY_REQUIRED', `${kind} task needs the connector library — pass { library } to serialize or use uip maestro case compile --library.`);
|
|
1531
1533
|
const rc = resolver.library.resolve(spec.connector.key, spec.connector.action, spec.connector.version, spec.connector.object);
|
|
1532
1534
|
const refused = externalDescriptorRefusal(kind, rc);
|
|
1533
1535
|
if (refused)
|
|
1534
|
-
throw new
|
|
1536
|
+
throw new CompileRefusal(EXTERNAL_TASK_DESCRIPTOR_CONTRACT, refused);
|
|
1535
1537
|
if (!isExternalExecutionMode(spec.mode))
|
|
1536
|
-
throw new
|
|
1538
|
+
throw new CompileRefusal('EXTERNAL_TASK_MODE', `${kind} task mode must be the connector object's executionType: "sync", "async" or "hybrid".`);
|
|
1537
1539
|
if (spec.mode === 'hybrid')
|
|
1538
|
-
throw new
|
|
1540
|
+
throw new CompileRefusal(EXTERNAL_TASK_HYBRID_UNSUPPORTED, externalHybridRefusal(where));
|
|
1539
1541
|
if (!spec.connector.connection || !spec.connector.folder)
|
|
1540
|
-
throw new
|
|
1542
|
+
throw new CompileRefusal(spec.connector.connection ? 'EXTERNAL_TASK_FOLDER' : 'EXTERNAL_TASK_CONNECTION', `${kind} task requires non-empty connection and folder binding names.`);
|
|
1541
1543
|
const data = connectorTaskData(spec.connector, resolver, elementId, where, spec.mode);
|
|
1542
1544
|
const context = data.context;
|
|
1543
1545
|
const metadataAt = context.findIndex((row) => row.name === 'metadata');
|
|
@@ -1642,7 +1644,7 @@ function folderBindingRef(resolver, name, connection, site) {
|
|
|
1642
1644
|
for (const row of resolver.bindingSink.values()) {
|
|
1643
1645
|
if (row.resourceKey === connection.resourceKey && row.propertyAttribute === 'folderKey'
|
|
1644
1646
|
&& row.default !== folderKey)
|
|
1645
|
-
throw new
|
|
1647
|
+
throw new CompileRefusal('BINDING_CONNECTION_TWO_FOLDERS', `${site.where}: connection "${connection.resourceKey}" is bound to two folders: `
|
|
1646
1648
|
+ `"${String(row.name)}" (${String(row.default)}) and "${name}" (${folderKey}). `
|
|
1647
1649
|
+ `A connection lives in one folder; use the same folder binding for every task on it.`);
|
|
1648
1650
|
}
|
|
@@ -1826,13 +1828,13 @@ function emitRules(rules, seed, ownerId, ids, resolver, site) {
|
|
|
1826
1828
|
function emitRule(r, seed, ownerId, ids, resolver, site) {
|
|
1827
1829
|
const [shape] = ruleShapeProblems(r);
|
|
1828
1830
|
if (shape)
|
|
1829
|
-
throw new
|
|
1831
|
+
throw new CompileRefusal(shape.code, `${shape.message.replace(/^Rule /, 'rule ')} (in ${site})`);
|
|
1830
1832
|
const ruleId = ids.ruleId(seed);
|
|
1831
1833
|
const out = { rule: r.rule, id: ruleId };
|
|
1832
1834
|
if (r.rule === 'selected-stage-completed' || r.rule === 'selected-stage-exited') {
|
|
1833
1835
|
const sid = r.stage && resolver.stageIdByLabel.get(r.stage);
|
|
1834
1836
|
if (!sid)
|
|
1835
|
-
throw new
|
|
1837
|
+
throw new CompileRefusal('UNKNOWN_STAGE_REF', `rule "${r.rule}" references unknown stage "${r.stage}".`);
|
|
1836
1838
|
out[CASE_FORMAT_PROFILE.selectedStageField] = CASE_FORMAT_PROFILE.selectedStageCardinality === 'singular' ? sid : [sid];
|
|
1837
1839
|
}
|
|
1838
1840
|
if (r.rule === 'selected-tasks-completed') {
|
|
@@ -1840,7 +1842,7 @@ function emitRule(r, seed, ownerId, ids, resolver, site) {
|
|
|
1840
1842
|
out.selectedTasksIds = names.map((n) => {
|
|
1841
1843
|
const tid = resolver.taskIdByKey.get(n);
|
|
1842
1844
|
if (!tid)
|
|
1843
|
-
throw new
|
|
1845
|
+
throw new CompileRefusal('UNKNOWN_TASK_REF', `rule "selected-tasks-completed" references unknown task "${n}".`);
|
|
1844
1846
|
return tid;
|
|
1845
1847
|
});
|
|
1846
1848
|
}
|
|
@@ -1853,12 +1855,12 @@ function emitRule(r, seed, ownerId, ids, resolver, site) {
|
|
|
1853
1855
|
// preallocated id. `slaId` alone = a breach rule; adding `escalationId` = at-risk.
|
|
1854
1856
|
const slaId = r.sla && resolver.slaIdByName.get(r.sla);
|
|
1855
1857
|
if (!slaId)
|
|
1856
|
-
throw new
|
|
1858
|
+
throw new CompileRefusal('UNKNOWN_SLA_REF', `rule "sla-status-change" references unknown SLA "${r.sla}" — declare it with .sla({ displayName: "${r.sla}", … }).`);
|
|
1857
1859
|
out.slaId = slaId;
|
|
1858
1860
|
if (r.escalation) {
|
|
1859
1861
|
const escId = resolver.escalationIdByName.get(r.escalation);
|
|
1860
1862
|
if (!escId)
|
|
1861
|
-
throw new
|
|
1863
|
+
throw new CompileRefusal('UNKNOWN_ESCALATION_REF', `rule "sla-status-change" references unknown escalation "${r.escalation}".`);
|
|
1862
1864
|
out.escalationId = escId;
|
|
1863
1865
|
}
|
|
1864
1866
|
}
|
|
@@ -1866,9 +1868,9 @@ function emitRule(r, seed, ownerId, ids, resolver, site) {
|
|
|
1866
1868
|
// The event is matched by its exact name at run time; a rule without one
|
|
1867
1869
|
// listens for nothing, so both a missing and an undeclared name refuse.
|
|
1868
1870
|
if (!r.businessEvent)
|
|
1869
|
-
throw new
|
|
1871
|
+
throw new CompileRefusal('API_EVENT_NO_BUSINESS_EVENT', `rule "api-event" in ${site} names no business event — write rule('api-event', { businessEvent: '<name>' }) with a name declared by .businessEvent('<name>').`);
|
|
1870
1872
|
if (!resolver.businessEvents.has(r.businessEvent))
|
|
1871
|
-
throw new
|
|
1873
|
+
throw new CompileRefusal('UNKNOWN_BUSINESS_EVENT', `rule "api-event" in ${site} listens for business event "${r.businessEvent}", which the case does not declare — add .businessEvent(${JSON.stringify(r.businessEvent)}).`);
|
|
1872
1874
|
out.businessEventType = r.businessEvent;
|
|
1873
1875
|
}
|
|
1874
1876
|
if (r.expression)
|
|
@@ -1939,7 +1941,7 @@ function emitExit(c, seed, ownerId, label, ids, resolver) {
|
|
|
1939
1941
|
if (c.exitToStage) {
|
|
1940
1942
|
const sid = resolver.stageIdByLabel.get(c.exitToStage);
|
|
1941
1943
|
if (!sid)
|
|
1942
|
-
throw new
|
|
1944
|
+
throw new CompileRefusal('UNKNOWN_EXIT_STAGE', `exit condition routes to unknown stage "${c.exitToStage}".`);
|
|
1943
1945
|
out.exitToStageId = sid;
|
|
1944
1946
|
}
|
|
1945
1947
|
return out;
|
|
@@ -23,14 +23,16 @@ export function registerFlowCommands(program) {
|
|
|
23
23
|
.option('--library <dir>', 'connector library directory')
|
|
24
24
|
.option('--bindings <file>', 'bindings.json path')
|
|
25
25
|
.option('--connectors-local <dir>', 'connection-resolved connector overlay')
|
|
26
|
-
.option('--no-check', '
|
|
26
|
+
.option('--no-check', 'deprecated, does nothing: compile no longer runs check')
|
|
27
27
|
.action(async (source, options) => {
|
|
28
28
|
const argv = [source];
|
|
29
29
|
addValue(argv, '-o', options.output);
|
|
30
30
|
addValue(argv, '--library', options.library);
|
|
31
31
|
addValue(argv, '--bindings', options.bindings);
|
|
32
32
|
addValue(argv, '--connectors-local', options.connectorsLocal);
|
|
33
|
-
|
|
33
|
+
// Passed on only when given, so compile prints its deprecation warning.
|
|
34
|
+
// Emit-only mode needs nothing here: compile does not run check.
|
|
35
|
+
if (options.check === false)
|
|
34
36
|
argv.push('--no-check');
|
|
35
37
|
await invoke('flow compile', () => import('../../compile-cli.js'), argv);
|
|
36
38
|
});
|
package/dist/compile-cli.js
CHANGED
|
@@ -10,12 +10,17 @@
|
|
|
10
10
|
* --library defaults to $FLOW_SDK_LIBRARY_JSON; --bindings defaults to
|
|
11
11
|
* .flow-sdk/bindings.json (or a legacy ./bindings.json) when present. Connector actions require a library.
|
|
12
12
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
13
|
+
* `compile` only serializes (docs/CHECK_COMPILE_CONTRACT_DESIGN.md D1, D2). It
|
|
14
|
+
* does not run `check()`'s lint rules: run `flow check` first for those. It
|
|
15
|
+
* stops only when it cannot write a correct `.flow`, a refusal the serializer
|
|
16
|
+
* throws. A draft node (a connector with no connection, an empty required
|
|
17
|
+
* input) the canvas saved is written, with a warning: decompile marks its step
|
|
18
|
+
* `{ draft: true }` and carries its definition in `.definitions([...])`. Any
|
|
19
|
+
* other draft, such as one an agent just wrote (even of an operation the canvas
|
|
20
|
+
* has a draft of), is refused.
|
|
21
|
+
*
|
|
22
|
+
* `--no-check` is a deprecated no-op: it is accepted for one minor version, with
|
|
23
|
+
* a warning, and then removed.
|
|
19
24
|
*/
|
|
20
25
|
import { diagnosticLine, reportDiagnostics, reportResult, thrownDiagnostic } from './cli-result.js';
|
|
21
26
|
import { writeJsonFile } from './core/json-file.js';
|
|
@@ -24,8 +29,7 @@ import { basename, dirname, join, relative, resolve } from 'node:path';
|
|
|
24
29
|
import { resolveFlowFile, loadBuiltFlow } from './load.js';
|
|
25
30
|
import { serializeWithSidecars } from './serialize.js';
|
|
26
31
|
import { compileOwnsResource } from './core/agent-tool-resources.js';
|
|
27
|
-
import {
|
|
28
|
-
import { flowCheckOpts, openCliLibrary } from './cli-library.js';
|
|
32
|
+
import { openCliLibrary } from './cli-library.js';
|
|
29
33
|
import { Bindings } from './bindings.js';
|
|
30
34
|
import { flagValue, runWhenInvokedDirectly } from './cli-run.js';
|
|
31
35
|
import { connectionResources, inlineAgentResources, mergeBindingsV2, refreshInlineAgentRows } from './bindings-v2.js';
|
|
@@ -35,8 +39,8 @@ import { defaultBindingsFile } from './workdir.js';
|
|
|
35
39
|
import { isWithin } from './path-within.js';
|
|
36
40
|
import { flowSources, resourceSourcesOfOtherFlows } from './project-resource-users.js';
|
|
37
41
|
import { fnv1aX4Uuid } from './core/stable-id.js';
|
|
38
|
-
import {
|
|
39
|
-
import {
|
|
42
|
+
import { CompileRefusal } from './core/compile-refusal.js';
|
|
43
|
+
import { cliSpelling } from './core/cli-spelling.js';
|
|
40
44
|
/** An agent folder name: a uuid, any case. */
|
|
41
45
|
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
42
46
|
export async function run(argv) {
|
|
@@ -45,7 +49,9 @@ export async function run(argv) {
|
|
|
45
49
|
return 2;
|
|
46
50
|
}
|
|
47
51
|
const flags = new Set(['-o', '--library', '--bindings', '--connectors-local']);
|
|
48
|
-
|
|
52
|
+
if (argv.includes('--no-check')) {
|
|
53
|
+
console.error(`compile: --no-check is deprecated and does nothing: compile no longer runs check. Run \`${cliSpelling()} flow check\` before \`compile\`; the flag will be removed.`);
|
|
54
|
+
}
|
|
49
55
|
const pos = argv.find((a, i) => !flags.has(a) && !flags.has(argv[i - 1]));
|
|
50
56
|
if (!pos) {
|
|
51
57
|
console.error('compile: missing <flow.ts | BaseName>');
|
|
@@ -66,37 +72,20 @@ export async function run(argv) {
|
|
|
66
72
|
// (e.g. HTTP-only) then compile fine even if the connector cache isn't built.
|
|
67
73
|
// A connector action with no library resolves to a clear error in serialize().
|
|
68
74
|
//
|
|
69
|
-
// Loaded BEFORE check() on purpose: one of check's rules (whether a connector
|
|
70
|
-
// event operation declares any event parameters) can only be answered from the
|
|
71
|
-
// library, and answering it from an assumption instead is what made
|
|
72
|
-
// EVENT_NO_SCOPE an error for HTTP Webhook, whose only correct `where` is empty.
|
|
73
75
|
const library = openCliLibrary(argv);
|
|
74
76
|
const bindFile = flagValue(argv, '--bindings') ?? defaultBindingsFile();
|
|
75
77
|
const bindings = new Bindings(bindFile);
|
|
76
78
|
// One list per run; every exit path reports it (reportResult overwrites keys).
|
|
77
79
|
const diagnostics = [];
|
|
78
|
-
if (!noCheck) {
|
|
79
|
-
// Same options as `flow check`, so the two cannot disagree.
|
|
80
|
-
const diags = check(built, flowCheckOpts(library, bindings));
|
|
81
|
-
const errors = diags.filter((d) => d.level === 'error');
|
|
82
|
-
for (const d of diags)
|
|
83
|
-
console.error(` ${diagnosticLine(d)}`);
|
|
84
|
-
diagnostics.push(...diags);
|
|
85
|
-
if (errors.length) {
|
|
86
|
-
reportDiagnostics(diagnostics);
|
|
87
|
-
console.error(`compile: ${errors.length} error(s); not emitting.`);
|
|
88
|
-
return 1;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
80
|
// The serializer's own observations — a connection or folder value that can
|
|
92
|
-
// never be a tenant resource key
|
|
93
|
-
//
|
|
94
|
-
//
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
-
//
|
|
99
|
-
//
|
|
81
|
+
// never be a tenant resource key, a draft node written from its carried
|
|
82
|
+
// definition — printed in the same `⚠ [CODE]` shape as check()'s. Warnings are
|
|
83
|
+
// never fatal: the .flow is still written, exit stays 0. A binding LABEL the
|
|
84
|
+
// serializer refuses (its own name as the resourceKey, or a label a populated
|
|
85
|
+
// bindings.json does not declare) is an error, not a warning, and so is a
|
|
86
|
+
// BYOA-only event on a connection `prepare` never checked
|
|
87
|
+
// (EVENT_BYOA_UNVERIFIED): printed in the same `✗ [CODE]` shape `check` uses
|
|
88
|
+
// for it. Nothing is written.
|
|
100
89
|
const out = flagValue(argv, '-o') ?? `${built.name || basename(file).replace(/\.flow\.ts$|\.ts$/, '')}.flow`;
|
|
101
90
|
const outDir = dirname(resolve(out));
|
|
102
91
|
// ONE FOLDER, ONE FLOW (#1184). An agent's `source` is the name of the folder
|
|
@@ -147,28 +136,37 @@ export async function run(argv) {
|
|
|
147
136
|
const warnings = [];
|
|
148
137
|
let emitted;
|
|
149
138
|
try {
|
|
150
|
-
emitted = serializeWithSidecars(built, {
|
|
139
|
+
emitted = serializeWithSidecars(built, {
|
|
140
|
+
library, bindings, warn: (w) => warnings.push(w), agentSource,
|
|
141
|
+
carriedDrafts: (d) => warnings.push(draftWarning(d)),
|
|
142
|
+
});
|
|
151
143
|
}
|
|
152
144
|
catch (error) {
|
|
153
145
|
for (const w of warnings)
|
|
154
|
-
console.error(`
|
|
146
|
+
console.error(` ${diagnosticLine(serializeWarningDiagnostic(w))}`);
|
|
155
147
|
diagnostics.push(...warnings.map(serializeWarningDiagnostic));
|
|
156
|
-
if (!(error instanceof
|
|
157
|
-
// Any other throw still ends the run through the
|
|
158
|
-
// `error`); the list is reported first.
|
|
148
|
+
if (!(error instanceof CompileRefusal)) {
|
|
149
|
+
// Any other throw (an internal SDK error) still ends the run through the
|
|
150
|
+
// caller (exit 1, `--json` `error`); the list is reported first.
|
|
159
151
|
diagnostics.push(thrownDiagnostic(error));
|
|
160
152
|
reportDiagnostics(diagnostics);
|
|
161
153
|
throw error;
|
|
162
154
|
}
|
|
163
|
-
|
|
164
|
-
|
|
155
|
+
// A coded refusal: the serializer cannot write a correct `.flow`. Printed in
|
|
156
|
+
// `check`'s `✗ [CODE]` shape, which `check` reports for it too.
|
|
157
|
+
const refused = {
|
|
158
|
+
level: 'error', code: error.code, message: error.message,
|
|
159
|
+
...(error.suggestion !== undefined ? { suggestion: error.suggestion } : {}),
|
|
160
|
+
};
|
|
161
|
+
console.error(` ${diagnosticLine(refused)}`);
|
|
162
|
+
diagnostics.push(refused);
|
|
165
163
|
reportDiagnostics(diagnostics);
|
|
166
164
|
console.error('compile: 1 error(s); not emitting.');
|
|
167
165
|
return 1;
|
|
168
166
|
}
|
|
169
167
|
const { flow: flowJson, sidecars } = emitted;
|
|
170
168
|
for (const w of warnings)
|
|
171
|
-
console.error(`
|
|
169
|
+
console.error(` ${diagnosticLine(serializeWarningDiagnostic(w))}`);
|
|
172
170
|
diagnostics.push(...warnings.map(serializeWarningDiagnostic));
|
|
173
171
|
// An inline agent is TWO artifacts: the node, and the agent's own configuration
|
|
174
172
|
// at `<source>/agent.json` — which is where flow-v1's serializer points the
|
|
@@ -325,7 +323,19 @@ export async function run(argv) {
|
|
|
325
323
|
return 0;
|
|
326
324
|
}
|
|
327
325
|
function serializeWarningDiagnostic(w) {
|
|
328
|
-
return { level: 'warning', code: w.code, message: w.message };
|
|
326
|
+
return { level: 'warning', code: w.code, message: w.message, ...(w.step !== undefined ? { step: w.step } : {}) };
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* A draft node written from its carried definition, as a warning: the node is
|
|
330
|
+
* in the `.flow` as the canvas saved it, unfinished. For instance
|
|
331
|
+
* `⚠ [BINDING_MISSING] … written as a draft from its carried definition`.
|
|
332
|
+
*/
|
|
333
|
+
function draftWarning(d) {
|
|
334
|
+
return {
|
|
335
|
+
code: String(d.code),
|
|
336
|
+
message: `${d.message} Written as a draft, with the definition the source carries; finish the node before the flow runs.`,
|
|
337
|
+
...(d.step !== undefined ? { step: d.step } : {}),
|
|
338
|
+
};
|
|
329
339
|
}
|
|
330
340
|
runWhenInvokedDirectly(import.meta.url, 'compile', run);
|
|
331
341
|
/** A JSON file's contents, or `undefined` when it is missing or unreadable. */
|
|
@@ -98,6 +98,26 @@ export declare function readEventFilter(leaf: unknown): {
|
|
|
98
98
|
keys: EventFilterOperatorKey[];
|
|
99
99
|
read?: ReadEventFilter;
|
|
100
100
|
};
|
|
101
|
+
/** A code `check` gives a filter leaf it refuses (`core/event-checks.ts`). */
|
|
102
|
+
export type EventFilterLeafCode = 'EVENT_FILTER_NO_FIELD' | 'EVENT_FILTER_MANY_OPERATORS' | 'EVENT_FILTER_NO_VALUE' | 'EVENT_FILTER_BACKSLASH' | 'EVENT_FILTER_BAD_VALUE';
|
|
103
|
+
/**
|
|
104
|
+
* Read an authored filter leaf and judge it the way `check` does: the first
|
|
105
|
+
* code `core/event-checks.ts` gives the leaf, in the same order, or the read
|
|
106
|
+
* condition when the leaf lowers. A serializer that refuses a bad leaf with one
|
|
107
|
+
* message calls this, so its code cannot drift from check's.
|
|
108
|
+
*
|
|
109
|
+
* @example
|
|
110
|
+
* readEventFilterLeaf({ field: 'subject', contains: 'Report' }) // { read: { field: 'subject', operator: 'contains', value: 'Report' } }
|
|
111
|
+
* readEventFilterLeaf({ contains: 'Report' }) // { code: 'EVENT_FILTER_NO_FIELD', read: { field: '', ... } }
|
|
112
|
+
*/
|
|
113
|
+
export declare function readEventFilterLeaf(leaf: unknown): {
|
|
114
|
+
code?: undefined;
|
|
115
|
+
read: ReadEventFilter;
|
|
116
|
+
} | {
|
|
117
|
+
code: EventFilterLeafCode;
|
|
118
|
+
read?: ReadEventFilter;
|
|
119
|
+
problem?: string;
|
|
120
|
+
};
|
|
101
121
|
export declare function isIsoDateString(value: unknown): value is string;
|
|
102
122
|
/**
|
|
103
123
|
* Why a filter cannot be lowered, or `undefined` when it can. Two shapes are
|
|
@@ -81,6 +81,33 @@ export function readEventFilter(leaf) {
|
|
|
81
81
|
return { keys };
|
|
82
82
|
return { keys, read: { field: String(f.field ?? ''), operator: keys[0], value } };
|
|
83
83
|
}
|
|
84
|
+
/**
|
|
85
|
+
* Read an authored filter leaf and judge it the way `check` does: the first
|
|
86
|
+
* code `core/event-checks.ts` gives the leaf, in the same order, or the read
|
|
87
|
+
* condition when the leaf lowers. A serializer that refuses a bad leaf with one
|
|
88
|
+
* message calls this, so its code cannot drift from check's.
|
|
89
|
+
*
|
|
90
|
+
* @example
|
|
91
|
+
* readEventFilterLeaf({ field: 'subject', contains: 'Report' }) // { read: { field: 'subject', operator: 'contains', value: 'Report' } }
|
|
92
|
+
* readEventFilterLeaf({ contains: 'Report' }) // { code: 'EVENT_FILTER_NO_FIELD', read: { field: '', ... } }
|
|
93
|
+
*/
|
|
94
|
+
export function readEventFilterLeaf(leaf) {
|
|
95
|
+
const { keys, read } = readEventFilter(leaf);
|
|
96
|
+
const field = leaf?.field;
|
|
97
|
+
if (typeof field !== 'string' || field === '')
|
|
98
|
+
return { code: 'EVENT_FILTER_NO_FIELD', read };
|
|
99
|
+
if (keys.length > 1)
|
|
100
|
+
return { code: 'EVENT_FILTER_MANY_OPERATORS' };
|
|
101
|
+
if (!read)
|
|
102
|
+
return { code: 'EVENT_FILTER_NO_VALUE' };
|
|
103
|
+
const problem = eventFilterProblem(read);
|
|
104
|
+
const ambiguous = typeof read.value === 'string' ? jmesStringProblem(read.value) : undefined;
|
|
105
|
+
if (ambiguous)
|
|
106
|
+
return { code: 'EVENT_FILTER_BACKSLASH', read, problem: problem ?? ambiguous };
|
|
107
|
+
if (problem)
|
|
108
|
+
return { code: 'EVENT_FILTER_BAD_VALUE', read, problem };
|
|
109
|
+
return { read };
|
|
110
|
+
}
|
|
84
111
|
/** An ISO-8601 calendar date, optionally with a time part — what the designer serializes a date filter as. */
|
|
85
112
|
const ISO_DATE = /^\d{4}-\d{2}-\d{2}(?:[T ]\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:?\d{2})?)?$/;
|
|
86
113
|
export function isIsoDateString(value) {
|