@tenonhq/dovetail-servicenow 0.0.43 → 0.0.45
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.
|
@@ -14,6 +14,21 @@
|
|
|
14
14
|
* ├─ sys_hub_step_ext_input (model_id → step)
|
|
15
15
|
* └─ sys_hub_step_ext_output (model_id → step)
|
|
16
16
|
*
|
|
17
|
+
* Plus two stores the action's IO depends on that are NOT FK children
|
|
18
|
+
* (verified live on tenonworkstudio 2026-09-30):
|
|
19
|
+
*
|
|
20
|
+
* sys_element_mapping output → step-output wiring, one row per mapped output
|
|
21
|
+
* (incl. __action_status__ / __dont_treat_as_error__):
|
|
22
|
+
* id = action sys_id, table = var__m_sys_hub_action_output_<action>,
|
|
23
|
+
* field = output element, value = the pill. Without these a
|
|
24
|
+
* clone publishes with every output unmapped (runs, returns empty).
|
|
25
|
+
* sys_documentation IO labels: name = var__m_sys_hub_action_{input,output}_<action>,
|
|
26
|
+
* element, label, language. Without these Flow Designer shows the
|
|
27
|
+
* element name as the label.
|
|
28
|
+
*
|
|
29
|
+
* Action IO rows are var_dictionary rows whose `name` IS their var__m table, so
|
|
30
|
+
* the clone retargets `name` onto the new action's var__m table too.
|
|
31
|
+
*
|
|
17
32
|
* Publish reuses the proven snapshot path (publishActionType): the SOURCE
|
|
18
33
|
* action's step graph is read from /step_instances, each step's `action` and
|
|
19
34
|
* `sys_id` are remapped onto the clone (old→new sys_id map), optional StepOps
|
|
@@ -77,6 +92,12 @@ export interface CloneActionTypePlan {
|
|
|
77
92
|
sourceScopeSysId: string;
|
|
78
93
|
/** Old → new sys_id for every cloned step instance. */
|
|
79
94
|
stepIdMap: Record<string, string>;
|
|
95
|
+
/**
|
|
96
|
+
* The sys_documentation (label) ops inside `ops`. They are UPSERTED after the
|
|
97
|
+
* graph is written (the dictionary insert may already have created the row),
|
|
98
|
+
* not run through executeWritePlan.
|
|
99
|
+
*/
|
|
100
|
+
labelOpIds: Array<string>;
|
|
80
101
|
}
|
|
81
102
|
export interface CloneActionTypeStepReport {
|
|
82
103
|
/** Step summary of the cloned graph before stepOps. */
|
|
@@ -99,6 +120,10 @@ export interface CloneActionTypeResult {
|
|
|
99
120
|
verify?: VerifyStepsResult;
|
|
100
121
|
steps?: CloneActionTypeStepReport;
|
|
101
122
|
}
|
|
123
|
+
/** var_dictionary table name for a model's variables: var__m_<table>_<model sys_id>. */
|
|
124
|
+
export declare function varTableName(table: string, modelSysId: string): string;
|
|
125
|
+
/** The encoded query for an action's output → step-output mappings. */
|
|
126
|
+
export declare function outputMappingQuery(actionSysId: string): string;
|
|
102
127
|
/** Slug for internal_name: lowercase, runs of non-alphanumerics → "_", trimmed of "_". */
|
|
103
128
|
export declare function slugInternalName(name: string): string;
|
|
104
129
|
/**
|
|
@@ -117,3 +142,18 @@ export declare function resolveScope(client: ServiceNowClient, ref: string): Pro
|
|
|
117
142
|
*/
|
|
118
143
|
export declare function remapClonedSteps(steps: Array<StepRecord>, newParentSysId: string, stepIdMap: Record<string, string>, idMap: Record<string, string>): Array<StepRecord>;
|
|
119
144
|
export declare function cloneActionType(opts: CloneActionTypeParams): Promise<CloneActionTypeResult>;
|
|
145
|
+
/**
|
|
146
|
+
* Set `name` on a cloned var_dictionary row (action IO / step ext IO) to the
|
|
147
|
+
* clone's var__m table — but only when it named the SOURCE model's var__m table
|
|
148
|
+
* exactly; anything else is left as it was.
|
|
149
|
+
*/
|
|
150
|
+
export declare function retargetVarName(fields: Record<string, unknown>, table: string, oldModelSysId: string, newModelSysId: string): void;
|
|
151
|
+
/**
|
|
152
|
+
* Compare the source action's output mappings with the clone's, by `field`.
|
|
153
|
+
* A source field absent on the clone fails; a differing value is noted but does
|
|
154
|
+
* not fail (publish may legitimately normalise a pill).
|
|
155
|
+
*/
|
|
156
|
+
export declare function verifyOutputMappings(sourceRows: Array<Record<string, unknown>>, cloneRows: Array<Record<string, unknown>>): {
|
|
157
|
+
ok: boolean;
|
|
158
|
+
notes: Array<string>;
|
|
159
|
+
};
|
|
@@ -15,6 +15,21 @@
|
|
|
15
15
|
* ├─ sys_hub_step_ext_input (model_id → step)
|
|
16
16
|
* └─ sys_hub_step_ext_output (model_id → step)
|
|
17
17
|
*
|
|
18
|
+
* Plus two stores the action's IO depends on that are NOT FK children
|
|
19
|
+
* (verified live on tenonworkstudio 2026-09-30):
|
|
20
|
+
*
|
|
21
|
+
* sys_element_mapping output → step-output wiring, one row per mapped output
|
|
22
|
+
* (incl. __action_status__ / __dont_treat_as_error__):
|
|
23
|
+
* id = action sys_id, table = var__m_sys_hub_action_output_<action>,
|
|
24
|
+
* field = output element, value = the pill. Without these a
|
|
25
|
+
* clone publishes with every output unmapped (runs, returns empty).
|
|
26
|
+
* sys_documentation IO labels: name = var__m_sys_hub_action_{input,output}_<action>,
|
|
27
|
+
* element, label, language. Without these Flow Designer shows the
|
|
28
|
+
* element name as the label.
|
|
29
|
+
*
|
|
30
|
+
* Action IO rows are var_dictionary rows whose `name` IS their var__m table, so
|
|
31
|
+
* the clone retargets `name` onto the new action's var__m table too.
|
|
32
|
+
*
|
|
18
33
|
* Publish reuses the proven snapshot path (publishActionType): the SOURCE
|
|
19
34
|
* action's step graph is read from /step_instances, each step's `action` and
|
|
20
35
|
* `sys_id` are remapped onto the clone (old→new sys_id map), optional StepOps
|
|
@@ -32,10 +47,14 @@
|
|
|
32
47
|
* docs/servicenow-flow-designer-headless-authoring.md.
|
|
33
48
|
*/
|
|
34
49
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
50
|
+
exports.varTableName = varTableName;
|
|
51
|
+
exports.outputMappingQuery = outputMappingQuery;
|
|
35
52
|
exports.slugInternalName = slugInternalName;
|
|
36
53
|
exports.resolveScope = resolveScope;
|
|
37
54
|
exports.remapClonedSteps = remapClonedSteps;
|
|
38
55
|
exports.cloneActionType = cloneActionType;
|
|
56
|
+
exports.retargetVarName = retargetVarName;
|
|
57
|
+
exports.verifyOutputMappings = verifyOutputMappings;
|
|
39
58
|
const shape_1 = require("./shape");
|
|
40
59
|
const writeOrder_1 = require("./writeOrder");
|
|
41
60
|
const actionTypeApi_1 = require("./actionTypeApi");
|
|
@@ -47,6 +66,16 @@ var OUTPUT_TABLE = "sys_hub_action_output";
|
|
|
47
66
|
var STEP_TABLE = "sys_hub_step_instance";
|
|
48
67
|
var EXT_INPUT_TABLE = "sys_hub_step_ext_input";
|
|
49
68
|
var EXT_OUTPUT_TABLE = "sys_hub_step_ext_output";
|
|
69
|
+
var MAPPING_TABLE = "sys_element_mapping";
|
|
70
|
+
var LABEL_TABLE = "sys_documentation";
|
|
71
|
+
/** var_dictionary table name for a model's variables: var__m_<table>_<model sys_id>. */
|
|
72
|
+
function varTableName(table, modelSysId) {
|
|
73
|
+
return "var__m_" + table + "_" + modelSysId;
|
|
74
|
+
}
|
|
75
|
+
/** The encoded query for an action's output → step-output mappings. */
|
|
76
|
+
function outputMappingQuery(actionSysId) {
|
|
77
|
+
return "id=" + actionSysId + "^tableSTARTSWITH" + "var__m_" + OUTPUT_TABLE + "_";
|
|
78
|
+
}
|
|
50
79
|
/** Rows per child query. A full page means the read may be truncated — refuse rather than half-clone. */
|
|
51
80
|
var CHILD_LIMIT = 1000;
|
|
52
81
|
/**
|
|
@@ -307,6 +336,10 @@ async function cloneActionType(opts) {
|
|
|
307
336
|
scope: scope.name,
|
|
308
337
|
},
|
|
309
338
|
];
|
|
339
|
+
// element → plan op id, per IO table, so label/mapping ops can depend on their row.
|
|
340
|
+
var ioOpByElement = {};
|
|
341
|
+
ioOpByElement[INPUT_TABLE] = {};
|
|
342
|
+
ioOpByElement[OUTPUT_TABLE] = {};
|
|
310
343
|
var pushModelChildren = function (table, prefix, rows) {
|
|
311
344
|
for (var r = 0; r < rows.length; r++) {
|
|
312
345
|
var src = flattenRow(rows[r]);
|
|
@@ -314,6 +347,11 @@ async function cloneActionType(opts) {
|
|
|
314
347
|
idMap[String(src.sys_id || "")] = childSysId;
|
|
315
348
|
var fields = cloneFields(src, scope.sysId, childSysId);
|
|
316
349
|
fields.model_id = newParentSysId;
|
|
350
|
+
retargetVarName(fields, table, opts.sourceSysId, newParentSysId);
|
|
351
|
+
var element = String(src.element || "");
|
|
352
|
+
if (element) {
|
|
353
|
+
ioOpByElement[table][element] = prefix + ":" + r;
|
|
354
|
+
}
|
|
317
355
|
ops.push({
|
|
318
356
|
id: prefix + ":" + r,
|
|
319
357
|
logicalName: table + ":" + String(src.element || src.name || src.sys_id),
|
|
@@ -356,6 +394,7 @@ async function cloneActionType(opts) {
|
|
|
356
394
|
idMap[String(srcExt.sys_id || "")] = newExtSysId;
|
|
357
395
|
var extFields = cloneFields(srcExt, scope.sysId, newExtSysId);
|
|
358
396
|
extFields.model_id = newStepSysId;
|
|
397
|
+
retargetVarName(extFields, extSpecs[x].table, oldStepSysId, newStepSysId);
|
|
359
398
|
ops.push({
|
|
360
399
|
id: stepOpId + ":" + extSpecs[x].prefix + ":" + e,
|
|
361
400
|
logicalName: extSpecs[x].table + ":" + String(srcExt.element || srcExt.name || srcExt.sys_id),
|
|
@@ -367,13 +406,78 @@ async function cloneActionType(opts) {
|
|
|
367
406
|
}
|
|
368
407
|
}
|
|
369
408
|
}
|
|
409
|
+
// 3b. Output → step-output wiring (sys_element_mapping). Step cids survive the
|
|
410
|
+
// clone, so field and value (the pill) carry over unchanged.
|
|
411
|
+
var newOutputVarTable = varTableName(OUTPUT_TABLE, newParentSysId);
|
|
412
|
+
var sourceMappings = await queryChildren(client, MAPPING_TABLE, outputMappingQuery(opts.sourceSysId));
|
|
413
|
+
for (var mi = 0; mi < sourceMappings.length; mi++) {
|
|
414
|
+
var srcMap = flattenRow(sourceMappings[mi]);
|
|
415
|
+
var mapField = String(srcMap.field || "");
|
|
416
|
+
var mapFields = cloneFields(srcMap, scope.sysId, (0, shape_1.generateSysId)());
|
|
417
|
+
mapFields.id = newParentSysId;
|
|
418
|
+
mapFields.table = newOutputVarTable;
|
|
419
|
+
// After the parent and its outputs (the matching output first), so a mapping never precedes the row it wires.
|
|
420
|
+
var mapDeps = ["parent"];
|
|
421
|
+
if (mapField && Object.prototype.hasOwnProperty.call(ioOpByElement[OUTPUT_TABLE], mapField)) {
|
|
422
|
+
mapDeps.push(ioOpByElement[OUTPUT_TABLE][mapField]);
|
|
423
|
+
}
|
|
424
|
+
var outputOpIds = Object.keys(ioOpByElement[OUTPUT_TABLE]).map(function (el) { return ioOpByElement[OUTPUT_TABLE][el]; });
|
|
425
|
+
for (var oi = 0; oi < outputOpIds.length; oi++) {
|
|
426
|
+
if (mapDeps.indexOf(outputOpIds[oi]) === -1) {
|
|
427
|
+
mapDeps.push(outputOpIds[oi]);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
ops.push({
|
|
431
|
+
id: "mapping:" + mi,
|
|
432
|
+
logicalName: MAPPING_TABLE + ":" + (mapField || String(srcMap.sys_id || mi)),
|
|
433
|
+
table: MAPPING_TABLE,
|
|
434
|
+
fields: mapFields,
|
|
435
|
+
dependsOn: mapDeps,
|
|
436
|
+
scope: scope.name,
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
// 3c. IO labels (sys_documentation keyed on the var__m table), retargeted.
|
|
440
|
+
var labelOps = [];
|
|
441
|
+
var labelSpecs = [
|
|
442
|
+
{ table: INPUT_TABLE, prefix: "label:input" },
|
|
443
|
+
{ table: OUTPUT_TABLE, prefix: "label:output" },
|
|
444
|
+
];
|
|
445
|
+
for (var li = 0; li < labelSpecs.length; li++) {
|
|
446
|
+
var spec = labelSpecs[li];
|
|
447
|
+
var srcVarTable = varTableName(spec.table, opts.sourceSysId);
|
|
448
|
+
var newVarTable = varTableName(spec.table, newParentSysId);
|
|
449
|
+
var labelRows = await queryChildren(client, LABEL_TABLE, "name=" + srcVarTable);
|
|
450
|
+
for (var lr = 0; lr < labelRows.length; lr++) {
|
|
451
|
+
var srcLabel = flattenRow(labelRows[lr]);
|
|
452
|
+
var labelElement = String(srcLabel.element || "");
|
|
453
|
+
if (!labelElement) {
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
456
|
+
var labelFields = cloneFields(srcLabel, scope.sysId, (0, shape_1.generateSysId)());
|
|
457
|
+
labelFields.name = newVarTable;
|
|
458
|
+
var labelDeps = ["parent"];
|
|
459
|
+
if (Object.prototype.hasOwnProperty.call(ioOpByElement[spec.table], labelElement)) {
|
|
460
|
+
labelDeps.push(ioOpByElement[spec.table][labelElement]);
|
|
461
|
+
}
|
|
462
|
+
labelOps.push({
|
|
463
|
+
id: spec.prefix + ":" + lr,
|
|
464
|
+
logicalName: LABEL_TABLE + ":" + newVarTable + "." + labelElement,
|
|
465
|
+
table: LABEL_TABLE,
|
|
466
|
+
fields: labelFields,
|
|
467
|
+
dependsOn: labelDeps,
|
|
468
|
+
scope: scope.name,
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
var allOps = ops.concat(labelOps);
|
|
370
473
|
var plan = {
|
|
371
|
-
ops:
|
|
372
|
-
counts: countByTable(
|
|
373
|
-
total:
|
|
474
|
+
ops: allOps,
|
|
475
|
+
counts: countByTable(allOps),
|
|
476
|
+
total: allOps.length,
|
|
374
477
|
scope: scope,
|
|
375
478
|
sourceScopeSysId: sourceScopeSysId,
|
|
376
479
|
stepIdMap: stepIdMap,
|
|
480
|
+
labelOpIds: labelOps.map(function (o) { return o.id; }),
|
|
377
481
|
};
|
|
378
482
|
// 4. The publishable step graph: the SOURCE's /step_instances, remapped.
|
|
379
483
|
var sourceSteps = await (0, actionTypeApi_1.fetchActionSteps)(client, opts.sourceSysId, sourceScopeSysId);
|
|
@@ -412,6 +516,11 @@ async function cloneActionType(opts) {
|
|
|
412
516
|
// 5. Write the graph, pinned to the update set.
|
|
413
517
|
var updateSetSysId = opts.updateSetSysId;
|
|
414
518
|
var written = await (0, writeOrder_1.executeWritePlan)(client, ops, updateSetSysId);
|
|
519
|
+
// Labels last: the IO dictionary inserts above may already have created a
|
|
520
|
+
// sys_documentation row for the new var__m table, so upsert rather than duplicate.
|
|
521
|
+
for (var lw = 0; lw < labelOps.length; lw++) {
|
|
522
|
+
written.push(await upsertLabel(client, labelOps[lw], updateSetSysId));
|
|
523
|
+
}
|
|
415
524
|
var result = {
|
|
416
525
|
sysId: newParentSysId,
|
|
417
526
|
internalName: internalName,
|
|
@@ -440,16 +549,129 @@ async function cloneActionType(opts) {
|
|
|
440
549
|
}
|
|
441
550
|
// 7. Verify — a 201 means "compiled", not "landed as sent". Read the clone's steps back.
|
|
442
551
|
var fresh = await (0, actionTypeApi_1.fetchActionSteps)(client, newParentSysId, scope.sysId);
|
|
552
|
+
var verify;
|
|
443
553
|
if (fresh.length === 0) {
|
|
444
|
-
|
|
554
|
+
verify = { ok: false, notes: ["read-back returned no steps — could not verify the publish"] };
|
|
445
555
|
}
|
|
446
556
|
else {
|
|
447
557
|
var expected = stepReport.after;
|
|
448
|
-
|
|
558
|
+
verify = (0, stepOps_1.verifySteps)(expected, (0, stepOps_1.summarizeSteps)(fresh), allCids(finalSteps));
|
|
449
559
|
if (fresh.length !== finalSteps.length) {
|
|
450
|
-
|
|
451
|
-
|
|
560
|
+
verify.ok = false;
|
|
561
|
+
verify.notes.push("step count on the instance is " + fresh.length + ", expected " + finalSteps.length);
|
|
452
562
|
}
|
|
453
563
|
}
|
|
564
|
+
// Output mappings: every source mapping must exist on the clone, or its outputs come back empty.
|
|
565
|
+
var cloneMappings = await queryChildren(client, MAPPING_TABLE, outputMappingQuery(newParentSysId));
|
|
566
|
+
var mappingCheck = verifyOutputMappings(sourceMappings, cloneMappings);
|
|
567
|
+
if (!mappingCheck.ok) {
|
|
568
|
+
verify.ok = false;
|
|
569
|
+
}
|
|
570
|
+
for (var vn = 0; vn < mappingCheck.notes.length; vn++) {
|
|
571
|
+
verify.notes.push(mappingCheck.notes[vn]);
|
|
572
|
+
}
|
|
573
|
+
result.verify = verify;
|
|
454
574
|
return result;
|
|
455
575
|
}
|
|
576
|
+
/**
|
|
577
|
+
* Set `name` on a cloned var_dictionary row (action IO / step ext IO) to the
|
|
578
|
+
* clone's var__m table — but only when it named the SOURCE model's var__m table
|
|
579
|
+
* exactly; anything else is left as it was.
|
|
580
|
+
*/
|
|
581
|
+
function retargetVarName(fields, table, oldModelSysId, newModelSysId) {
|
|
582
|
+
if (!Object.prototype.hasOwnProperty.call(fields, "name")) {
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
if (String(fields.name) === varTableName(table, oldModelSysId)) {
|
|
586
|
+
fields.name = varTableName(table, newModelSysId);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
/**
|
|
590
|
+
* Compare the source action's output mappings with the clone's, by `field`.
|
|
591
|
+
* A source field absent on the clone fails; a differing value is noted but does
|
|
592
|
+
* not fail (publish may legitimately normalise a pill).
|
|
593
|
+
*/
|
|
594
|
+
function verifyOutputMappings(sourceRows, cloneRows) {
|
|
595
|
+
var cloneByField = {};
|
|
596
|
+
for (var c = 0; c < cloneRows.length; c++) {
|
|
597
|
+
var cr = flattenRow(cloneRows[c]);
|
|
598
|
+
var cf = String(cr.field || "");
|
|
599
|
+
if (cf) {
|
|
600
|
+
cloneByField[cf] = String(cr.value === undefined || cr.value === null ? "" : cr.value);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
var missing = [];
|
|
604
|
+
var notes = [];
|
|
605
|
+
for (var s = 0; s < sourceRows.length; s++) {
|
|
606
|
+
var sr = flattenRow(sourceRows[s]);
|
|
607
|
+
var sf = String(sr.field || "");
|
|
608
|
+
if (!sf) {
|
|
609
|
+
continue;
|
|
610
|
+
}
|
|
611
|
+
if (!Object.prototype.hasOwnProperty.call(cloneByField, sf)) {
|
|
612
|
+
missing.push(sf);
|
|
613
|
+
continue;
|
|
614
|
+
}
|
|
615
|
+
var sv = String(sr.value === undefined || sr.value === null ? "" : sr.value);
|
|
616
|
+
if (cloneByField[sf] !== sv) {
|
|
617
|
+
notes.push("output mapping '" + sf + "' differs on the clone: '" + cloneByField[sf] + "' (source '" + sv + "')");
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
if (missing.length > 0) {
|
|
621
|
+
notes.unshift(missing.length + " output mapping(s) missing on the clone (" + MAPPING_TABLE + "): " + missing.join(", "));
|
|
622
|
+
}
|
|
623
|
+
else if (sourceRows.length > 0) {
|
|
624
|
+
notes.unshift("all " + sourceRows.length + " output mapping(s) present on the clone");
|
|
625
|
+
}
|
|
626
|
+
return { ok: missing.length === 0, notes: notes };
|
|
627
|
+
}
|
|
628
|
+
/**
|
|
629
|
+
* Write one cloned sys_documentation row: update the existing (name, element,
|
|
630
|
+
* language) row on the target if the platform already made one, else create it.
|
|
631
|
+
*/
|
|
632
|
+
async function upsertLabel(client, op, updateSetSysId) {
|
|
633
|
+
var f = op.fields;
|
|
634
|
+
var keyParts = [String(f.name), String(f.element || ""), String(f.language || "")];
|
|
635
|
+
for (var k = 0; k < keyParts.length; k++) {
|
|
636
|
+
if (/[\^\r\n]/.test(keyParts[k])) {
|
|
637
|
+
throw new Error("cloneActionType: label key " + JSON.stringify(keyParts[k]) + " contains '^' or a line break.");
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
var query = "name=" + keyParts[0] + "^element=" + keyParts[1];
|
|
641
|
+
if (keyParts[2] !== "") {
|
|
642
|
+
query += "^language=" + keyParts[2];
|
|
643
|
+
}
|
|
644
|
+
try {
|
|
645
|
+
var hits = await client.table.query(LABEL_TABLE, query, { limit: 1, fields: ["sys_id"] });
|
|
646
|
+
var existingSysId = hits.length > 0 ? String(flattenRow(hits[0]).sys_id || "") : "";
|
|
647
|
+
if (RX_SYS_ID.test(existingSysId)) {
|
|
648
|
+
var update = Object.assign({}, f);
|
|
649
|
+
delete update.sys_id;
|
|
650
|
+
await client.claude.pushWithUpdateSet({
|
|
651
|
+
update_set_sys_id: updateSetSysId,
|
|
652
|
+
table: LABEL_TABLE,
|
|
653
|
+
record_sys_id: existingSysId,
|
|
654
|
+
fields: update,
|
|
655
|
+
});
|
|
656
|
+
return { id: op.id, logicalName: op.logicalName, table: op.table, sysId: existingSysId, action: "updated" };
|
|
657
|
+
}
|
|
658
|
+
var created = await client.claude.createRecord({
|
|
659
|
+
table: op.table,
|
|
660
|
+
fields: f,
|
|
661
|
+
scope: op.scope,
|
|
662
|
+
update_set_sys_id: updateSetSysId,
|
|
663
|
+
sys_id: String(f.sys_id),
|
|
664
|
+
});
|
|
665
|
+
return {
|
|
666
|
+
id: op.id,
|
|
667
|
+
logicalName: op.logicalName,
|
|
668
|
+
table: op.table,
|
|
669
|
+
sysId: created.sys_id ? String(created.sys_id) : String(f.sys_id),
|
|
670
|
+
action: "created",
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
catch (err) {
|
|
674
|
+
var msg = err instanceof Error ? err.message : String(err);
|
|
675
|
+
throw new Error("cloneActionType: label write failed at op '" + op.id + "' (" + op.table + "): " + msg);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
@@ -301,5 +301,6 @@ export interface PlanActionDefinitionParams {
|
|
|
301
301
|
* before/after views; throws on any invalid or dangling reference.
|
|
302
302
|
*/
|
|
303
303
|
export declare function planActionDefinition(params: PlanActionDefinitionParams): DefineActionPlan;
|
|
304
|
+
export declare function compareForVerify(expected: ActionView, actual: ActionView): DefineActionVerify;
|
|
304
305
|
export declare function defineActionType(params: DefineActionTypeParams): Promise<DefineActionTypeResult>;
|
|
305
306
|
export {};
|
|
@@ -50,6 +50,7 @@ exports.canonValue = canonValue;
|
|
|
50
50
|
exports.viewAction = viewAction;
|
|
51
51
|
exports.diffViews = diffViews;
|
|
52
52
|
exports.planActionDefinition = planActionDefinition;
|
|
53
|
+
exports.compareForVerify = compareForVerify;
|
|
53
54
|
exports.defineActionType = defineActionType;
|
|
54
55
|
const crypto_1 = require("crypto");
|
|
55
56
|
const actionTypeApi_1 = require("./actionTypeApi");
|
|
@@ -1547,9 +1548,28 @@ async function resolveAliasDisplays(client, ids) {
|
|
|
1547
1548
|
}
|
|
1548
1549
|
return out;
|
|
1549
1550
|
}
|
|
1551
|
+
// Variable attributes the server assigns a default to on save when the spec left
|
|
1552
|
+
// them unset (live-observed 2026-09-30: a new boolean input came back with
|
|
1553
|
+
// maxLength 40). A blank expected value for one of these is "unspecified", not a
|
|
1554
|
+
// mismatch; an explicitly specified value is still verified.
|
|
1555
|
+
var SERVER_DEFAULTED_FIELDS = ["maxLength"];
|
|
1556
|
+
function dropServerDefaults(changed) {
|
|
1557
|
+
return changed
|
|
1558
|
+
.map(function (c) {
|
|
1559
|
+
var kept = c.changes.filter(function (f) {
|
|
1560
|
+
return !(SERVER_DEFAULTED_FIELDS.indexOf(f.field) !== -1 && f.before === "");
|
|
1561
|
+
});
|
|
1562
|
+
return Object.assign({}, c, { changes: kept });
|
|
1563
|
+
})
|
|
1564
|
+
.filter(function (c) {
|
|
1565
|
+
return c.changes.length > 0;
|
|
1566
|
+
});
|
|
1567
|
+
}
|
|
1550
1568
|
function compareForVerify(expected, actual) {
|
|
1551
1569
|
var notes = [];
|
|
1552
1570
|
var d = diffViews(expected, actual, {});
|
|
1571
|
+
d.inputs.changed = dropServerDefaults(d.inputs.changed);
|
|
1572
|
+
d.outputs.changed = dropServerDefaults(d.outputs.changed);
|
|
1553
1573
|
// Expected -> actual: any difference means the save did not land as planned.
|
|
1554
1574
|
d.action.forEach(function (c) {
|
|
1555
1575
|
notes.push("action." + c.field + " is '" + c.after + "', expected '" + c.before + "'");
|
|
@@ -1585,10 +1605,13 @@ function compareForVerify(expected, actual) {
|
|
|
1585
1605
|
d.steps.changed.forEach(function (s) {
|
|
1586
1606
|
notes.push("step '" + s.label + "': " + s.changes.join("; "));
|
|
1587
1607
|
});
|
|
1588
|
-
|
|
1608
|
+
// ok is derived from the filtered notes, not d.empty: server-defaulted fields
|
|
1609
|
+
// were dropped above and must not fail the verify.
|
|
1610
|
+
var ok = notes.length === 0;
|
|
1611
|
+
if (ok) {
|
|
1589
1612
|
notes.push("read-back matches the planned definition");
|
|
1590
1613
|
}
|
|
1591
|
-
return { ok:
|
|
1614
|
+
return { ok: ok, notes: notes };
|
|
1592
1615
|
}
|
|
1593
1616
|
async function defineActionType(params) {
|
|
1594
1617
|
var client = params.client;
|
|
@@ -36,8 +36,12 @@ export interface WriteOpResult {
|
|
|
36
36
|
logicalName: string;
|
|
37
37
|
table: string;
|
|
38
38
|
sysId: string;
|
|
39
|
-
/**
|
|
40
|
-
|
|
39
|
+
/**
|
|
40
|
+
* executeWritePlan always reports "created" — claude.createRecord doesn't
|
|
41
|
+
* distinguish. "updated" is reported by callers that upsert (cloneActionType's
|
|
42
|
+
* label rows, which the platform may already have created on the dictionary insert).
|
|
43
|
+
*/
|
|
44
|
+
action: "created" | "updated";
|
|
41
45
|
}
|
|
42
46
|
export declare class WriteOrderError extends Error {
|
|
43
47
|
readonly cycleIds?: Array<string> | undefined;
|