@tenonhq/dovetail-core 0.0.85
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/README.md +6 -0
- package/dist/FileLogger.js +207 -0
- package/dist/FileUtils.js +387 -0
- package/dist/Logger.js +88 -0
- package/dist/MultiScopeWatcher.js +615 -0
- package/dist/PluginManager.js +125 -0
- package/dist/_defaultOptionsOLD.js +44 -0
- package/dist/allScopesCommands.js +418 -0
- package/dist/appUtils.js +929 -0
- package/dist/benchmark.js +104 -0
- package/dist/bootstrap.js +62 -0
- package/dist/claudeCommand.js +60 -0
- package/dist/clickupCommands.js +472 -0
- package/dist/clickupPushHelper.js +33 -0
- package/dist/commander.js +466 -0
- package/dist/commands.js +378 -0
- package/dist/config.js +697 -0
- package/dist/constants.js +15 -0
- package/dist/createRecordCommand.js +314 -0
- package/dist/dashboardCommand.js +38 -0
- package/dist/defaultOptions.js +14 -0
- package/dist/deleteRecordCommand.js +282 -0
- package/dist/flowDesigner/values.js +55 -0
- package/dist/genericUtils.js +189 -0
- package/dist/gitUtils.js +99 -0
- package/dist/index.js +35 -0
- package/dist/initSystem/corePlugin.js +344 -0
- package/dist/initSystem/discovery.js +107 -0
- package/dist/initSystem/orchestrator.js +408 -0
- package/dist/logMessages.js +82 -0
- package/dist/loginCommand.js +20 -0
- package/dist/migrateCommand.js +123 -0
- package/dist/projectFiles.js +32 -0
- package/dist/recentEdits.js +66 -0
- package/dist/schemaCommand.js +85 -0
- package/dist/snClient.js +519 -0
- package/dist/tests/benchmarkRefresh.test.js +265 -0
- package/dist/tests/clickupCommands.test.js +384 -0
- package/dist/tests/clickupPushHelper.test.js +124 -0
- package/dist/tests/configFallback.test.js +68 -0
- package/dist/tests/cred.test.js +59 -0
- package/dist/tests/discovery.test.js +67 -0
- package/dist/tests/ensureUpdateSetWarnings.test.js +218 -0
- package/dist/tests/errorLogLevels.test.js +273 -0
- package/dist/tests/example.test.js +6 -0
- package/dist/tests/fileContextSkipReason.test.js +116 -0
- package/dist/tests/globalDebounce.test.js +308 -0
- package/dist/tests/migrateCommand.test.js +113 -0
- package/dist/tests/multi-scope-watcher.test.js +558 -0
- package/dist/tests/orchestrator.test.js +23 -0
- package/dist/tests/pushFiles.test.js +248 -0
- package/dist/tests/rateLimitCoordination.test.js +271 -0
- package/dist/tests/resolveConfigReadOnly.test.js +67 -0
- package/dist/tests/retryOnHttpErr.test.js +154 -0
- package/dist/tests/scopeCaching.test.js +115 -0
- package/dist/tests/serializeUpdateSetConfig.test.js +325 -0
- package/dist/tests/snClient.fallback.test.js +120 -0
- package/dist/tests/syncManifestRefresh.test.js +287 -0
- package/dist/tests/syncManifestWhitelist.test.js +192 -0
- package/dist/tests/taskClear.test.js +170 -0
- package/dist/tests/taskStaleness.test.js +220 -0
- package/dist/tests/unwrapSNResponseError.test.js +145 -0
- package/dist/tests/v2Values.test.js +79 -0
- package/dist/tests/validateTaskId.test.js +304 -0
- package/dist/tests/verifyUpdateSetSwitch.test.js +277 -0
- package/dist/tests/writeEnvVar.test.js +119 -0
- package/dist/updateSetCommands.js +702 -0
- package/dist/wizard.js +40 -0
- package/package.json +69 -0
- package/skills/dove-configure-pipeline.md +158 -0
- package/skills/dove-create-plugin.md +174 -0
- package/skills/dove-create-record.md +135 -0
- package/skills/dove-debug-build.md +117 -0
- package/skills/dove-delete-record.md +104 -0
- package/skills/dove-manage-tables.md +174 -0
- package/skills/dove-manage-update-sets.md +88 -0
- package/skills/dove-setup-project.md +154 -0
- package/skills/dove-troubleshoot-sync.md +113 -0
package/dist/appUtils.js
ADDED
|
@@ -0,0 +1,929 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.createAndAssignUpdateSet = exports.swapScope = exports.buildFiles = exports.summarizeRecord = exports.pushFiles = exports.getAppFileList = exports.groupAppFiles = exports.processMissingFiles = exports.refreshAllFiles = exports.findMissingFiles = exports.syncManifest = exports.processManifest = exports.normalizeManifestKeys = void 0;
|
|
40
|
+
const path_1 = __importDefault(require("path"));
|
|
41
|
+
const fs_1 = __importDefault(require("fs"));
|
|
42
|
+
const progress_1 = __importDefault(require("progress"));
|
|
43
|
+
const fUtils = __importStar(require("./FileUtils"));
|
|
44
|
+
const ConfigManager = __importStar(require("./config"));
|
|
45
|
+
const constants_1 = require("./constants");
|
|
46
|
+
const PluginManager_1 = __importDefault(require("./PluginManager"));
|
|
47
|
+
const FileLogger_1 = require("./FileLogger");
|
|
48
|
+
const projectFiles_1 = require("./projectFiles");
|
|
49
|
+
const snClient_1 = require("./snClient");
|
|
50
|
+
const benchmark_1 = require("./benchmark");
|
|
51
|
+
const Logger_1 = require("./Logger");
|
|
52
|
+
const genericUtils_1 = require("./genericUtils");
|
|
53
|
+
const getUpdateSetConfig = () => {
|
|
54
|
+
const configPath = (0, projectFiles_1.getUpdateSetsConfigPath)();
|
|
55
|
+
try {
|
|
56
|
+
if (fs_1.default.existsSync(configPath)) {
|
|
57
|
+
return JSON.parse(fs_1.default.readFileSync(configPath, "utf8"));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
catch (e) {
|
|
61
|
+
Logger_1.logger.warn(`Failed to parse update set config at ${configPath}: ${e instanceof Error ? e.message : String(e)}`);
|
|
62
|
+
}
|
|
63
|
+
return {};
|
|
64
|
+
};
|
|
65
|
+
// Merge _lastUpdatedOn into the server-provided metadata content. Preserves all
|
|
66
|
+
// record fields (sys_id, sys_scope, field value/display_value pairs, etc.) so
|
|
67
|
+
// the local metaData.json is a full snapshot of the record, not just a stub.
|
|
68
|
+
const stampMetadataContent = (file) => {
|
|
69
|
+
if (file.name !== "metaData" || file.type !== "json")
|
|
70
|
+
return file;
|
|
71
|
+
const stamp = new Date().toISOString();
|
|
72
|
+
if (!file.content) {
|
|
73
|
+
return { ...file, content: JSON.stringify({ _lastUpdatedOn: stamp }, null, 2) };
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
const metadata = JSON.parse(file.content);
|
|
77
|
+
if (metadata.sys_updated_on && metadata.sys_updated_on.value) {
|
|
78
|
+
metadata._lastUpdatedOn = metadata.sys_updated_on.value;
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
metadata._lastUpdatedOn = stamp;
|
|
82
|
+
}
|
|
83
|
+
return { ...file, content: JSON.stringify(metadata, null, 2) };
|
|
84
|
+
}
|
|
85
|
+
catch (e) {
|
|
86
|
+
// Content isn't JSON — leave as-is, it will be written verbatim.
|
|
87
|
+
return file;
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
const hasServerMetadata = (files) => files.some((f) => f.name === "metaData" && f.type === "json" && !!f.content);
|
|
91
|
+
const processFilesInManRec = async (recPath, rec, forceWrite) => {
|
|
92
|
+
FileLogger_1.fileLogger.debug("Processing record: " + rec.name + " (" + rec.files.length + " files)");
|
|
93
|
+
const fileWrite = fUtils.writeSNFileCurry(forceWrite);
|
|
94
|
+
// If the server did not provide a metadata file, fall back to a timestamp-only
|
|
95
|
+
// stub so the record directory always has a metaData.json.
|
|
96
|
+
if (!hasServerMetadata(rec.files)) {
|
|
97
|
+
const stubMetadata = {
|
|
98
|
+
name: "metaData",
|
|
99
|
+
type: "json",
|
|
100
|
+
content: JSON.stringify({ _lastUpdatedOn: new Date().toISOString() }, null, 2),
|
|
101
|
+
};
|
|
102
|
+
await fileWrite(stubMetadata, recPath);
|
|
103
|
+
}
|
|
104
|
+
const writeResults = await (0, genericUtils_1.allSettledBatched)(rec.files, constants_1.CONCURRENCY_FILES, function (file) { return fileWrite(stampMetadataContent(file), recPath); });
|
|
105
|
+
const writeFailures = writeResults.filter((r) => r.status === "rejected");
|
|
106
|
+
if (writeFailures.length > 0) {
|
|
107
|
+
writeFailures.forEach((f) => {
|
|
108
|
+
FileLogger_1.fileLogger.error("File write failed: " + f.reason);
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
// Remove content from ALL files (metadata is not included in manifest)
|
|
112
|
+
rec.files = rec.files.map((file) => {
|
|
113
|
+
const fileCopy = { ...file };
|
|
114
|
+
delete fileCopy.content;
|
|
115
|
+
return fileCopy;
|
|
116
|
+
});
|
|
117
|
+
};
|
|
118
|
+
const processRecsInManTable = async (tablePath, table, forceWrite, onRecordProcessed) => {
|
|
119
|
+
const { records } = table;
|
|
120
|
+
const recKeys = Object.keys(records);
|
|
121
|
+
const recKeyToPath = (key) => path_1.default.join(tablePath, records[key].name);
|
|
122
|
+
const recPathPromises = recKeys
|
|
123
|
+
.map(recKeyToPath)
|
|
124
|
+
.map(fUtils.createDirRecursively);
|
|
125
|
+
await Promise.all(recPathPromises);
|
|
126
|
+
await (0, genericUtils_1.processBatched)(recKeys, constants_1.CONCURRENCY_RECORDS, function (recKey) {
|
|
127
|
+
return processFilesInManRec(recKeyToPath(recKey), records[recKey], forceWrite).then(function () {
|
|
128
|
+
if (onRecordProcessed)
|
|
129
|
+
onRecordProcessed();
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
};
|
|
133
|
+
const countRecordsInTables = (tables) => {
|
|
134
|
+
return Object.keys(tables).reduce(function (sum, tableName) {
|
|
135
|
+
return sum + Object.keys(tables[tableName].records).length;
|
|
136
|
+
}, 0);
|
|
137
|
+
};
|
|
138
|
+
const processTablesInManifest = async (tables, forceWrite, sourcePath, onRecordProcessed, scope) => {
|
|
139
|
+
var basePath = sourcePath || ConfigManager.getSourcePath();
|
|
140
|
+
var tableNames = Object.keys(tables);
|
|
141
|
+
// Defense-in-depth: filter out any table not in the resolved whitelist before
|
|
142
|
+
// writing to disk. Protects against upstream defects that let non-whitelisted
|
|
143
|
+
// tables through (e.g. server-side manifest fanout, stale manifest entries).
|
|
144
|
+
if (scope) {
|
|
145
|
+
try {
|
|
146
|
+
var resolved = ConfigManager.resolveConfigForScope(scope);
|
|
147
|
+
var allowed = resolved.tables;
|
|
148
|
+
if (allowed && allowed.length > 0) {
|
|
149
|
+
var skipped = [];
|
|
150
|
+
tableNames = tableNames.filter(function (t) {
|
|
151
|
+
if (allowed.indexOf(t) === -1) {
|
|
152
|
+
skipped.push(t);
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
return true;
|
|
156
|
+
});
|
|
157
|
+
if (skipped.length > 0) {
|
|
158
|
+
FileLogger_1.fileLogger.debug("processTablesInManifest: dropped " + skipped.length +
|
|
159
|
+
" non-whitelisted tables for scope '" + scope + "': " + skipped.join(", "));
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
catch (e) {
|
|
164
|
+
// Config resolution can fail for legacy single-scope manifests; fall
|
|
165
|
+
// through and process whatever the manifest contains.
|
|
166
|
+
FileLogger_1.fileLogger.debug("processTablesInManifest: could not resolve whitelist for scope '" + scope + "'");
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
await (0, genericUtils_1.processBatched)(tableNames, constants_1.CONCURRENCY_TABLES, function (tableName) {
|
|
170
|
+
return processRecsInManTable(path_1.default.join(basePath, tableName), tables[tableName], forceWrite, onRecordProcessed);
|
|
171
|
+
});
|
|
172
|
+
};
|
|
173
|
+
/**
|
|
174
|
+
* Re-keys manifest records from sys_id to record.name (display value).
|
|
175
|
+
* Some ServiceNow tables return records keyed by sys_id instead of display name.
|
|
176
|
+
* This ensures consistent naming for directories and manifest lookups.
|
|
177
|
+
*/
|
|
178
|
+
const normalizeManifestKeys = (manifest) => {
|
|
179
|
+
var tables = manifest.tables || {};
|
|
180
|
+
var tableNames = Object.keys(tables);
|
|
181
|
+
for (var i = 0; i < tableNames.length; i++) {
|
|
182
|
+
var tableName = tableNames[i];
|
|
183
|
+
var records = tables[tableName].records || {};
|
|
184
|
+
var recordKeys = Object.keys(records);
|
|
185
|
+
var normalized = {};
|
|
186
|
+
for (var j = 0; j < recordKeys.length; j++) {
|
|
187
|
+
var key = recordKeys[j];
|
|
188
|
+
var record = records[key];
|
|
189
|
+
var displayKey = record.name || key;
|
|
190
|
+
// Handle duplicate display names by appending sys_id suffix
|
|
191
|
+
if (normalized[displayKey]) {
|
|
192
|
+
displayKey = displayKey + " (" + record.sys_id.substring(0, 8) + ")";
|
|
193
|
+
}
|
|
194
|
+
normalized[displayKey] = record;
|
|
195
|
+
}
|
|
196
|
+
tables[tableName].records = normalized;
|
|
197
|
+
}
|
|
198
|
+
return manifest;
|
|
199
|
+
};
|
|
200
|
+
exports.normalizeManifestKeys = normalizeManifestKeys;
|
|
201
|
+
const processManifest = async (manifest, forceWrite = false, sourcePath) => {
|
|
202
|
+
const tableCount = Object.keys(manifest.tables).length;
|
|
203
|
+
FileLogger_1.fileLogger.debug("Processing manifest: " + (manifest.scope || "legacy") + " (" + tableCount + " tables)");
|
|
204
|
+
var recordCount = countRecordsInTables(manifest.tables);
|
|
205
|
+
var progress = createScopeProgress(Logger_1.logger.getLogLevel(), {
|
|
206
|
+
scope: manifest.scope || "default",
|
|
207
|
+
total: recordCount,
|
|
208
|
+
});
|
|
209
|
+
await processTablesInManifest(manifest.tables, forceWrite, sourcePath, progress.tick, manifest.scope);
|
|
210
|
+
if (manifest.scope) {
|
|
211
|
+
await fUtils.writeScopeManifest(manifest.scope, manifest);
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
await fUtils.writeFileForce(ConfigManager.getManifestPath(), JSON.stringify(manifest, null, 2));
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
exports.processManifest = processManifest;
|
|
218
|
+
const syncManifest = async (scope, options = {}) => {
|
|
219
|
+
// Top-level entry owns the collector lifecycle. Recursive calls (all-scopes
|
|
220
|
+
// → per-scope) inherit the collector via options._benchmarkCollector.
|
|
221
|
+
var isBenchmarkOwner = false;
|
|
222
|
+
var collector = options._benchmarkCollector;
|
|
223
|
+
if (options.benchmark && !collector) {
|
|
224
|
+
collector = new benchmark_1.BenchmarkCollector();
|
|
225
|
+
(0, snClient_1.setBenchmarkSink)(collector);
|
|
226
|
+
isBenchmarkOwner = true;
|
|
227
|
+
}
|
|
228
|
+
try {
|
|
229
|
+
const curManifest = await ConfigManager.getManifest();
|
|
230
|
+
if (!curManifest)
|
|
231
|
+
throw new Error("No manifest file loaded!");
|
|
232
|
+
const config = ConfigManager.getConfig();
|
|
233
|
+
const declaredScopes = (config.scopes && Object.keys(config.scopes)) || [];
|
|
234
|
+
// If a specific scope is provided, sync only that scope
|
|
235
|
+
if (scope) {
|
|
236
|
+
// Scope whitelist gate: refuse to refresh scopes not declared in dove.config.js.
|
|
237
|
+
// Without this, stale entries in dove.manifest.json leak undeclared scopes into
|
|
238
|
+
// the refresh loop (see RFC-0004 / sys_alias debris incident 2026-04-14).
|
|
239
|
+
if (declaredScopes.length > 0 && declaredScopes.indexOf(scope) === -1) {
|
|
240
|
+
Logger_1.logger.warn("Skipping scope '" + scope + "' — not declared in dove.config.js `scopes`. " +
|
|
241
|
+
"Add it to config.scopes to sync, or remove its manifest file.");
|
|
242
|
+
FileLogger_1.fileLogger.debug("syncManifest: skipped undeclared scope '" + scope + "'");
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
Logger_1.logger.info("Refreshing scope: " + scope + "...");
|
|
246
|
+
const client = (0, snClient_1.defaultClient)();
|
|
247
|
+
// Resolve scope-specific source directory + table whitelist
|
|
248
|
+
var scopeSourcePath = ConfigManager.getSourcePathForScope(scope);
|
|
249
|
+
var resolvedConfig = ConfigManager.resolveConfigForScope(scope);
|
|
250
|
+
var allowedTables = resolvedConfig.tables;
|
|
251
|
+
const newManifest = (0, exports.normalizeManifestKeys)(await (0, snClient_1.unwrapSNResponse)(client.getManifest(scope, config)));
|
|
252
|
+
// Table whitelist gate: drop any table the server returned that is not in
|
|
253
|
+
// the resolved _tables whitelist for this scope. Mirrors the filter in
|
|
254
|
+
// commands.ts downloadCommand() and allScopesCommands.ts processScope().
|
|
255
|
+
if (allowedTables && allowedTables.length > 0) {
|
|
256
|
+
var manifestTableNames = Object.keys(newManifest.tables || {});
|
|
257
|
+
var filteredTables = {};
|
|
258
|
+
var skippedCount = 0;
|
|
259
|
+
for (var t = 0; t < manifestTableNames.length; t++) {
|
|
260
|
+
var tName = manifestTableNames[t];
|
|
261
|
+
if (allowedTables.indexOf(tName) !== -1) {
|
|
262
|
+
filteredTables[tName] = newManifest.tables[tName];
|
|
263
|
+
}
|
|
264
|
+
else {
|
|
265
|
+
skippedCount++;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
if (skippedCount > 0) {
|
|
269
|
+
FileLogger_1.fileLogger.debug("syncManifest: filtered " + skippedCount + " tables not in _tables whitelist for " +
|
|
270
|
+
scope + " (kept " + Object.keys(filteredTables).length + " of " + manifestTableNames.length + ")");
|
|
271
|
+
}
|
|
272
|
+
newManifest.tables = filteredTables;
|
|
273
|
+
}
|
|
274
|
+
else {
|
|
275
|
+
Logger_1.logger.warn("No _tables whitelist defined — writing ALL tables for " + scope);
|
|
276
|
+
}
|
|
277
|
+
const refreshTableCount = Object.keys(newManifest.tables).length;
|
|
278
|
+
FileLogger_1.fileLogger.debug("Refreshed manifest for " + scope + ": " + refreshTableCount + " tables");
|
|
279
|
+
await fUtils.writeScopeManifest(scope, newManifest);
|
|
280
|
+
if (collector)
|
|
281
|
+
collector.startScope(scope);
|
|
282
|
+
await (0, exports.refreshAllFiles)(newManifest, scopeSourcePath, {
|
|
283
|
+
force: options.force,
|
|
284
|
+
benchmarkCollector: collector,
|
|
285
|
+
});
|
|
286
|
+
// Update the in-memory manifest for this scope
|
|
287
|
+
if (ConfigManager.isMultiScopeManifest(curManifest)) {
|
|
288
|
+
curManifest[scope] = newManifest;
|
|
289
|
+
ConfigManager.updateManifest(curManifest);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
else {
|
|
293
|
+
// Sync all scopes. Prefer the declared-scopes list (config.scopes) over
|
|
294
|
+
// the persisted manifest keys — the manifest may contain stale undeclared
|
|
295
|
+
// scopes that leaked in before the whitelist gate existed.
|
|
296
|
+
var childOptions = {
|
|
297
|
+
force: options.force,
|
|
298
|
+
_benchmarkCollector: collector,
|
|
299
|
+
};
|
|
300
|
+
if (declaredScopes.length > 0) {
|
|
301
|
+
for (var d = 0; d < declaredScopes.length; d++) {
|
|
302
|
+
await (0, exports.syncManifest)(declaredScopes[d], childOptions);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
else if (ConfigManager.isMultiScopeManifest(curManifest)) {
|
|
306
|
+
// No declared scopes — fall back to the persisted manifest's scopes.
|
|
307
|
+
for (const scopeName of Object.keys(curManifest)) {
|
|
308
|
+
await (0, exports.syncManifest)(scopeName, childOptions);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
else if (curManifest.scope) {
|
|
312
|
+
// Single scope manifest
|
|
313
|
+
await (0, exports.syncManifest)(curManifest.scope, childOptions);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
catch (e) {
|
|
318
|
+
let message;
|
|
319
|
+
if (e instanceof Error)
|
|
320
|
+
message = e.message;
|
|
321
|
+
else
|
|
322
|
+
message = String(e);
|
|
323
|
+
Logger_1.logger.error("Refresh failed: " + message);
|
|
324
|
+
}
|
|
325
|
+
finally {
|
|
326
|
+
if (isBenchmarkOwner && collector) {
|
|
327
|
+
(0, snClient_1.setBenchmarkSink)(null);
|
|
328
|
+
Logger_1.logger.info(collector.formatSummary());
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
};
|
|
332
|
+
exports.syncManifest = syncManifest;
|
|
333
|
+
const markFileMissing = (missingObj) => (table) => (recordId) => (file) => {
|
|
334
|
+
if (!missingObj[table]) {
|
|
335
|
+
missingObj[table] = {};
|
|
336
|
+
}
|
|
337
|
+
if (!missingObj[table][recordId]) {
|
|
338
|
+
missingObj[table][recordId] = [];
|
|
339
|
+
}
|
|
340
|
+
const { name, type } = file;
|
|
341
|
+
missingObj[table][recordId].push({ name, type });
|
|
342
|
+
};
|
|
343
|
+
const markRecordMissing = (record, missingFunc) => {
|
|
344
|
+
record.files.forEach((file) => {
|
|
345
|
+
missingFunc(record.sys_id)(file);
|
|
346
|
+
});
|
|
347
|
+
};
|
|
348
|
+
const markTableMissing = (table, tableName, missingFunc) => {
|
|
349
|
+
Object.keys(table.records).forEach((recName) => {
|
|
350
|
+
markRecordMissing(table.records[recName], missingFunc(tableName));
|
|
351
|
+
});
|
|
352
|
+
};
|
|
353
|
+
const checkFilesForMissing = async (recPath, files, missingFunc) => {
|
|
354
|
+
const checkPromises = files.map(fUtils.SNFileExists(recPath));
|
|
355
|
+
const checks = await Promise.all(checkPromises);
|
|
356
|
+
checks.forEach((check, index) => {
|
|
357
|
+
if (!check) {
|
|
358
|
+
missingFunc(files[index]);
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
};
|
|
362
|
+
const checkRecordsForMissing = async (tablePath, records, missingFunc) => {
|
|
363
|
+
const recNames = Object.keys(records);
|
|
364
|
+
const recPaths = recNames.map(fUtils.appendToPath(tablePath));
|
|
365
|
+
const checkPromises = recNames.map((recName, index) => fUtils.pathExists(recPaths[index]));
|
|
366
|
+
const checks = await Promise.all(checkPromises);
|
|
367
|
+
const fileCheckPromises = checks.map(async (check, index) => {
|
|
368
|
+
const recName = recNames[index];
|
|
369
|
+
const record = records[recName];
|
|
370
|
+
if (!check) {
|
|
371
|
+
markRecordMissing(record, missingFunc);
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
await checkFilesForMissing(recPaths[index], record.files, missingFunc(record.sys_id));
|
|
375
|
+
});
|
|
376
|
+
await Promise.all(fileCheckPromises);
|
|
377
|
+
};
|
|
378
|
+
const checkTablesForMissing = async (topPath, tables, missingFunc) => {
|
|
379
|
+
const tableNames = Object.keys(tables);
|
|
380
|
+
const tablePaths = tableNames.map(fUtils.appendToPath(topPath));
|
|
381
|
+
const checkPromises = tableNames.map((tableName, index) => fUtils.pathExists(tablePaths[index]));
|
|
382
|
+
const checks = await Promise.all(checkPromises);
|
|
383
|
+
const recCheckPromises = checks.map(async (check, index) => {
|
|
384
|
+
const tableName = tableNames[index];
|
|
385
|
+
if (!check) {
|
|
386
|
+
markTableMissing(tables[tableName], tableName, missingFunc);
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
await checkRecordsForMissing(tablePaths[index], tables[tableName].records, missingFunc(tableName));
|
|
390
|
+
});
|
|
391
|
+
await Promise.all(recCheckPromises);
|
|
392
|
+
};
|
|
393
|
+
const findMissingFiles = async (manifest, sourcePath) => {
|
|
394
|
+
const missing = {};
|
|
395
|
+
const { tables } = manifest;
|
|
396
|
+
const missingTableFunc = markFileMissing(missing);
|
|
397
|
+
await checkTablesForMissing(sourcePath || ConfigManager.getSourcePath(), tables, missingTableFunc);
|
|
398
|
+
// missing gets mutated along the way as things get processed
|
|
399
|
+
return missing;
|
|
400
|
+
};
|
|
401
|
+
exports.findMissingFiles = findMissingFiles;
|
|
402
|
+
// Chunk bulkDownload by table to stay under ServiceNow's 10 MB REST payload cap.
|
|
403
|
+
// A single unchunked call 500s on large scopes (e.g. x_cadso_automate at ~29 MB).
|
|
404
|
+
// Must mirror the chunk size used by allScopesCommands.ts (watch path) so behaviour
|
|
405
|
+
// is consistent across `refresh` and `watch`.
|
|
406
|
+
const BULK_DOWNLOAD_TABLE_CHUNK_SIZE = 5;
|
|
407
|
+
/**
|
|
408
|
+
* Builds a MissingFileTableMap containing EVERY file in the manifest — ignores
|
|
409
|
+
* local disk state. Used by `sinc refresh` to pull instance-side edits down.
|
|
410
|
+
*/
|
|
411
|
+
const buildAllFilesMap = (manifest) => {
|
|
412
|
+
const result = {};
|
|
413
|
+
const { tables } = manifest;
|
|
414
|
+
const tableNames = Object.keys(tables);
|
|
415
|
+
for (var t = 0; t < tableNames.length; t++) {
|
|
416
|
+
var tableName = tableNames[t];
|
|
417
|
+
var records = tables[tableName].records;
|
|
418
|
+
var recNames = Object.keys(records);
|
|
419
|
+
if (recNames.length === 0)
|
|
420
|
+
continue;
|
|
421
|
+
var recMap = {};
|
|
422
|
+
for (var r = 0; r < recNames.length; r++) {
|
|
423
|
+
var rec = records[recNames[r]];
|
|
424
|
+
if (!rec.files || rec.files.length === 0)
|
|
425
|
+
continue;
|
|
426
|
+
// Strip any content that may be lingering on manifest file entries; the
|
|
427
|
+
// bulkDownload endpoint only needs name + type to resolve each field.
|
|
428
|
+
recMap[rec.sys_id] = rec.files.map(function (f) {
|
|
429
|
+
return { name: f.name, type: f.type };
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
if (Object.keys(recMap).length > 0)
|
|
433
|
+
result[tableName] = recMap;
|
|
434
|
+
}
|
|
435
|
+
return result;
|
|
436
|
+
};
|
|
437
|
+
/**
|
|
438
|
+
* Refreshes local files against the ServiceNow instance for every file in the
|
|
439
|
+
* given manifest. Unlike `processMissingFiles` (which only writes files absent
|
|
440
|
+
* from disk), this walks ALL manifest files, fetches their current content from
|
|
441
|
+
* the instance, and writes when content differs.
|
|
442
|
+
*
|
|
443
|
+
* @param options.force — when true, always overwrite local files even if their
|
|
444
|
+
* content matches the instance. Use for deliberate "reset local to instance".
|
|
445
|
+
*/
|
|
446
|
+
const refreshAllFiles = async (newManifest, sourcePath, options = {}) => {
|
|
447
|
+
try {
|
|
448
|
+
const allFiles = buildAllFilesMap(newManifest);
|
|
449
|
+
const tableNames = Object.keys(allFiles);
|
|
450
|
+
if (tableNames.length === 0)
|
|
451
|
+
return;
|
|
452
|
+
FileLogger_1.fileLogger.debug("Refreshing file content for " + tableNames.length + " tables (force=" + !!options.force + ")");
|
|
453
|
+
const { tableOptions = {} } = ConfigManager.getConfig();
|
|
454
|
+
const client = (0, snClient_1.defaultClient)();
|
|
455
|
+
const totalChunks = Math.ceil(tableNames.length / BULK_DOWNLOAD_TABLE_CHUNK_SIZE);
|
|
456
|
+
const filesToProcess = {};
|
|
457
|
+
for (var i = 0; i < tableNames.length; i += BULK_DOWNLOAD_TABLE_CHUNK_SIZE) {
|
|
458
|
+
const chunkTableNames = tableNames.slice(i, i + BULK_DOWNLOAD_TABLE_CHUNK_SIZE);
|
|
459
|
+
const chunkMissing = {};
|
|
460
|
+
for (var j = 0; j < chunkTableNames.length; j++) {
|
|
461
|
+
chunkMissing[chunkTableNames[j]] = allFiles[chunkTableNames[j]];
|
|
462
|
+
}
|
|
463
|
+
const batchNum = Math.floor(i / BULK_DOWNLOAD_TABLE_CHUNK_SIZE) + 1;
|
|
464
|
+
FileLogger_1.fileLogger.debug("Refresh download batch " + batchNum + "/" + totalChunks +
|
|
465
|
+
" (" + chunkTableNames.length + " tables): " + chunkTableNames.join(", "));
|
|
466
|
+
const chunkResult = await (0, snClient_1.unwrapSNResponse)(client.getMissingFiles(chunkMissing, tableOptions));
|
|
467
|
+
for (var tableName in chunkResult) {
|
|
468
|
+
filesToProcess[tableName] = chunkResult[tableName];
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
var basePath = sourcePath || ConfigManager.getSourcePath();
|
|
472
|
+
var recordCount = countRecordsInTables(filesToProcess);
|
|
473
|
+
var progress = createScopeProgress(Logger_1.logger.getLogLevel(), {
|
|
474
|
+
scope: newManifest.scope || "default",
|
|
475
|
+
total: recordCount,
|
|
476
|
+
});
|
|
477
|
+
var writtenCount = 0;
|
|
478
|
+
var unchangedCount = 0;
|
|
479
|
+
const forceWrite = !!options.force;
|
|
480
|
+
const forceWriter = fUtils.writeSNFileCurry(false);
|
|
481
|
+
const processedTableNames = Object.keys(filesToProcess);
|
|
482
|
+
await (0, genericUtils_1.processBatched)(processedTableNames, constants_1.CONCURRENCY_TABLES, async function (tableName) {
|
|
483
|
+
var tablePath = path_1.default.join(basePath, tableName);
|
|
484
|
+
var recs = filesToProcess[tableName].records;
|
|
485
|
+
var recKeys = Object.keys(recs);
|
|
486
|
+
await Promise.all(recKeys.map(function (k) {
|
|
487
|
+
return fUtils.createDirRecursively(path_1.default.join(tablePath, recs[k].name));
|
|
488
|
+
}));
|
|
489
|
+
await (0, genericUtils_1.processBatched)(recKeys, constants_1.CONCURRENCY_RECORDS, async function (recKey) {
|
|
490
|
+
var rec = recs[recKey];
|
|
491
|
+
var recPath = path_1.default.join(tablePath, rec.name);
|
|
492
|
+
// Split server-provided metadata off from the regular files so we can
|
|
493
|
+
// track whether any regular file actually changed — metaData shouldn't
|
|
494
|
+
// be the trigger for "this record changed" since we stamp it on every
|
|
495
|
+
// touch.
|
|
496
|
+
var metadataFiles = [];
|
|
497
|
+
var regularFiles = [];
|
|
498
|
+
for (var mi = 0; mi < rec.files.length; mi++) {
|
|
499
|
+
var rf = rec.files[mi];
|
|
500
|
+
if (rf.name === "metaData" && rf.type === "json") {
|
|
501
|
+
metadataFiles.push(rf);
|
|
502
|
+
}
|
|
503
|
+
else {
|
|
504
|
+
regularFiles.push(rf);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
var results = await (0, genericUtils_1.allSettledBatched)(regularFiles, constants_1.CONCURRENCY_FILES, async function (file) {
|
|
508
|
+
if (forceWrite) {
|
|
509
|
+
await forceWriter(file, recPath);
|
|
510
|
+
return true;
|
|
511
|
+
}
|
|
512
|
+
return fUtils.writeSNFileIfDifferent(file, recPath);
|
|
513
|
+
});
|
|
514
|
+
var anyChanged = false;
|
|
515
|
+
for (var f = 0; f < results.length; f++) {
|
|
516
|
+
var res = results[f];
|
|
517
|
+
if (res.status === "rejected") {
|
|
518
|
+
FileLogger_1.fileLogger.error("File write failed: " + res.reason);
|
|
519
|
+
continue;
|
|
520
|
+
}
|
|
521
|
+
if (res.value) {
|
|
522
|
+
anyChanged = true;
|
|
523
|
+
writtenCount++;
|
|
524
|
+
}
|
|
525
|
+
else {
|
|
526
|
+
unchangedCount++;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
// Only touch metaData when at least one regular file in the record
|
|
530
|
+
// actually changed. Avoids rewriting _lastUpdatedOn for records that
|
|
531
|
+
// were already in sync with the instance. Prefer the server-provided
|
|
532
|
+
// metadata (full field snapshot) over a stub; fall back to a stub only
|
|
533
|
+
// when the server didn't send metadata at all.
|
|
534
|
+
if (anyChanged || forceWrite) {
|
|
535
|
+
let metadataFile;
|
|
536
|
+
if (metadataFiles.length > 0 && metadataFiles[0].content) {
|
|
537
|
+
metadataFile = stampMetadataContent(metadataFiles[0]);
|
|
538
|
+
}
|
|
539
|
+
else {
|
|
540
|
+
metadataFile = {
|
|
541
|
+
name: "metaData",
|
|
542
|
+
type: "json",
|
|
543
|
+
content: JSON.stringify({ _lastUpdatedOn: new Date().toISOString() }, null, 2),
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
await forceWriter(metadataFile, recPath);
|
|
547
|
+
}
|
|
548
|
+
// Strip content from manifest entries to keep memory bounded.
|
|
549
|
+
rec.files = rec.files.map(function (file) {
|
|
550
|
+
var copy = Object.assign({}, file);
|
|
551
|
+
delete copy.content;
|
|
552
|
+
return copy;
|
|
553
|
+
});
|
|
554
|
+
progress.tick();
|
|
555
|
+
});
|
|
556
|
+
});
|
|
557
|
+
FileLogger_1.fileLogger.debug("Refresh complete: " + writtenCount + " written, " + unchangedCount + " unchanged");
|
|
558
|
+
if (writtenCount > 0) {
|
|
559
|
+
Logger_1.logger.info("Refreshed " + writtenCount + " file(s) from instance" +
|
|
560
|
+
(unchangedCount > 0 ? " (" + unchangedCount + " already in sync)" : ""));
|
|
561
|
+
}
|
|
562
|
+
else {
|
|
563
|
+
Logger_1.logger.debug("No file changes detected from instance (" + unchangedCount + " checked)");
|
|
564
|
+
}
|
|
565
|
+
if (options.benchmarkCollector) {
|
|
566
|
+
options.benchmarkCollector.endScope(writtenCount, unchangedCount);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
catch (e) {
|
|
570
|
+
if (options.benchmarkCollector) {
|
|
571
|
+
options.benchmarkCollector.endScope(0, 0);
|
|
572
|
+
}
|
|
573
|
+
throw e;
|
|
574
|
+
}
|
|
575
|
+
};
|
|
576
|
+
exports.refreshAllFiles = refreshAllFiles;
|
|
577
|
+
const processMissingFiles = async (newManifest, sourcePath) => {
|
|
578
|
+
try {
|
|
579
|
+
const missing = await (0, exports.findMissingFiles)(newManifest, sourcePath);
|
|
580
|
+
const missingTableCount = Object.keys(missing).length;
|
|
581
|
+
if (missingTableCount === 0)
|
|
582
|
+
return;
|
|
583
|
+
FileLogger_1.fileLogger.debug("Downloading missing files from " + missingTableCount + " tables");
|
|
584
|
+
const { tableOptions = {} } = ConfigManager.getConfig();
|
|
585
|
+
const client = (0, snClient_1.defaultClient)();
|
|
586
|
+
// Chunk the bulkDownload request: ServiceNow rejects REST payloads > 10 MB,
|
|
587
|
+
// so send table batches and merge the results before processing.
|
|
588
|
+
const tableNames = Object.keys(missing);
|
|
589
|
+
const totalChunks = Math.ceil(tableNames.length / BULK_DOWNLOAD_TABLE_CHUNK_SIZE);
|
|
590
|
+
const filesToProcess = {};
|
|
591
|
+
for (var i = 0; i < tableNames.length; i += BULK_DOWNLOAD_TABLE_CHUNK_SIZE) {
|
|
592
|
+
const chunkTableNames = tableNames.slice(i, i + BULK_DOWNLOAD_TABLE_CHUNK_SIZE);
|
|
593
|
+
const chunkMissing = {};
|
|
594
|
+
for (var j = 0; j < chunkTableNames.length; j++) {
|
|
595
|
+
chunkMissing[chunkTableNames[j]] = missing[chunkTableNames[j]];
|
|
596
|
+
}
|
|
597
|
+
const batchNum = Math.floor(i / BULK_DOWNLOAD_TABLE_CHUNK_SIZE) + 1;
|
|
598
|
+
FileLogger_1.fileLogger.debug("Bulk download batch " + batchNum + "/" + totalChunks +
|
|
599
|
+
" (" + chunkTableNames.length + " tables): " + chunkTableNames.join(", "));
|
|
600
|
+
const chunkResult = await (0, snClient_1.unwrapSNResponse)(client.getMissingFiles(chunkMissing, tableOptions));
|
|
601
|
+
// Chunks are partitioned by table key, so merging is a simple assign.
|
|
602
|
+
for (var tableName in chunkResult) {
|
|
603
|
+
filesToProcess[tableName] = chunkResult[tableName];
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
var recordCount = countRecordsInTables(filesToProcess);
|
|
607
|
+
var progress = createScopeProgress(Logger_1.logger.getLogLevel(), {
|
|
608
|
+
scope: newManifest.scope || "default",
|
|
609
|
+
total: recordCount,
|
|
610
|
+
});
|
|
611
|
+
await processTablesInManifest(filesToProcess, false, sourcePath, progress.tick, newManifest.scope);
|
|
612
|
+
}
|
|
613
|
+
catch (e) {
|
|
614
|
+
throw e;
|
|
615
|
+
}
|
|
616
|
+
};
|
|
617
|
+
exports.processMissingFiles = processMissingFiles;
|
|
618
|
+
const groupAppFiles = (fileCtxs) => {
|
|
619
|
+
const combinedFiles = fileCtxs.reduce((groupMap, cur) => {
|
|
620
|
+
const { tableName, targetField, sys_id } = cur;
|
|
621
|
+
const key = `${tableName}-${sys_id}`;
|
|
622
|
+
const entry = groupMap[key] ?? {
|
|
623
|
+
table: tableName,
|
|
624
|
+
sysId: sys_id,
|
|
625
|
+
fields: {},
|
|
626
|
+
};
|
|
627
|
+
const newEntry = {
|
|
628
|
+
...entry,
|
|
629
|
+
fields: { ...entry.fields, [targetField]: cur ?? "" },
|
|
630
|
+
};
|
|
631
|
+
return { ...groupMap, [key]: newEntry };
|
|
632
|
+
}, {});
|
|
633
|
+
return Object.values(combinedFiles);
|
|
634
|
+
};
|
|
635
|
+
exports.groupAppFiles = groupAppFiles;
|
|
636
|
+
const getAppFileList = async (paths) => {
|
|
637
|
+
const validPaths = typeof paths === "object"
|
|
638
|
+
? paths
|
|
639
|
+
: await fUtils.encodedPathsToFilePaths(paths);
|
|
640
|
+
const appFileCtxs = [];
|
|
641
|
+
validPaths.forEach(function (filePath) {
|
|
642
|
+
var result = fUtils.getFileContextWithSkipReason(filePath);
|
|
643
|
+
if (result.context) {
|
|
644
|
+
appFileCtxs.push(result.context);
|
|
645
|
+
}
|
|
646
|
+
else {
|
|
647
|
+
var reason = result.skipReason || "unknown";
|
|
648
|
+
if (reason === "not in manifest") {
|
|
649
|
+
Logger_1.logger.info(`Skipped: ${filePath} (${reason})`);
|
|
650
|
+
}
|
|
651
|
+
else {
|
|
652
|
+
Logger_1.logger.warn(`Skipped: ${filePath} (${reason})`);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
});
|
|
656
|
+
return (0, exports.groupAppFiles)(appFileCtxs);
|
|
657
|
+
};
|
|
658
|
+
exports.getAppFileList = getAppFileList;
|
|
659
|
+
const buildRec = async (rec) => {
|
|
660
|
+
const fields = Object.keys(rec.fields);
|
|
661
|
+
const buildPromises = fields.map((field) => {
|
|
662
|
+
return PluginManager_1.default.getFinalFileContents(rec.fields[field]);
|
|
663
|
+
});
|
|
664
|
+
const builtFiles = await (0, genericUtils_1.allSettled)(buildPromises);
|
|
665
|
+
const buildSuccess = !builtFiles.find((buildRes) => buildRes.status === "rejected");
|
|
666
|
+
if (!buildSuccess) {
|
|
667
|
+
return {
|
|
668
|
+
success: false,
|
|
669
|
+
message: (0, genericUtils_1.aggregateErrorMessages)(builtFiles
|
|
670
|
+
.filter((b) => b.status === "rejected")
|
|
671
|
+
.map((b) => b.reason), "Failed to build!", (_, index) => `${index}`),
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
const builtRec = builtFiles.reduce((acc, buildRes, index) => {
|
|
675
|
+
const { value: content } = buildRes;
|
|
676
|
+
const fieldName = fields[index];
|
|
677
|
+
return { ...acc, [fieldName]: content };
|
|
678
|
+
}, {});
|
|
679
|
+
return {
|
|
680
|
+
success: true,
|
|
681
|
+
builtRec,
|
|
682
|
+
};
|
|
683
|
+
};
|
|
684
|
+
const pushRec = async (client, table, sysId, builtRec, summary, scope, updateSetConfig) => {
|
|
685
|
+
const recSummary = summary ?? `${table} > ${sysId}`;
|
|
686
|
+
try {
|
|
687
|
+
// Use the batch-level config passed from pushFiles() to avoid re-reading per record
|
|
688
|
+
const config = updateSetConfig || {};
|
|
689
|
+
const updateSet = scope ? config[scope] : undefined;
|
|
690
|
+
const pushFn = updateSet
|
|
691
|
+
? () => {
|
|
692
|
+
Logger_1.logger.debug(`Pushing ${recSummary} via update set: ${updateSet.name}`);
|
|
693
|
+
return client.pushWithUpdateSet(updateSet.sys_id, table, sysId, builtRec);
|
|
694
|
+
}
|
|
695
|
+
: () => client.updateRecord(table, sysId, builtRec);
|
|
696
|
+
const pushRes = await (0, snClient_1.retryOnHttpErr)(pushFn, recSummary);
|
|
697
|
+
return (0, snClient_1.processPushResponse)(pushRes, recSummary);
|
|
698
|
+
}
|
|
699
|
+
catch (e) {
|
|
700
|
+
let message;
|
|
701
|
+
if (e instanceof Error)
|
|
702
|
+
message = e.message;
|
|
703
|
+
else
|
|
704
|
+
message = String(e);
|
|
705
|
+
const errMsg = message || "Too many retries";
|
|
706
|
+
return { success: false, message: `${recSummary} : ${errMsg}` };
|
|
707
|
+
}
|
|
708
|
+
};
|
|
709
|
+
const pushFiles = async (recs) => {
|
|
710
|
+
const client = (0, snClient_1.defaultClient)();
|
|
711
|
+
const updateSetConfig = getUpdateSetConfig();
|
|
712
|
+
const hasUpdateSets = Object.keys(updateSetConfig).length > 0;
|
|
713
|
+
if (hasUpdateSets) {
|
|
714
|
+
const activeScopes = Object.entries(updateSetConfig)
|
|
715
|
+
.map(([scope, us]) => `${scope} -> ${us.name}`)
|
|
716
|
+
.join(", ");
|
|
717
|
+
Logger_1.logger.info(`Update set routing active: ${activeScopes}`);
|
|
718
|
+
}
|
|
719
|
+
// Pre-resolve read-only table sets for every scope present in this batch.
|
|
720
|
+
// Cheaper than re-resolving per record. Tables flagged in _readOnlyTables
|
|
721
|
+
// are pulled normally but pushes are skipped.
|
|
722
|
+
const readOnlyByScope = {};
|
|
723
|
+
for (const rec of recs) {
|
|
724
|
+
const fieldNames = Object.keys(rec.fields);
|
|
725
|
+
if (fieldNames.length === 0)
|
|
726
|
+
continue;
|
|
727
|
+
const scope = rec.fields[fieldNames[0]].scope;
|
|
728
|
+
if (scope && !readOnlyByScope[scope]) {
|
|
729
|
+
readOnlyByScope[scope] = ConfigManager.getReadOnlyTablesForScope(scope);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
const announcedSkipTables = {};
|
|
733
|
+
const tick = getProgTick(Logger_1.logger.getLogLevel(), recs.length * 2) || (() => { });
|
|
734
|
+
const results = await (0, genericUtils_1.allSettledBatched)(recs, constants_1.CONCURRENCY_PUSH, async function (rec) {
|
|
735
|
+
const fieldNames = Object.keys(rec.fields);
|
|
736
|
+
const firstField = rec.fields[fieldNames[0]];
|
|
737
|
+
const recSummary = (0, exports.summarizeRecord)(rec.table, firstField.name);
|
|
738
|
+
const scope = firstField.scope;
|
|
739
|
+
const readOnlySet = scope ? readOnlyByScope[scope] : undefined;
|
|
740
|
+
if (readOnlySet && readOnlySet.has(rec.table)) {
|
|
741
|
+
tick();
|
|
742
|
+
tick();
|
|
743
|
+
const skipKey = `${scope}:${rec.table}`;
|
|
744
|
+
if (!announcedSkipTables[skipKey]) {
|
|
745
|
+
announcedSkipTables[skipKey] = true;
|
|
746
|
+
Logger_1.logger.info(`Read-only table ${rec.table} in scope ${scope}: push skipped`);
|
|
747
|
+
}
|
|
748
|
+
return { success: true, message: `${recSummary} : skipped (read-only table)` };
|
|
749
|
+
}
|
|
750
|
+
const buildRes = await buildRec(rec);
|
|
751
|
+
tick();
|
|
752
|
+
if (!buildRes.success) {
|
|
753
|
+
tick();
|
|
754
|
+
return { success: false, message: `${recSummary} : ${buildRes.message}` };
|
|
755
|
+
}
|
|
756
|
+
const pushRes = await pushRec(client, rec.table, rec.sysId, buildRes.builtRec, recSummary, scope, updateSetConfig);
|
|
757
|
+
tick();
|
|
758
|
+
return pushRes;
|
|
759
|
+
});
|
|
760
|
+
return results.map(function (result) {
|
|
761
|
+
if (result.status === "fulfilled") {
|
|
762
|
+
return result.value;
|
|
763
|
+
}
|
|
764
|
+
return { success: false, message: `Push failed: ${result.reason}` };
|
|
765
|
+
});
|
|
766
|
+
};
|
|
767
|
+
exports.pushFiles = pushFiles;
|
|
768
|
+
const summarizeRecord = (table, recDescriptor) => `${table} > ${recDescriptor}`;
|
|
769
|
+
exports.summarizeRecord = summarizeRecord;
|
|
770
|
+
const createScopeProgress = (logLevel, options) => {
|
|
771
|
+
if (logLevel !== "info" || options.total === 0) {
|
|
772
|
+
return { tick: function () { }, setTotal: function () { } };
|
|
773
|
+
}
|
|
774
|
+
var progBar = new progress_1.default(":scope :bar :current/:total (:percent)", {
|
|
775
|
+
total: options.total,
|
|
776
|
+
width: 40,
|
|
777
|
+
complete: "=",
|
|
778
|
+
incomplete: "-",
|
|
779
|
+
});
|
|
780
|
+
return {
|
|
781
|
+
tick: function () {
|
|
782
|
+
progBar.tick({ scope: options.scope });
|
|
783
|
+
},
|
|
784
|
+
setTotal: function (n) {
|
|
785
|
+
progBar.total = n;
|
|
786
|
+
},
|
|
787
|
+
};
|
|
788
|
+
};
|
|
789
|
+
const getProgTick = (logLevel, total) => {
|
|
790
|
+
if (logLevel === "info") {
|
|
791
|
+
const progBar = new progress_1.default(":bar (:percent)", {
|
|
792
|
+
total,
|
|
793
|
+
width: 60,
|
|
794
|
+
});
|
|
795
|
+
return () => {
|
|
796
|
+
progBar.tick();
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
// no-op at other log levels
|
|
800
|
+
return undefined;
|
|
801
|
+
};
|
|
802
|
+
const writeBuildFile = async (preBuild, buildRes, summary) => {
|
|
803
|
+
const { fields, table, sysId } = preBuild;
|
|
804
|
+
const recSummary = summary ?? `${table} > ${sysId}`;
|
|
805
|
+
const sourcePath = ConfigManager.getSourcePath();
|
|
806
|
+
const buildPath = ConfigManager.getBuildPath();
|
|
807
|
+
const fieldNames = Object.keys(fields);
|
|
808
|
+
const writePromises = fieldNames.map(async (field) => {
|
|
809
|
+
const fieldCtx = fields[field];
|
|
810
|
+
const srcFilePath = fieldCtx.filePath;
|
|
811
|
+
const relativePath = path_1.default.relative(sourcePath, srcFilePath);
|
|
812
|
+
const relPathNoExt = relativePath.split(".").slice(0, -1).join();
|
|
813
|
+
const buildExt = fUtils.getBuildExt(fieldCtx.tableName, fieldCtx.name, fieldCtx.targetField, fieldCtx.scope);
|
|
814
|
+
const relPathNewExt = `${relPathNoExt}.${buildExt}`;
|
|
815
|
+
const buildFilePath = path_1.default.join(buildPath, relPathNewExt);
|
|
816
|
+
await fUtils.createDirRecursively(path_1.default.dirname(buildFilePath));
|
|
817
|
+
const writeResult = await fUtils.writeFileForce(buildFilePath, buildRes.builtRec[fieldCtx.targetField]);
|
|
818
|
+
return writeResult;
|
|
819
|
+
});
|
|
820
|
+
try {
|
|
821
|
+
await (0, genericUtils_1.processBatched)(fieldNames, constants_1.CONCURRENCY_FILES, async function (field) {
|
|
822
|
+
const fieldCtx = fields[field];
|
|
823
|
+
const srcFilePath = fieldCtx.filePath;
|
|
824
|
+
const relativePath = path_1.default.relative(sourcePath, srcFilePath);
|
|
825
|
+
const relPathNoExt = relativePath.split(".").slice(0, -1).join();
|
|
826
|
+
const buildExt = fUtils.getBuildExt(fieldCtx.tableName, fieldCtx.name, fieldCtx.targetField);
|
|
827
|
+
const relPathNewExt = `${relPathNoExt}.${buildExt}`;
|
|
828
|
+
const buildFilePath = path_1.default.join(buildPath, relPathNewExt);
|
|
829
|
+
await fUtils.createDirRecursively(path_1.default.dirname(buildFilePath));
|
|
830
|
+
await fUtils.writeFileForce(buildFilePath, buildRes.builtRec[fieldCtx.targetField]);
|
|
831
|
+
});
|
|
832
|
+
return { success: true, message: `${recSummary} built successfully` };
|
|
833
|
+
}
|
|
834
|
+
catch (e) {
|
|
835
|
+
return {
|
|
836
|
+
success: false,
|
|
837
|
+
message: `${recSummary} : ${e}`,
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
};
|
|
841
|
+
const buildFiles = async (fileList) => {
|
|
842
|
+
const tick = getProgTick(Logger_1.logger.getLogLevel(), fileList.length * 2) || (() => { });
|
|
843
|
+
const results = await (0, genericUtils_1.allSettledBatched)(fileList, constants_1.CONCURRENCY_BUILD, async function (rec) {
|
|
844
|
+
const { fields, table } = rec;
|
|
845
|
+
const fieldNames = Object.keys(fields);
|
|
846
|
+
const recSummary = (0, exports.summarizeRecord)(table, fields[fieldNames[0]].name);
|
|
847
|
+
const buildRes = await buildRec(rec);
|
|
848
|
+
tick();
|
|
849
|
+
if (!buildRes.success) {
|
|
850
|
+
tick();
|
|
851
|
+
return { success: false, message: `${recSummary} : ${buildRes.message}` };
|
|
852
|
+
}
|
|
853
|
+
// writeFile
|
|
854
|
+
const writeRes = await writeBuildFile(rec, buildRes, recSummary);
|
|
855
|
+
tick();
|
|
856
|
+
return writeRes;
|
|
857
|
+
});
|
|
858
|
+
return results.map(function (result) {
|
|
859
|
+
if (result.status === "fulfilled") {
|
|
860
|
+
return result.value;
|
|
861
|
+
}
|
|
862
|
+
return { success: false, message: `Build failed: ${result.reason}` };
|
|
863
|
+
});
|
|
864
|
+
};
|
|
865
|
+
exports.buildFiles = buildFiles;
|
|
866
|
+
const swapScope = async (currentScope) => {
|
|
867
|
+
try {
|
|
868
|
+
const client = (0, snClient_1.defaultClient)();
|
|
869
|
+
const scopeId = await (0, snClient_1.unwrapTableAPIFirstItem)(client.getScopeId(currentScope), "sys_id");
|
|
870
|
+
await swapServerScope(scopeId);
|
|
871
|
+
const scopeObj = await (0, snClient_1.unwrapSNResponse)(client.getCurrentScope());
|
|
872
|
+
return scopeObj;
|
|
873
|
+
}
|
|
874
|
+
catch (e) {
|
|
875
|
+
throw e;
|
|
876
|
+
}
|
|
877
|
+
};
|
|
878
|
+
exports.swapScope = swapScope;
|
|
879
|
+
const swapServerScope = async (scopeId) => {
|
|
880
|
+
try {
|
|
881
|
+
const client = (0, snClient_1.defaultClient)();
|
|
882
|
+
const userSysId = await (0, snClient_1.unwrapTableAPIFirstItem)(client.getUserSysId(), "sys_id");
|
|
883
|
+
const curAppUserPrefId = (await (0, snClient_1.unwrapTableAPIFirstItem)(client.getCurrentAppUserPrefSysId(userSysId), "sys_id")) || "";
|
|
884
|
+
// If not user pref record exists, create it.
|
|
885
|
+
if (curAppUserPrefId !== "")
|
|
886
|
+
await client.updateCurrentAppUserPref(scopeId, curAppUserPrefId);
|
|
887
|
+
else
|
|
888
|
+
await client.createCurrentAppUserPref(scopeId, userSysId);
|
|
889
|
+
}
|
|
890
|
+
catch (e) {
|
|
891
|
+
let message;
|
|
892
|
+
if (e instanceof Error)
|
|
893
|
+
message = e.message;
|
|
894
|
+
else
|
|
895
|
+
message = String(e);
|
|
896
|
+
Logger_1.logger.error(message);
|
|
897
|
+
throw e;
|
|
898
|
+
}
|
|
899
|
+
};
|
|
900
|
+
/**
|
|
901
|
+
* Creates a new update set and assigns it to the current user.
|
|
902
|
+
* @param updateSetName - does not create update set if value is blank
|
|
903
|
+
* @param scope - optional scope name (e.g. x_cadso_work) to create the update set in
|
|
904
|
+
*/
|
|
905
|
+
const createAndAssignUpdateSet = async (updateSetName = "", scope) => {
|
|
906
|
+
Logger_1.logger.info(`Update Set Name: ${updateSetName}` + (scope ? ` (scope: ${scope})` : ""));
|
|
907
|
+
const client = (0, snClient_1.defaultClient)();
|
|
908
|
+
var scopeSysId;
|
|
909
|
+
if (scope) {
|
|
910
|
+
var scopeResult = await (0, snClient_1.unwrapSNResponse)(client.getScopeId(scope));
|
|
911
|
+
if (scopeResult.length > 0) {
|
|
912
|
+
scopeSysId = scopeResult[0].sys_id;
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
const { sys_id: updateSetSysId } = await (0, snClient_1.unwrapSNResponse)(client.createUpdateSet(updateSetName, scopeSysId));
|
|
916
|
+
const userSysId = await (0, snClient_1.unwrapTableAPIFirstItem)(client.getUserSysId(), "sys_id");
|
|
917
|
+
const curUpdateSetUserPrefId = await (0, snClient_1.unwrapTableAPIFirstItem)(client.getCurrentUpdateSetUserPref(userSysId), "sys_id");
|
|
918
|
+
if (curUpdateSetUserPrefId !== "") {
|
|
919
|
+
await client.updateCurrentUpdateSetUserPref(updateSetSysId, curUpdateSetUserPrefId);
|
|
920
|
+
}
|
|
921
|
+
else {
|
|
922
|
+
await client.createCurrentUpdateSetUserPref(updateSetSysId, userSysId);
|
|
923
|
+
}
|
|
924
|
+
return {
|
|
925
|
+
name: updateSetName,
|
|
926
|
+
id: updateSetSysId,
|
|
927
|
+
};
|
|
928
|
+
};
|
|
929
|
+
exports.createAndAssignUpdateSet = createAndAssignUpdateSet;
|