@tenonhq/sincronia-core 0.0.82 → 0.0.84
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/allScopesCommands.js +4 -4
- package/dist/appUtils.js +94 -19
- package/dist/config.js +33 -2
- package/dist/flowDesigner/values.js +55 -0
- package/dist/index.js +15 -4
- package/dist/tests/pushFiles.test.js +83 -0
- package/dist/tests/resolveConfigReadOnly.test.js +67 -0
- package/dist/tests/v2Values.test.js +79 -0
- package/dist/updateSetCommands.js +3 -3
- package/package.json +2 -2
|
@@ -90,7 +90,7 @@ async function processManifestForScope(manifest, sourceDirectory, forceWrite = f
|
|
|
90
90
|
const recordDirName = record.name || recordName;
|
|
91
91
|
const recordPath = path.join(tablePath, recordDirName);
|
|
92
92
|
// Check if metadata file exists in the files from server
|
|
93
|
-
const hasMetadataFromServer = record.files
|
|
93
|
+
const hasMetadataFromServer = record.files && record.files.some((f) => f.name === 'metaData' && f.type === 'json');
|
|
94
94
|
// Ensure the record directory exists
|
|
95
95
|
await fsp.mkdir(recordPath, { recursive: true });
|
|
96
96
|
// Process each file in the record
|
|
@@ -243,7 +243,7 @@ async function processScope(scopeName, scopeConfig, apiDelay = 0) {
|
|
|
243
243
|
}
|
|
244
244
|
}
|
|
245
245
|
}
|
|
246
|
-
const tableCount = Object.keys(manifest
|
|
246
|
+
const tableCount = Object.keys((manifest && manifest.tables) || {}).length;
|
|
247
247
|
Logger_1.logger.info("Writing " + tableCount + " tables for " + scopeName + "...");
|
|
248
248
|
await processManifestForScope(manifest, sourceDirectory, true);
|
|
249
249
|
// Create the scope-specific manifest structure
|
|
@@ -325,8 +325,8 @@ async function initScopesCommand(args) {
|
|
|
325
325
|
}
|
|
326
326
|
else {
|
|
327
327
|
failCount++;
|
|
328
|
-
const error = result.status === "rejected" ? result.reason : result.value
|
|
329
|
-
Logger_1.logger.error(`Failed to process ${scopeName}: ${error
|
|
328
|
+
const error = result.status === "rejected" ? result.reason : (result.value && result.value.error);
|
|
329
|
+
Logger_1.logger.error(`Failed to process ${scopeName}: ${(error && error.message) || "Unknown error"}`);
|
|
330
330
|
}
|
|
331
331
|
});
|
|
332
332
|
// Write per-scope manifest files instead of a single combined one
|
package/dist/appUtils.js
CHANGED
|
@@ -61,19 +61,46 @@ const getUpdateSetConfig = () => {
|
|
|
61
61
|
}
|
|
62
62
|
return {};
|
|
63
63
|
};
|
|
64
|
+
// Merge _lastUpdatedOn into the server-provided metadata content. Preserves all
|
|
65
|
+
// record fields (sys_id, sys_scope, field value/display_value pairs, etc.) so
|
|
66
|
+
// the local metaData.json is a full snapshot of the record, not just a stub.
|
|
67
|
+
const stampMetadataContent = (file) => {
|
|
68
|
+
if (file.name !== "metaData" || file.type !== "json")
|
|
69
|
+
return file;
|
|
70
|
+
const stamp = new Date().toISOString();
|
|
71
|
+
if (!file.content) {
|
|
72
|
+
return { ...file, content: JSON.stringify({ _lastUpdatedOn: stamp }, null, 2) };
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
const metadata = JSON.parse(file.content);
|
|
76
|
+
if (metadata.sys_updated_on && metadata.sys_updated_on.value) {
|
|
77
|
+
metadata._lastUpdatedOn = metadata.sys_updated_on.value;
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
metadata._lastUpdatedOn = stamp;
|
|
81
|
+
}
|
|
82
|
+
return { ...file, content: JSON.stringify(metadata, null, 2) };
|
|
83
|
+
}
|
|
84
|
+
catch (e) {
|
|
85
|
+
// Content isn't JSON — leave as-is, it will be written verbatim.
|
|
86
|
+
return file;
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
const hasServerMetadata = (files) => files.some((f) => f.name === "metaData" && f.type === "json" && !!f.content);
|
|
64
90
|
const processFilesInManRec = async (recPath, rec, forceWrite) => {
|
|
65
91
|
FileLogger_1.fileLogger.debug("Processing record: " + rec.name + " (" + rec.files.length + " files)");
|
|
66
92
|
const fileWrite = fUtils.writeSNFileCurry(forceWrite);
|
|
67
|
-
//
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
93
|
+
// If the server did not provide a metadata file, fall back to a timestamp-only
|
|
94
|
+
// stub so the record directory always has a metaData.json.
|
|
95
|
+
if (!hasServerMetadata(rec.files)) {
|
|
96
|
+
const stubMetadata = {
|
|
97
|
+
name: "metaData",
|
|
98
|
+
type: "json",
|
|
99
|
+
content: JSON.stringify({ _lastUpdatedOn: new Date().toISOString() }, null, 2),
|
|
100
|
+
};
|
|
101
|
+
await fileWrite(stubMetadata, recPath);
|
|
102
|
+
}
|
|
103
|
+
const writeResults = await (0, genericUtils_1.allSettledBatched)(rec.files, constants_1.CONCURRENCY_FILES, function (file) { return fileWrite(stampMetadataContent(file), recPath); });
|
|
77
104
|
const writeFailures = writeResults.filter((r) => r.status === "rejected");
|
|
78
105
|
if (writeFailures.length > 0) {
|
|
79
106
|
writeFailures.forEach((f) => {
|
|
@@ -461,7 +488,22 @@ const refreshAllFiles = async (newManifest, sourcePath, options = {}) => {
|
|
|
461
488
|
await (0, genericUtils_1.processBatched)(recKeys, constants_1.CONCURRENCY_RECORDS, async function (recKey) {
|
|
462
489
|
var rec = recs[recKey];
|
|
463
490
|
var recPath = path_1.default.join(tablePath, rec.name);
|
|
464
|
-
|
|
491
|
+
// Split server-provided metadata off from the regular files so we can
|
|
492
|
+
// track whether any regular file actually changed — metaData shouldn't
|
|
493
|
+
// be the trigger for "this record changed" since we stamp it on every
|
|
494
|
+
// touch.
|
|
495
|
+
var metadataFiles = [];
|
|
496
|
+
var regularFiles = [];
|
|
497
|
+
for (var mi = 0; mi < rec.files.length; mi++) {
|
|
498
|
+
var rf = rec.files[mi];
|
|
499
|
+
if (rf.name === "metaData" && rf.type === "json") {
|
|
500
|
+
metadataFiles.push(rf);
|
|
501
|
+
}
|
|
502
|
+
else {
|
|
503
|
+
regularFiles.push(rf);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
var results = await (0, genericUtils_1.allSettledBatched)(regularFiles, constants_1.CONCURRENCY_FILES, async function (file) {
|
|
465
507
|
if (forceWrite) {
|
|
466
508
|
await forceWriter(file, recPath);
|
|
467
509
|
return true;
|
|
@@ -483,15 +525,23 @@ const refreshAllFiles = async (newManifest, sourcePath, options = {}) => {
|
|
|
483
525
|
unchangedCount++;
|
|
484
526
|
}
|
|
485
527
|
}
|
|
486
|
-
// Only touch metaData when at least one file in the record
|
|
487
|
-
// changed. Avoids rewriting _lastUpdatedOn for records that
|
|
488
|
-
// already in sync with the instance.
|
|
528
|
+
// Only touch metaData when at least one regular file in the record
|
|
529
|
+
// actually changed. Avoids rewriting _lastUpdatedOn for records that
|
|
530
|
+
// were already in sync with the instance. Prefer the server-provided
|
|
531
|
+
// metadata (full field snapshot) over a stub; fall back to a stub only
|
|
532
|
+
// when the server didn't send metadata at all.
|
|
489
533
|
if (anyChanged || forceWrite) {
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
534
|
+
let metadataFile;
|
|
535
|
+
if (metadataFiles.length > 0 && metadataFiles[0].content) {
|
|
536
|
+
metadataFile = stampMetadataContent(metadataFiles[0]);
|
|
537
|
+
}
|
|
538
|
+
else {
|
|
539
|
+
metadataFile = {
|
|
540
|
+
name: "metaData",
|
|
541
|
+
type: "json",
|
|
542
|
+
content: JSON.stringify({ _lastUpdatedOn: new Date().toISOString() }, null, 2),
|
|
543
|
+
};
|
|
544
|
+
}
|
|
495
545
|
await forceWriter(metadataFile, recPath);
|
|
496
546
|
}
|
|
497
547
|
// Strip content from manifest entries to keep memory bounded.
|
|
@@ -665,12 +715,37 @@ const pushFiles = async (recs) => {
|
|
|
665
715
|
.join(", ");
|
|
666
716
|
Logger_1.logger.info(`Update set routing active: ${activeScopes}`);
|
|
667
717
|
}
|
|
718
|
+
// Pre-resolve read-only table sets for every scope present in this batch.
|
|
719
|
+
// Cheaper than re-resolving per record. Tables flagged in _readOnlyTables
|
|
720
|
+
// are pulled normally but pushes are skipped.
|
|
721
|
+
const readOnlyByScope = {};
|
|
722
|
+
for (const rec of recs) {
|
|
723
|
+
const fieldNames = Object.keys(rec.fields);
|
|
724
|
+
if (fieldNames.length === 0)
|
|
725
|
+
continue;
|
|
726
|
+
const scope = rec.fields[fieldNames[0]].scope;
|
|
727
|
+
if (scope && !readOnlyByScope[scope]) {
|
|
728
|
+
readOnlyByScope[scope] = ConfigManager.getReadOnlyTablesForScope(scope);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
const announcedSkipTables = {};
|
|
668
732
|
const tick = getProgTick(Logger_1.logger.getLogLevel(), recs.length * 2) || (() => { });
|
|
669
733
|
const results = await (0, genericUtils_1.allSettledBatched)(recs, constants_1.CONCURRENCY_PUSH, async function (rec) {
|
|
670
734
|
const fieldNames = Object.keys(rec.fields);
|
|
671
735
|
const firstField = rec.fields[fieldNames[0]];
|
|
672
736
|
const recSummary = (0, exports.summarizeRecord)(rec.table, firstField.name);
|
|
673
737
|
const scope = firstField.scope;
|
|
738
|
+
const readOnlySet = scope ? readOnlyByScope[scope] : undefined;
|
|
739
|
+
if (readOnlySet && readOnlySet.has(rec.table)) {
|
|
740
|
+
tick();
|
|
741
|
+
tick();
|
|
742
|
+
const skipKey = `${scope}:${rec.table}`;
|
|
743
|
+
if (!announcedSkipTables[skipKey]) {
|
|
744
|
+
announcedSkipTables[skipKey] = true;
|
|
745
|
+
Logger_1.logger.info(`Read-only table ${rec.table} in scope ${scope}: push skipped`);
|
|
746
|
+
}
|
|
747
|
+
return { success: true, message: `${recSummary} : skipped (read-only table)` };
|
|
748
|
+
}
|
|
674
749
|
const buildRes = await buildRec(rec);
|
|
675
750
|
tick();
|
|
676
751
|
if (!buildRes.success) {
|
package/dist/config.js
CHANGED
|
@@ -61,6 +61,7 @@ exports.resolveScopeFromPath = resolveScopeFromPath;
|
|
|
61
61
|
exports.isDirectiveKey = isDirectiveKey;
|
|
62
62
|
exports.stripDirectiveKeys = stripDirectiveKeys;
|
|
63
63
|
exports.resolveConfigForScope = resolveConfigForScope;
|
|
64
|
+
exports.getReadOnlyTablesForScope = getReadOnlyTablesForScope;
|
|
64
65
|
const path_1 = __importDefault(require("path"));
|
|
65
66
|
const fs_1 = require("fs");
|
|
66
67
|
const Logger_1 = require("./Logger");
|
|
@@ -559,8 +560,8 @@ function stripDirectiveKeys(obj) {
|
|
|
559
560
|
}
|
|
560
561
|
return result;
|
|
561
562
|
}
|
|
562
|
-
function resolveConfigForScope(scopeName) {
|
|
563
|
-
var cfg = getConfig();
|
|
563
|
+
function resolveConfigForScope(scopeName, cfgOverride) {
|
|
564
|
+
var cfg = cfgOverride || getConfig();
|
|
564
565
|
var cfgIncludes = cfg.includes || {};
|
|
565
566
|
var cfgExcludes = cfg.excludes || {};
|
|
566
567
|
// Backward compat: support old "table" key (no underscore)
|
|
@@ -639,10 +640,40 @@ function resolveConfigForScope(scopeName) {
|
|
|
639
640
|
var apiIncludes = Object.assign({}, fieldOverrides);
|
|
640
641
|
// API excludes: strip _ keys
|
|
641
642
|
var apiExcludes = stripDirectiveKeys(cfgExcludes);
|
|
643
|
+
// Read-only tables: union of global _readOnlyTables and scope-level override.
|
|
644
|
+
// A table flagged read-only is still pulled from the instance but pushes are
|
|
645
|
+
// skipped. Default when absent: empty (bidirectional for all tables).
|
|
646
|
+
var globalReadOnly = Array.isArray(cfgIncludes._readOnlyTables)
|
|
647
|
+
? cfgIncludes._readOnlyTables
|
|
648
|
+
: [];
|
|
649
|
+
var scopeReadOnly = Array.isArray(scopeOverride._readOnlyTables)
|
|
650
|
+
? scopeOverride._readOnlyTables
|
|
651
|
+
: [];
|
|
652
|
+
var readOnlySet = {};
|
|
653
|
+
for (i = 0; i < globalReadOnly.length; i++) {
|
|
654
|
+
readOnlySet[globalReadOnly[i]] = true;
|
|
655
|
+
}
|
|
656
|
+
for (i = 0; i < scopeReadOnly.length; i++) {
|
|
657
|
+
readOnlySet[scopeReadOnly[i]] = true;
|
|
658
|
+
}
|
|
659
|
+
var resolvedReadOnly = Object.keys(readOnlySet);
|
|
642
660
|
return {
|
|
643
661
|
tables: resolvedTables,
|
|
644
662
|
fieldOverrides: fieldOverrides,
|
|
645
663
|
apiIncludes: apiIncludes,
|
|
646
664
|
apiExcludes: apiExcludes,
|
|
665
|
+
readOnlyTables: resolvedReadOnly,
|
|
647
666
|
};
|
|
648
667
|
}
|
|
668
|
+
// Convenience wrapper for push-side lookups — returns a Set for O(1) membership.
|
|
669
|
+
// Fails safe (empty set = all tables writable) when config isn't available
|
|
670
|
+
// rather than blocking a push over a lookup error.
|
|
671
|
+
function getReadOnlyTablesForScope(scopeName) {
|
|
672
|
+
try {
|
|
673
|
+
var resolved = resolveConfigForScope(scopeName);
|
|
674
|
+
return new Set(resolved.readOnlyTables || []);
|
|
675
|
+
}
|
|
676
|
+
catch (e) {
|
|
677
|
+
return new Set();
|
|
678
|
+
}
|
|
679
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.decodeV2Values = decodeV2Values;
|
|
4
|
+
exports.encodeV2Values = encodeV2Values;
|
|
5
|
+
const zlib_1 = require("zlib");
|
|
6
|
+
/**
|
|
7
|
+
* Decode a ServiceNow V2 Flow Designer `values` blob.
|
|
8
|
+
*
|
|
9
|
+
* Source tables:
|
|
10
|
+
* - sys_hub_action_instance_v2.values
|
|
11
|
+
* - sys_hub_trigger_instance_v2.values
|
|
12
|
+
* - sys_hub_flow_logic_instance_v2.values
|
|
13
|
+
*
|
|
14
|
+
* Storage format: base64-encoded gzipped JSON.
|
|
15
|
+
* Mirrors `GlideStringUtil.base64DecodeAsBytes()` + `GlideCompressionUtil.expandToString()`
|
|
16
|
+
* + `JSON.parse` on the ServiceNow platform.
|
|
17
|
+
*
|
|
18
|
+
* Throws with a clear message if the blob is empty, not base64, or not gzip.
|
|
19
|
+
*/
|
|
20
|
+
function decodeV2Values(blob) {
|
|
21
|
+
if (typeof blob !== "string" || blob.trim().length === 0) {
|
|
22
|
+
throw new Error("decodeV2Values: blob must be a non-empty string");
|
|
23
|
+
}
|
|
24
|
+
let buf;
|
|
25
|
+
try {
|
|
26
|
+
buf = Buffer.from(blob.trim(), "base64");
|
|
27
|
+
}
|
|
28
|
+
catch (err) {
|
|
29
|
+
throw new Error("decodeV2Values: blob is not valid base64");
|
|
30
|
+
}
|
|
31
|
+
let json;
|
|
32
|
+
try {
|
|
33
|
+
json = (0, zlib_1.gunzipSync)(buf).toString("utf8");
|
|
34
|
+
}
|
|
35
|
+
catch (err) {
|
|
36
|
+
throw new Error("decodeV2Values: decompression failed (input is not valid gzip after base64 decode)");
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
return JSON.parse(json);
|
|
40
|
+
}
|
|
41
|
+
catch (err) {
|
|
42
|
+
throw new Error("decodeV2Values: decompressed body is not valid JSON");
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Encode a V2 values payload back to ServiceNow's storage format.
|
|
47
|
+
*
|
|
48
|
+
* Inverse of decodeV2Values. Provided for symmetry, diagnostic round-trip
|
|
49
|
+
* tests, and any future write path. Current Craftsman sync treats sys_hub_*
|
|
50
|
+
* as _readOnlyTables, so this is not invoked on the push path today.
|
|
51
|
+
*/
|
|
52
|
+
function encodeV2Values(value) {
|
|
53
|
+
const json = JSON.stringify(value);
|
|
54
|
+
return (0, zlib_1.gzipSync)(Buffer.from(json, "utf8")).toString("base64");
|
|
55
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -1,14 +1,25 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
"use strict";
|
|
3
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.encodeV2Values = exports.decodeV2Values = void 0;
|
|
4
5
|
const bootstrap_1 = require("./bootstrap");
|
|
5
6
|
const FileLogger_1 = require("./FileLogger");
|
|
7
|
+
// Library exports — consumers can `import { decodeV2Values } from "@tenonhq/sincronia-core"`.
|
|
8
|
+
// Keep exports above the CLI entry so bundlers/TS can tree-shake and require() consumers
|
|
9
|
+
// never accidentally invoke main().
|
|
10
|
+
var values_1 = require("./flowDesigner/values");
|
|
11
|
+
Object.defineProperty(exports, "decodeV2Values", { enumerable: true, get: function () { return values_1.decodeV2Values; } });
|
|
12
|
+
Object.defineProperty(exports, "encodeV2Values", { enumerable: true, get: function () { return values_1.encodeV2Values; } });
|
|
6
13
|
async function main() {
|
|
7
14
|
// Initialize file logging as early as possible
|
|
8
15
|
FileLogger_1.fileLogger.info("Starting Sincronia...");
|
|
9
16
|
await (0, bootstrap_1.init)();
|
|
10
17
|
}
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
18
|
+
// Only run the CLI when this file is executed directly (e.g. `npx sinc`, `node dist/index.js`).
|
|
19
|
+
// When imported as a library, require.main !== module and main() is skipped.
|
|
20
|
+
if (require.main === module) {
|
|
21
|
+
main().catch(function (e) {
|
|
22
|
+
FileLogger_1.fileLogger.error("Fatal error: " + String(e));
|
|
23
|
+
process.exit(1);
|
|
24
|
+
});
|
|
25
|
+
}
|
|
@@ -3,6 +3,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
// Track how many times the update set config file is read
|
|
4
4
|
var configReadCount = 0;
|
|
5
5
|
var mockConfigData = {};
|
|
6
|
+
// Per-scope read-only table sets, consumed by the mocked ../config module.
|
|
7
|
+
// Mutate this object between tests to drive the push-gate behavior.
|
|
8
|
+
var mockReadOnlyByScope = {};
|
|
6
9
|
jest.mock("fs", function () {
|
|
7
10
|
var actualFs = jest.requireActual("fs");
|
|
8
11
|
return {
|
|
@@ -22,6 +25,16 @@ jest.mock("fs", function () {
|
|
|
22
25
|
},
|
|
23
26
|
};
|
|
24
27
|
});
|
|
28
|
+
// Mock only the config surface pushFiles touches. getReadOnlyTablesForScope
|
|
29
|
+
// is the one new call site; the rest of pushFiles reads no ConfigManager
|
|
30
|
+
// exports, so a minimal mock keeps the test focused on the push gate.
|
|
31
|
+
jest.mock("../config", function () {
|
|
32
|
+
return {
|
|
33
|
+
getReadOnlyTablesForScope: function (scope) {
|
|
34
|
+
return mockReadOnlyByScope[scope] || new Set();
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
});
|
|
25
38
|
// Mock dependencies that pushFiles needs
|
|
26
39
|
var mockPushWithUpdateSet = jest.fn().mockResolvedValue({
|
|
27
40
|
data: { result: { status: "success" } },
|
|
@@ -98,6 +111,7 @@ describe("pushFiles", function () {
|
|
|
98
111
|
beforeEach(function () {
|
|
99
112
|
configReadCount = 0;
|
|
100
113
|
mockConfigData = {};
|
|
114
|
+
mockReadOnlyByScope = {};
|
|
101
115
|
mockPushWithUpdateSet.mockClear();
|
|
102
116
|
mockUpdateRecord.mockClear();
|
|
103
117
|
});
|
|
@@ -159,4 +173,73 @@ describe("pushFiles", function () {
|
|
|
159
173
|
expect(mockUpdateRecord).toHaveBeenCalledTimes(1);
|
|
160
174
|
expect(mockPushWithUpdateSet).not.toHaveBeenCalled();
|
|
161
175
|
});
|
|
176
|
+
it("skips push for tables listed in _readOnlyTables for the scope", async function () {
|
|
177
|
+
mockConfigData = {};
|
|
178
|
+
mockReadOnlyByScope = {
|
|
179
|
+
x_cadso_core: new Set(["sys_hub_flow"]),
|
|
180
|
+
};
|
|
181
|
+
var { pushFiles } = require("../appUtils");
|
|
182
|
+
var records = [
|
|
183
|
+
makeRecord("sys_hub_flow", "flow1", "x_cadso_core"),
|
|
184
|
+
];
|
|
185
|
+
var results = await pushFiles(records);
|
|
186
|
+
expect(mockPushWithUpdateSet).not.toHaveBeenCalled();
|
|
187
|
+
expect(mockUpdateRecord).not.toHaveBeenCalled();
|
|
188
|
+
expect(results).toHaveLength(1);
|
|
189
|
+
expect(results[0].success).toBe(true);
|
|
190
|
+
expect(results[0].message).toContain("skipped (read-only table)");
|
|
191
|
+
});
|
|
192
|
+
it("pushes normally when the scope has no read-only tables", async function () {
|
|
193
|
+
mockConfigData = {};
|
|
194
|
+
mockReadOnlyByScope = {};
|
|
195
|
+
var { pushFiles } = require("../appUtils");
|
|
196
|
+
var records = [
|
|
197
|
+
makeRecord("sys_script_include", "rec1", "x_cadso_core"),
|
|
198
|
+
];
|
|
199
|
+
await pushFiles(records);
|
|
200
|
+
expect(mockUpdateRecord).toHaveBeenCalledTimes(1);
|
|
201
|
+
expect(mockPushWithUpdateSet).not.toHaveBeenCalled();
|
|
202
|
+
});
|
|
203
|
+
it("in a mixed batch, only non-read-only tables are pushed", async function () {
|
|
204
|
+
mockConfigData = {};
|
|
205
|
+
mockReadOnlyByScope = {
|
|
206
|
+
x_cadso_core: new Set(["sys_hub_flow"]),
|
|
207
|
+
};
|
|
208
|
+
var { pushFiles } = require("../appUtils");
|
|
209
|
+
var records = [
|
|
210
|
+
makeRecord("sys_hub_flow", "flow1", "x_cadso_core"),
|
|
211
|
+
makeRecord("sys_script_include", "rec1", "x_cadso_core"),
|
|
212
|
+
makeRecord("sys_hub_action_instance", "ai1", "x_cadso_core"),
|
|
213
|
+
];
|
|
214
|
+
var results = await pushFiles(records);
|
|
215
|
+
// Only the read-only flow row is skipped; the other two hit updateRecord.
|
|
216
|
+
expect(mockUpdateRecord).toHaveBeenCalledTimes(2);
|
|
217
|
+
expect(mockPushWithUpdateSet).not.toHaveBeenCalled();
|
|
218
|
+
expect(results).toHaveLength(3);
|
|
219
|
+
var skipped = results.filter(function (r) {
|
|
220
|
+
return r.message.indexOf("skipped (read-only table)") !== -1;
|
|
221
|
+
});
|
|
222
|
+
expect(skipped).toHaveLength(1);
|
|
223
|
+
});
|
|
224
|
+
it("applies scope-specific read-only tables independently across scopes", async function () {
|
|
225
|
+
mockConfigData = {};
|
|
226
|
+
mockReadOnlyByScope = {
|
|
227
|
+
x_cadso_core: new Set(["sys_hub_flow"]),
|
|
228
|
+
x_cadso_work: new Set(),
|
|
229
|
+
};
|
|
230
|
+
var { pushFiles } = require("../appUtils");
|
|
231
|
+
var records = [
|
|
232
|
+
makeRecord("sys_hub_flow", "flow1", "x_cadso_core"),
|
|
233
|
+
makeRecord("sys_hub_flow", "flow2", "x_cadso_work"),
|
|
234
|
+
];
|
|
235
|
+
var results = await pushFiles(records);
|
|
236
|
+
// x_cadso_core's flow is blocked; x_cadso_work's flow pushes because it
|
|
237
|
+
// isn't in that scope's read-only set.
|
|
238
|
+
expect(mockUpdateRecord).toHaveBeenCalledTimes(1);
|
|
239
|
+
expect(results).toHaveLength(2);
|
|
240
|
+
var skipped = results.filter(function (r) {
|
|
241
|
+
return r.message.indexOf("skipped (read-only table)") !== -1;
|
|
242
|
+
});
|
|
243
|
+
expect(skipped).toHaveLength(1);
|
|
244
|
+
});
|
|
162
245
|
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const config_1 = require("../config");
|
|
4
|
+
const baseConfig = {
|
|
5
|
+
sourceDirectory: "src",
|
|
6
|
+
buildDirectory: "build",
|
|
7
|
+
includes: {},
|
|
8
|
+
excludes: {},
|
|
9
|
+
tableOptions: {},
|
|
10
|
+
refreshInterval: 30,
|
|
11
|
+
scopes: {},
|
|
12
|
+
};
|
|
13
|
+
function withIncludes(overrides) {
|
|
14
|
+
return {
|
|
15
|
+
...baseConfig,
|
|
16
|
+
includes: overrides,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
describe("resolveConfigForScope - _readOnlyTables", () => {
|
|
20
|
+
it("returns an empty list when _readOnlyTables is absent", () => {
|
|
21
|
+
const cfg = withIncludes({ _tables: ["incident", "sys_hub_flow"] });
|
|
22
|
+
const resolved = (0, config_1.resolveConfigForScope)("any_scope", cfg);
|
|
23
|
+
expect(resolved.readOnlyTables).toEqual([]);
|
|
24
|
+
});
|
|
25
|
+
it("returns the global _readOnlyTables list when no scope override", () => {
|
|
26
|
+
const cfg = withIncludes({
|
|
27
|
+
_tables: ["incident", "sys_hub_flow"],
|
|
28
|
+
_readOnlyTables: ["sys_hub_flow"],
|
|
29
|
+
});
|
|
30
|
+
const resolved = (0, config_1.resolveConfigForScope)("x_cadso_core", cfg);
|
|
31
|
+
expect(resolved.readOnlyTables).toEqual(["sys_hub_flow"]);
|
|
32
|
+
});
|
|
33
|
+
it("unions global and scope _readOnlyTables without duplicates", () => {
|
|
34
|
+
const cfg = withIncludes({
|
|
35
|
+
_tables: ["incident", "sys_hub_flow", "sys_hub_action_instance"],
|
|
36
|
+
_readOnlyTables: ["sys_hub_flow"],
|
|
37
|
+
_scopes: {
|
|
38
|
+
x_cadso_core: {
|
|
39
|
+
_readOnlyTables: ["sys_hub_flow", "sys_hub_action_instance"],
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
const resolved = (0, config_1.resolveConfigForScope)("x_cadso_core", cfg);
|
|
44
|
+
expect(resolved.readOnlyTables.sort()).toEqual(["sys_hub_action_instance", "sys_hub_flow"].sort());
|
|
45
|
+
});
|
|
46
|
+
it("falls back to the global list for scopes without an override", () => {
|
|
47
|
+
const cfg = withIncludes({
|
|
48
|
+
_tables: ["incident", "sys_hub_flow"],
|
|
49
|
+
_readOnlyTables: ["sys_hub_flow"],
|
|
50
|
+
_scopes: {
|
|
51
|
+
x_cadso_core: {
|
|
52
|
+
_readOnlyTables: ["sys_hub_action_instance"],
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
const resolved = (0, config_1.resolveConfigForScope)("x_cadso_other", cfg);
|
|
57
|
+
expect(resolved.readOnlyTables).toEqual(["sys_hub_flow"]);
|
|
58
|
+
});
|
|
59
|
+
it("ignores non-array _readOnlyTables values safely", () => {
|
|
60
|
+
const cfg = withIncludes({
|
|
61
|
+
_tables: ["incident"],
|
|
62
|
+
_readOnlyTables: "oops-not-an-array",
|
|
63
|
+
});
|
|
64
|
+
const resolved = (0, config_1.resolveConfigForScope)("any_scope", cfg);
|
|
65
|
+
expect(resolved.readOnlyTables).toEqual([]);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const zlib_1 = require("zlib");
|
|
4
|
+
const values_1 = require("../flowDesigner/values");
|
|
5
|
+
describe("decodeV2Values / encodeV2Values", () => {
|
|
6
|
+
describe("round-trip", () => {
|
|
7
|
+
it("decodes what encodeV2Values encoded (primitive array)", () => {
|
|
8
|
+
const original = [
|
|
9
|
+
{ actionInstanceSysId: "abc", id: "1", name: "to", value: "user@example.com" },
|
|
10
|
+
{ actionInstanceSysId: "abc", id: "2", name: "subject", value: "hi" },
|
|
11
|
+
];
|
|
12
|
+
const blob = (0, values_1.encodeV2Values)(original);
|
|
13
|
+
expect((0, values_1.decodeV2Values)(blob)).toEqual(original);
|
|
14
|
+
});
|
|
15
|
+
it("decodes what encodeV2Values encoded (nested parameter objects)", () => {
|
|
16
|
+
const original = [
|
|
17
|
+
{
|
|
18
|
+
actionInstanceSysId: "abc123",
|
|
19
|
+
id: "input_a",
|
|
20
|
+
name: "record",
|
|
21
|
+
value: { table: "incident", sys_id: "xyz" },
|
|
22
|
+
parameter: {
|
|
23
|
+
type: "reference",
|
|
24
|
+
labels: { en: "Record" },
|
|
25
|
+
validation: { mandatory: true },
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
];
|
|
29
|
+
const blob = (0, values_1.encodeV2Values)(original);
|
|
30
|
+
expect((0, values_1.decodeV2Values)(blob)).toEqual(original);
|
|
31
|
+
});
|
|
32
|
+
it("handles empty array", () => {
|
|
33
|
+
expect((0, values_1.decodeV2Values)((0, values_1.encodeV2Values)([]))).toEqual([]);
|
|
34
|
+
});
|
|
35
|
+
it("handles large payloads without truncation", () => {
|
|
36
|
+
const big = Array.from({ length: 500 }, (_, i) => ({
|
|
37
|
+
id: String(i),
|
|
38
|
+
name: "field_" + i,
|
|
39
|
+
value: "x".repeat(200),
|
|
40
|
+
}));
|
|
41
|
+
expect((0, values_1.decodeV2Values)((0, values_1.encodeV2Values)(big))).toEqual(big);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
describe("decode — input validation", () => {
|
|
45
|
+
it("throws on empty string", () => {
|
|
46
|
+
expect(() => (0, values_1.decodeV2Values)("")).toThrow(/non-empty string/);
|
|
47
|
+
});
|
|
48
|
+
it("throws on whitespace-only string", () => {
|
|
49
|
+
expect(() => (0, values_1.decodeV2Values)(" \n ")).toThrow(/non-empty string/);
|
|
50
|
+
});
|
|
51
|
+
it("throws on non-string input", () => {
|
|
52
|
+
expect(() => (0, values_1.decodeV2Values)(null)).toThrow(/non-empty string/);
|
|
53
|
+
expect(() => (0, values_1.decodeV2Values)(undefined)).toThrow(/non-empty string/);
|
|
54
|
+
expect(() => (0, values_1.decodeV2Values)(123)).toThrow(/non-empty string/);
|
|
55
|
+
});
|
|
56
|
+
it("throws with a clear message on non-gzip base64 input", () => {
|
|
57
|
+
// Valid base64, but the decoded bytes are not gzip.
|
|
58
|
+
const notGzip = Buffer.from("hello world").toString("base64");
|
|
59
|
+
expect(() => (0, values_1.decodeV2Values)(notGzip)).toThrow(/decompression failed/);
|
|
60
|
+
});
|
|
61
|
+
it("throws on gzip of invalid JSON", () => {
|
|
62
|
+
const badJson = (0, zlib_1.gzipSync)(Buffer.from("{not valid json", "utf8")).toString("base64");
|
|
63
|
+
expect(() => (0, values_1.decodeV2Values)(badJson)).toThrow(/not valid JSON/);
|
|
64
|
+
});
|
|
65
|
+
it("tolerates trailing whitespace and newlines (common from file reads)", () => {
|
|
66
|
+
const blob = (0, values_1.encodeV2Values)([{ id: "1", value: "a" }]);
|
|
67
|
+
expect((0, values_1.decodeV2Values)(blob + "\n")).toEqual([{ id: "1", value: "a" }]);
|
|
68
|
+
expect((0, values_1.decodeV2Values)(" " + blob + " ")).toEqual([{ id: "1", value: "a" }]);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
describe("generic typing", () => {
|
|
72
|
+
it("preserves the caller's expected type", () => {
|
|
73
|
+
const blob = (0, values_1.encodeV2Values)([{ actionInstanceSysId: "a", id: "1" }]);
|
|
74
|
+
const decoded = (0, values_1.decodeV2Values)(blob);
|
|
75
|
+
expect(decoded[0].actionInstanceSysId).toBe("a");
|
|
76
|
+
expect(decoded[0].id).toBe("1");
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
});
|
|
@@ -283,7 +283,7 @@ async function switchUpdateSetCommand(args) {
|
|
|
283
283
|
// Extract the actual values from display_value objects
|
|
284
284
|
const sysId = typeof targetUpdateSet.sys_id === 'object' && targetUpdateSet.sys_id !== null ? targetUpdateSet.sys_id.value : targetUpdateSet.sys_id;
|
|
285
285
|
const name = typeof targetUpdateSet.name === 'object' && targetUpdateSet.name !== null ? targetUpdateSet.name.value : targetUpdateSet.name;
|
|
286
|
-
const applicationName = targetUpdateSet.application
|
|
286
|
+
const applicationName = (targetUpdateSet.application && targetUpdateSet.application.display_value) || (typeof targetUpdateSet.application === 'object' && targetUpdateSet.application !== null ? targetUpdateSet.application.value : targetUpdateSet.application);
|
|
287
287
|
const applicationScope = typeof targetUpdateSet.application === 'object' && targetUpdateSet.application !== null ? targetUpdateSet.application.value : targetUpdateSet.application;
|
|
288
288
|
// Switch to the selected update set
|
|
289
289
|
await switchToUpdateSet(sysId, name, args.scope || applicationScope);
|
|
@@ -334,7 +334,7 @@ async function listUpdateSetsCommand(args) {
|
|
|
334
334
|
const name = typeof updateSet.name === 'object' && updateSet.name !== null ? updateSet.name.value : updateSet.name;
|
|
335
335
|
const description = typeof updateSet.description === 'object' && updateSet.description !== null ? updateSet.description.value : updateSet.description;
|
|
336
336
|
const createdBy = typeof updateSet.sys_created_by === 'object' && updateSet.sys_created_by !== null ? updateSet.sys_created_by.display_value || updateSet.sys_created_by.value : updateSet.sys_created_by;
|
|
337
|
-
const applicationName = updateSet.application
|
|
337
|
+
const applicationName = (updateSet.application && updateSet.application.display_value) || (typeof updateSet.application === 'object' && updateSet.application !== null ? updateSet.application.value : updateSet.application);
|
|
338
338
|
const isCurrent = sysId === currentUpdateSetId;
|
|
339
339
|
const marker = isCurrent ? chalk_1.default.green("► ") : " ";
|
|
340
340
|
const displayName = isCurrent ? chalk_1.default.green(name) : name;
|
|
@@ -616,7 +616,7 @@ async function selectUpdateSet(nameFilter, scopeFilter) {
|
|
|
616
616
|
const choices = filteredSets.map(us => {
|
|
617
617
|
const name = typeof us.name === 'object' && us.name !== null ? us.name.value : us.name;
|
|
618
618
|
const description = typeof us.description === 'object' && us.description !== null ? us.description.value : us.description;
|
|
619
|
-
const applicationName = us.application
|
|
619
|
+
const applicationName = (us.application && us.application.display_value) || (typeof us.application === 'object' && us.application !== null ? us.application.value : us.application);
|
|
620
620
|
return {
|
|
621
621
|
name: `${name}${applicationName ? ` (${applicationName})` : ""} - ${description || "No description"}`,
|
|
622
622
|
value: us
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tenonhq/sincronia-core",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.84",
|
|
4
4
|
"description": "Next-gen file syncer",
|
|
5
5
|
"license": "GPL-3.0",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"typescript": "^5.2.2"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@tenonhq/sincronia-clickup": "^0.0.
|
|
41
|
+
"@tenonhq/sincronia-clickup": "^0.0.6",
|
|
42
42
|
"@tenonhq/sincronia-schema": "^0.0.1",
|
|
43
43
|
"axios": "^1.5.1",
|
|
44
44
|
"axios-cookiejar-support": "^4.0.7",
|