@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.
Files changed (78) hide show
  1. package/README.md +6 -0
  2. package/dist/FileLogger.js +207 -0
  3. package/dist/FileUtils.js +387 -0
  4. package/dist/Logger.js +88 -0
  5. package/dist/MultiScopeWatcher.js +615 -0
  6. package/dist/PluginManager.js +125 -0
  7. package/dist/_defaultOptionsOLD.js +44 -0
  8. package/dist/allScopesCommands.js +418 -0
  9. package/dist/appUtils.js +929 -0
  10. package/dist/benchmark.js +104 -0
  11. package/dist/bootstrap.js +62 -0
  12. package/dist/claudeCommand.js +60 -0
  13. package/dist/clickupCommands.js +472 -0
  14. package/dist/clickupPushHelper.js +33 -0
  15. package/dist/commander.js +466 -0
  16. package/dist/commands.js +378 -0
  17. package/dist/config.js +697 -0
  18. package/dist/constants.js +15 -0
  19. package/dist/createRecordCommand.js +314 -0
  20. package/dist/dashboardCommand.js +38 -0
  21. package/dist/defaultOptions.js +14 -0
  22. package/dist/deleteRecordCommand.js +282 -0
  23. package/dist/flowDesigner/values.js +55 -0
  24. package/dist/genericUtils.js +189 -0
  25. package/dist/gitUtils.js +99 -0
  26. package/dist/index.js +35 -0
  27. package/dist/initSystem/corePlugin.js +344 -0
  28. package/dist/initSystem/discovery.js +107 -0
  29. package/dist/initSystem/orchestrator.js +408 -0
  30. package/dist/logMessages.js +82 -0
  31. package/dist/loginCommand.js +20 -0
  32. package/dist/migrateCommand.js +123 -0
  33. package/dist/projectFiles.js +32 -0
  34. package/dist/recentEdits.js +66 -0
  35. package/dist/schemaCommand.js +85 -0
  36. package/dist/snClient.js +519 -0
  37. package/dist/tests/benchmarkRefresh.test.js +265 -0
  38. package/dist/tests/clickupCommands.test.js +384 -0
  39. package/dist/tests/clickupPushHelper.test.js +124 -0
  40. package/dist/tests/configFallback.test.js +68 -0
  41. package/dist/tests/cred.test.js +59 -0
  42. package/dist/tests/discovery.test.js +67 -0
  43. package/dist/tests/ensureUpdateSetWarnings.test.js +218 -0
  44. package/dist/tests/errorLogLevels.test.js +273 -0
  45. package/dist/tests/example.test.js +6 -0
  46. package/dist/tests/fileContextSkipReason.test.js +116 -0
  47. package/dist/tests/globalDebounce.test.js +308 -0
  48. package/dist/tests/migrateCommand.test.js +113 -0
  49. package/dist/tests/multi-scope-watcher.test.js +558 -0
  50. package/dist/tests/orchestrator.test.js +23 -0
  51. package/dist/tests/pushFiles.test.js +248 -0
  52. package/dist/tests/rateLimitCoordination.test.js +271 -0
  53. package/dist/tests/resolveConfigReadOnly.test.js +67 -0
  54. package/dist/tests/retryOnHttpErr.test.js +154 -0
  55. package/dist/tests/scopeCaching.test.js +115 -0
  56. package/dist/tests/serializeUpdateSetConfig.test.js +325 -0
  57. package/dist/tests/snClient.fallback.test.js +120 -0
  58. package/dist/tests/syncManifestRefresh.test.js +287 -0
  59. package/dist/tests/syncManifestWhitelist.test.js +192 -0
  60. package/dist/tests/taskClear.test.js +170 -0
  61. package/dist/tests/taskStaleness.test.js +220 -0
  62. package/dist/tests/unwrapSNResponseError.test.js +145 -0
  63. package/dist/tests/v2Values.test.js +79 -0
  64. package/dist/tests/validateTaskId.test.js +304 -0
  65. package/dist/tests/verifyUpdateSetSwitch.test.js +277 -0
  66. package/dist/tests/writeEnvVar.test.js +119 -0
  67. package/dist/updateSetCommands.js +702 -0
  68. package/dist/wizard.js +40 -0
  69. package/package.json +69 -0
  70. package/skills/dove-configure-pipeline.md +158 -0
  71. package/skills/dove-create-plugin.md +174 -0
  72. package/skills/dove-create-record.md +135 -0
  73. package/skills/dove-debug-build.md +117 -0
  74. package/skills/dove-delete-record.md +104 -0
  75. package/skills/dove-manage-tables.md +174 -0
  76. package/skills/dove-manage-update-sets.md +88 -0
  77. package/skills/dove-setup-project.md +154 -0
  78. package/skills/dove-troubleshoot-sync.md +113 -0
@@ -0,0 +1,66 @@
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
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.writeRecentEdit = writeRecentEdit;
37
+ const fs = __importStar(require("fs"));
38
+ const projectFiles_1 = require("./projectFiles");
39
+ const MAX_ENTRIES = 5;
40
+ function getFilePath() {
41
+ return (0, projectFiles_1.getRecentEditsPath)();
42
+ }
43
+ function writeRecentEdit(context) {
44
+ var filePath = getFilePath();
45
+ var edits = [];
46
+ try {
47
+ if (fs.existsSync(filePath)) {
48
+ edits = JSON.parse(fs.readFileSync(filePath, "utf-8"));
49
+ }
50
+ }
51
+ catch (e) {
52
+ edits = [];
53
+ }
54
+ edits.unshift({
55
+ tableName: context.tableName,
56
+ name: context.name,
57
+ targetField: context.targetField,
58
+ sys_id: context.sys_id,
59
+ scope: context.scope,
60
+ timestamp: new Date().toISOString(),
61
+ });
62
+ if (edits.length > MAX_ENTRIES) {
63
+ edits = edits.slice(0, MAX_ENTRIES);
64
+ }
65
+ fs.writeFileSync(filePath, JSON.stringify(edits, null, 2));
66
+ }
@@ -0,0 +1,85 @@
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.schemaPullCommand = schemaPullCommand;
40
+ const dovetail_schema_1 = require("@tenonhq/dovetail-schema");
41
+ const Logger_1 = require("./Logger");
42
+ const ConfigManager = __importStar(require("./config"));
43
+ const path_1 = __importDefault(require("path"));
44
+ async function schemaPullCommand(args) {
45
+ const { SN_USER = "", SN_PASSWORD = "", SN_INSTANCE = "" } = process.env;
46
+ if (!SN_USER || !SN_PASSWORD || !SN_INSTANCE) {
47
+ throw new Error("Missing ServiceNow credentials. Ensure SN_INSTANCE, SN_USER, and SN_PASSWORD are set in your .env file or environment.");
48
+ }
49
+ const config = ConfigManager.getConfig();
50
+ const configScopes = config.scopes ? Object.keys(config.scopes) : [];
51
+ if (configScopes.length === 0) {
52
+ throw new Error("No scopes configured in dove.config.js. Add scopes to the 'scopes' object in your configuration.");
53
+ }
54
+ // If --scope flag is provided, filter to that single scope
55
+ let scopes = configScopes;
56
+ if (args.scope) {
57
+ if (!configScopes.includes(args.scope)) {
58
+ throw new Error(`Scope "${args.scope}" is not configured in dove.config.js. Available scopes: ${configScopes.join(", ")}`);
59
+ }
60
+ scopes = [args.scope];
61
+ }
62
+ const rootDir = ConfigManager.getRootDir();
63
+ const outputDir = args.output
64
+ ? path_1.default.resolve(args.output)
65
+ : path_1.default.join(rootDir, "schema");
66
+ try {
67
+ const index = await (0, dovetail_schema_1.pullSchema)({
68
+ instance: SN_INSTANCE,
69
+ username: SN_USER,
70
+ password: SN_PASSWORD,
71
+ outputDir,
72
+ scopes,
73
+ });
74
+ Logger_1.logger.success(`Schema pull complete! ${index.total_tables} tables across ${index.applications.length} applications written to ${outputDir}`);
75
+ }
76
+ catch (e) {
77
+ let message;
78
+ if (e instanceof Error)
79
+ message = e.message;
80
+ else
81
+ message = String(e);
82
+ Logger_1.logger.error("Schema pull failed: " + message);
83
+ throw e;
84
+ }
85
+ }
@@ -0,0 +1,519 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.unwrapSNResponse = exports.defaultClient = exports.snClient = exports.processPushResponse = exports.retryOnHttpErr = exports.retryOnErr = void 0;
7
+ exports.setBenchmarkSink = setBenchmarkSink;
8
+ exports._resetDovetailApiFallback = _resetDovetailApiFallback;
9
+ exports._isUsingLegacyClaudePath = _isUsingLegacyClaudePath;
10
+ exports._callDovetailApi = _callDovetailApi;
11
+ exports.unwrapTableAPIFirstItem = unwrapTableAPIFirstItem;
12
+ const axios_1 = __importDefault(require("axios"));
13
+ const axios_cookiejar_support_1 = require("axios-cookiejar-support");
14
+ const axios_rate_limit_1 = __importDefault(require("axios-rate-limit"));
15
+ const tough_cookie_1 = require("tough-cookie");
16
+ const genericUtils_1 = require("./genericUtils");
17
+ const Logger_1 = require("./Logger");
18
+ const FileLogger_1 = require("./FileLogger");
19
+ // Benchmark sink. Null when --benchmark is off; zero overhead in that case.
20
+ let _benchmarkSink = null;
21
+ function setBenchmarkSink(sink) {
22
+ _benchmarkSink = sink;
23
+ }
24
+ // Local helper to strip _ directive keys before sending to ServiceNow API.
25
+ // Defined here (not imported from config.ts) to avoid circular dependencies.
26
+ function _stripUnderscoreKeys(obj) {
27
+ var result = {};
28
+ var keys = Object.keys(obj || {});
29
+ for (var i = 0; i < keys.length; i++) {
30
+ if (keys[i].charAt(0) !== "_") {
31
+ result[keys[i]] = obj[keys[i]];
32
+ }
33
+ }
34
+ return result;
35
+ }
36
+ const retryOnErr = async (f, allowedRetries, msBetween = 0, onRetry) => {
37
+ try {
38
+ return await f();
39
+ }
40
+ catch (e) {
41
+ const newRetries = allowedRetries - 1;
42
+ if (newRetries <= 0) {
43
+ throw e;
44
+ }
45
+ if (onRetry) {
46
+ onRetry(newRetries);
47
+ }
48
+ await (0, genericUtils_1.wait)(msBetween);
49
+ return (0, exports.retryOnErr)(f, newRetries, msBetween, onRetry);
50
+ }
51
+ };
52
+ exports.retryOnErr = retryOnErr;
53
+ function _getHttpStatus(e) {
54
+ if (axios_1.default.isAxiosError(e) && e.response) {
55
+ return e.response.status;
56
+ }
57
+ return undefined;
58
+ }
59
+ // Dovetail server-side Scripted REST API was renamed from "Claude" (path
60
+ // /api/cadso/claude/*) to "Dovetail" (path /api/cadso/dovetail/*). On instances
61
+ // where the rename hasn't been imported yet, the new path returns 404. We try
62
+ // the dovetail path first; if it 404s we fall back to the legacy claude path
63
+ // for the rest of the session, with a one-time deprecation warning.
64
+ let _dovetailApiUseLegacyClaudePath = false;
65
+ function _dovetailEndpoint(op) {
66
+ return _dovetailApiUseLegacyClaudePath
67
+ ? "api/cadso/claude/" + op
68
+ : "api/cadso/dovetail/" + op;
69
+ }
70
+ // Exported for tests only: reset the latched fallback state between test cases.
71
+ function _resetDovetailApiFallback() {
72
+ _dovetailApiUseLegacyClaudePath = false;
73
+ }
74
+ // Exported for tests only: inspect the latched fallback state.
75
+ function _isUsingLegacyClaudePath() {
76
+ return _dovetailApiUseLegacyClaudePath;
77
+ }
78
+ async function _callDovetailApi(op, call) {
79
+ try {
80
+ return await call(_dovetailEndpoint(op));
81
+ }
82
+ catch (e) {
83
+ if (!_dovetailApiUseLegacyClaudePath && _getHttpStatus(e) === 404) {
84
+ Logger_1.logger.warn("[deprecation] /api/cadso/dovetail/" + op +
85
+ " returned 404. Falling back to legacy /api/cadso/claude/" + op +
86
+ ". Re-import the Dovetail Scripted REST API XML on your ServiceNow instance to silence this warning.");
87
+ _dovetailApiUseLegacyClaudePath = true;
88
+ return await call(_dovetailEndpoint(op));
89
+ }
90
+ throw e;
91
+ }
92
+ }
93
+ function _getRetryAfterMs(e) {
94
+ if (axios_1.default.isAxiosError(e) && e.response && e.response.headers) {
95
+ var retryAfter = e.response.headers["retry-after"];
96
+ if (retryAfter) {
97
+ var seconds = Number(retryAfter);
98
+ if (!isNaN(seconds) && seconds > 0) {
99
+ return seconds * 1000;
100
+ }
101
+ }
102
+ }
103
+ return 10000; // Default 10s for 429 without Retry-After
104
+ }
105
+ const retryOnHttpErr = async (f, recSummary) => {
106
+ var maxServerRetries = 3;
107
+ var backoffMs = 1000;
108
+ var attempt = 0;
109
+ while (true) {
110
+ try {
111
+ return await f();
112
+ }
113
+ catch (e) {
114
+ var status = _getHttpStatus(e);
115
+ attempt++;
116
+ // 401/403: Auth failure — fail immediately
117
+ if (status === 401 || status === 403) {
118
+ var authMsg = status === 401 ? "Unauthorized" : "Forbidden";
119
+ Logger_1.logger.error(authMsg + " (" + status + ") pushing " + recSummary +
120
+ ". Verify your ServiceNow credentials and permissions.");
121
+ throw e;
122
+ }
123
+ // 404: Record not found — fail immediately
124
+ if (status === 404) {
125
+ Logger_1.logger.error("Record not found (404) pushing " + recSummary +
126
+ ". The record may have been deleted from the instance.");
127
+ throw e;
128
+ }
129
+ // 429: Rate limited — honor Retry-After, then retry
130
+ if (status === 429) {
131
+ var retryWait = _getRetryAfterMs(e);
132
+ Logger_1.logger.warn("Rate limited (429) pushing " + recSummary +
133
+ ". Waiting " + Math.round(retryWait / 1000) + "s before retry.");
134
+ await (0, genericUtils_1.wait)(retryWait);
135
+ continue;
136
+ }
137
+ // 500/502/503: Server error — exponential backoff, max 3 retries
138
+ if (status === 500 || status === 502 || status === 503) {
139
+ if (attempt > maxServerRetries) {
140
+ Logger_1.logger.error("Server error (" + status + ") pushing " + recSummary +
141
+ " after " + maxServerRetries + " retries. Giving up.");
142
+ throw e;
143
+ }
144
+ var cappedBackoff = Math.min(backoffMs, 8000);
145
+ Logger_1.logger.warn("Server error (" + status + ") pushing " + recSummary +
146
+ ". Retrying in " + (cappedBackoff / 1000) + "s (" +
147
+ (maxServerRetries - attempt) + " retries left).");
148
+ await (0, genericUtils_1.wait)(cappedBackoff);
149
+ backoffMs = backoffMs * 2;
150
+ continue;
151
+ }
152
+ // Unknown status or non-HTTP error — retry once, then fail
153
+ if (attempt > 1) {
154
+ var errDetail = status ? "HTTP " + status : "unknown error";
155
+ Logger_1.logger.error("Push failed for " + recSummary + " (" + errDetail +
156
+ ") after 1 retry. Giving up.");
157
+ throw e;
158
+ }
159
+ var retryDetail = status ? "HTTP " + status : "unknown error";
160
+ Logger_1.logger.warn("Unexpected error (" + retryDetail + ") pushing " + recSummary +
161
+ ". Retrying once.");
162
+ await (0, genericUtils_1.wait)(1000);
163
+ }
164
+ }
165
+ };
166
+ exports.retryOnHttpErr = retryOnHttpErr;
167
+ const processPushResponse = (response, recSummary) => {
168
+ const { status } = response;
169
+ if (status === 404) {
170
+ return {
171
+ success: false,
172
+ message: `Could not find ${recSummary} on the server.`,
173
+ };
174
+ }
175
+ if (status < 200 || status > 299) {
176
+ return {
177
+ success: false,
178
+ message: `Failed to push ${recSummary}. Recieved an unexpected response (${status})`,
179
+ };
180
+ }
181
+ return {
182
+ success: true,
183
+ message: `${recSummary} pushed successfully!`,
184
+ };
185
+ };
186
+ exports.processPushResponse = processPushResponse;
187
+ const snClient = (baseURL, username, password) => {
188
+ const jar = new tough_cookie_1.CookieJar();
189
+ const rawAxios = (0, axios_cookiejar_support_1.wrapper)(axios_1.default.create({
190
+ withCredentials: true,
191
+ auth: {
192
+ username,
193
+ password,
194
+ },
195
+ headers: {
196
+ "Content-Type": "application/json",
197
+ },
198
+ baseURL,
199
+ jar,
200
+ }));
201
+ // Request interceptor: stamp startedAt so the response interceptor can
202
+ // compute durationMs. Only when a benchmark sink is attached — when it
203
+ // isn't, this is a single property assignment and does not change shape.
204
+ rawAxios.interceptors.request.use(function (cfg) {
205
+ if (_benchmarkSink) {
206
+ cfg._benchStartedAt = Date.now();
207
+ }
208
+ return cfg;
209
+ });
210
+ rawAxios.interceptors.response.use(function (response) {
211
+ if (!_benchmarkSink)
212
+ return response;
213
+ var startedAt = response.config && response.config._benchStartedAt;
214
+ if (!startedAt)
215
+ return response;
216
+ var url = (response.config && response.config.url) || "";
217
+ var payload = response.data;
218
+ var responseBytes = 0;
219
+ try {
220
+ responseBytes = payload ? JSON.stringify(payload).length : 0;
221
+ }
222
+ catch (e) {
223
+ responseBytes = 0;
224
+ }
225
+ var tableCount = 0;
226
+ var result = payload && payload.result;
227
+ if (result && typeof result === "object") {
228
+ if (url.indexOf("bulkDownload") !== -1) {
229
+ tableCount = Object.keys(result).length;
230
+ }
231
+ else if (url.indexOf("getManifest") !== -1 && result.tables) {
232
+ tableCount = Object.keys(result.tables).length;
233
+ }
234
+ }
235
+ _benchmarkSink.recordHttp({
236
+ path: url,
237
+ tableCount: tableCount,
238
+ durationMs: Date.now() - startedAt,
239
+ statusCode: response.status,
240
+ responseBytes: responseBytes,
241
+ });
242
+ return response;
243
+ }, function (error) {
244
+ // Preserve original rejection; don't record errored requests (they skew
245
+ // latency tails without adding signal for the "scales the same" question).
246
+ return Promise.reject(error);
247
+ });
248
+ const client = (0, axios_rate_limit_1.default)(rawAxios, { maxRPS: 20 });
249
+ const getAppList = () => {
250
+ const endpoint = "api/sinc/sincronia/getAppList";
251
+ return client.get(endpoint);
252
+ };
253
+ const updateATFfile = (contents, sysId) => {
254
+ const endpoint = "api/sinc/sincronia/pushATFfile";
255
+ try {
256
+ return client.post(endpoint, { file: contents, sys_id: sysId });
257
+ }
258
+ catch (e) {
259
+ throw e;
260
+ }
261
+ };
262
+ const updateRecord = (table, recordId, fields) => {
263
+ if (table === "sys_atf_step") {
264
+ updateATFfile(fields["inputs.script"], recordId);
265
+ }
266
+ const endpoint = `api/now/table/${table}/${recordId}`;
267
+ return client.patch(endpoint, fields);
268
+ };
269
+ const getScopeId = (scopeName) => {
270
+ const endpoint = "api/now/table/sys_scope";
271
+ return client.get(endpoint, {
272
+ params: {
273
+ sysparm_query: `scope=${scopeName}`,
274
+ sysparm_fields: "sys_id",
275
+ },
276
+ });
277
+ };
278
+ const getUserSysId = (userName = process.env.SN_USER) => {
279
+ const endpoint = "api/now/table/sys_user";
280
+ return client.get(endpoint, {
281
+ params: {
282
+ sysparm_query: `user_name=${userName}`,
283
+ sysparm_fields: "sys_id",
284
+ },
285
+ });
286
+ };
287
+ const getCurrentAppUserPrefSysId = (userSysId) => {
288
+ const endpoint = `api/now/table/sys_user_preference`;
289
+ return client.get(endpoint, {
290
+ params: {
291
+ sysparm_query: `user=${userSysId}^name=apps.current_app`,
292
+ sysparm_fields: "sys_id",
293
+ },
294
+ });
295
+ };
296
+ const updateCurrentAppUserPref = (appSysId, userPrefSysId) => {
297
+ const endpoint = `api/now/table/sys_user_preference/${userPrefSysId}`;
298
+ return client.put(endpoint, { value: appSysId });
299
+ };
300
+ const createCurrentAppUserPref = (appSysId, userSysId) => {
301
+ const endpoint = `api/now/table/sys_user_preference`;
302
+ return client.post(endpoint, {
303
+ value: appSysId,
304
+ name: "apps.current_app",
305
+ type: "string",
306
+ user: userSysId,
307
+ });
308
+ };
309
+ const getCurrentScope = () => {
310
+ const endpoint = "api/sinc/sincronia/getCurrentScope";
311
+ return client.get(endpoint);
312
+ };
313
+ const createUpdateSet = (updateSetName, scopeSysId, description) => {
314
+ const endpoint = `api/now/table/sys_update_set`;
315
+ const data = {
316
+ name: updateSetName,
317
+ state: "in progress",
318
+ };
319
+ if (scopeSysId) {
320
+ data.application = scopeSysId;
321
+ }
322
+ if (description) {
323
+ data.description = description;
324
+ }
325
+ return client.post(endpoint, data);
326
+ };
327
+ const getCurrentUpdateSetUserPref = (userSysId) => {
328
+ const endpoint = `api/now/table/sys_user_preference`;
329
+ return client.get(endpoint, {
330
+ params: {
331
+ sysparm_query: `user=${userSysId}^name=sys_update_set`,
332
+ sysparm_fields: "sys_id",
333
+ },
334
+ });
335
+ };
336
+ const updateCurrentUpdateSetUserPref = (updateSetSysId, userPrefSysId) => {
337
+ const endpoint = `api/now/table/sys_user_preference/${userPrefSysId}`;
338
+ return client.put(endpoint, { value: updateSetSysId });
339
+ };
340
+ const createCurrentUpdateSetUserPref = (updateSetSysId, userSysId) => {
341
+ const endpoint = `api/now/table/sys_user_preference`;
342
+ return client.put(endpoint, {
343
+ value: updateSetSysId,
344
+ name: "sys_update_set",
345
+ type: "string",
346
+ user: userSysId,
347
+ });
348
+ };
349
+ const getMissingFiles = (missingFiles, tableOptions) => {
350
+ const endpoint = `api/sinc/sincronia/bulkDownload`;
351
+ const tableCount = Object.keys(missingFiles).length;
352
+ FileLogger_1.fileLogger.debug("Bulk downloading files for " + tableCount + " tables");
353
+ return client.post(endpoint, { missingFiles, tableOptions });
354
+ };
355
+ const getManifest = (scope, config, withFiles = false) => {
356
+ const endpoint = `api/sinc/sincronia/getManifest/${scope}`;
357
+ const { includes = {}, excludes = {}, tableOptions = {}, scopes = {}, } = config;
358
+ // Strip _ directive keys before sending to ServiceNow API
359
+ var cleanIncludes = _stripUnderscoreKeys(includes);
360
+ var cleanExcludes = _stripUnderscoreKeys(excludes);
361
+ FileLogger_1.fileLogger.debug("Fetching manifest for scope " + scope + (withFiles ? " (with file contents)" : " (structure only)"));
362
+ return client.post(endpoint, {
363
+ includes: cleanIncludes,
364
+ excludes: cleanExcludes,
365
+ tableOptions,
366
+ withFiles,
367
+ getContents: withFiles, // ServiceNow expects getContents, not withFiles
368
+ });
369
+ };
370
+ const changeUpdateSet = (params) => {
371
+ return _callDovetailApi("changeUpdateSet", (endpoint) => client.get(endpoint, { params }));
372
+ };
373
+ const getCurrentUpdateSet = (scope) => {
374
+ const params = {};
375
+ if (scope) {
376
+ params.scope = scope;
377
+ }
378
+ return _callDovetailApi("currentUpdateSet", (endpoint) => client.get(endpoint, { params }));
379
+ };
380
+ const changeScope = (scope) => {
381
+ return _callDovetailApi("changeScope", (endpoint) => client.get(endpoint, { params: { scope } }));
382
+ };
383
+ const pushWithUpdateSet = (updateSetSysId, table, recordSysId, fields) => {
384
+ return _callDovetailApi("pushWithUpdateSet", (endpoint) => client.post(endpoint, {
385
+ update_set_sys_id: updateSetSysId,
386
+ table,
387
+ record_sys_id: recordSysId,
388
+ fields,
389
+ }));
390
+ };
391
+ const createRecord = (params) => {
392
+ return _callDovetailApi("createRecord", (endpoint) => client.post(endpoint, params));
393
+ };
394
+ const deleteRecord = (params) => {
395
+ return _callDovetailApi("deleteRecord", (endpoint) => client.post(endpoint, params));
396
+ };
397
+ return {
398
+ getAppList,
399
+ updateRecord,
400
+ getScopeId,
401
+ getUserSysId,
402
+ getCurrentAppUserPrefSysId,
403
+ updateCurrentAppUserPref,
404
+ createCurrentAppUserPref,
405
+ getCurrentScope,
406
+ createUpdateSet,
407
+ getCurrentUpdateSetUserPref,
408
+ updateCurrentUpdateSetUserPref,
409
+ createCurrentUpdateSetUserPref,
410
+ getMissingFiles,
411
+ getManifest,
412
+ changeUpdateSet,
413
+ getCurrentUpdateSet,
414
+ changeScope,
415
+ pushWithUpdateSet,
416
+ createRecord,
417
+ deleteRecord,
418
+ client, // Expose the axios client for custom queries
419
+ };
420
+ };
421
+ exports.snClient = snClient;
422
+ let internalClient = undefined;
423
+ const defaultClient = () => {
424
+ if (internalClient) {
425
+ return internalClient;
426
+ }
427
+ const { SN_USER = "", SN_PASSWORD = "", SN_INSTANCE = "" } = process.env;
428
+ internalClient = (0, exports.snClient)(`https://${SN_INSTANCE}/`, SN_USER, SN_PASSWORD);
429
+ return internalClient;
430
+ };
431
+ exports.defaultClient = defaultClient;
432
+ const unwrapSNResponse = async (clientPromise) => {
433
+ try {
434
+ const resp = await clientPromise;
435
+ // Log response summary for key endpoints
436
+ if (resp.config && resp.config.url) {
437
+ const url = resp.config.url;
438
+ const result = resp.data.result;
439
+ if (url.includes("getManifest") && result && result.tables) {
440
+ const tableCount = Object.keys(result.tables).length;
441
+ FileLogger_1.fileLogger.debug("Manifest received: " + tableCount + " tables (status " + resp.status + ")");
442
+ }
443
+ else if (url.includes("bulkDownload") && result) {
444
+ const tableCount = Object.keys(result).length;
445
+ FileLogger_1.fileLogger.debug("Bulk download received: " + tableCount + " tables (status " + resp.status + ")");
446
+ }
447
+ }
448
+ return resp.data.result;
449
+ }
450
+ catch (e) {
451
+ const instance = process.env.SN_INSTANCE || "unknown";
452
+ if (axios_1.default.isAxiosError(e) && e.response) {
453
+ const status = e.response.status;
454
+ const statusText = e.response.statusText || "";
455
+ const method = ((e.config && e.config.method) || "").toUpperCase();
456
+ const url = (e.config && e.config.url) || "";
457
+ const data = e.response.data;
458
+ // Extract ServiceNow-shaped error message if present (`{error: {message, detail}, status}`)
459
+ let snMessage = "";
460
+ if (data && typeof data === "object") {
461
+ if (data.error && typeof data.error === "object" && data.error.message) {
462
+ snMessage = " — " + String(data.error.message);
463
+ }
464
+ else if (typeof data.message === "string") {
465
+ snMessage = " — " + data.message;
466
+ }
467
+ }
468
+ // Pull scope out of /getManifest/:scope URLs for easy grepping
469
+ let scope;
470
+ const manifestMatch = url.match(/\/getManifest\/([^/?]+)/);
471
+ if (manifestMatch)
472
+ scope = manifestMatch[1];
473
+ Logger_1.logger.error("Error from " + instance + ": HTTP " + status + " " +
474
+ method + " " + url + snMessage);
475
+ FileLogger_1.fileLogger.debug("REST error detail", {
476
+ instance: instance,
477
+ scope: scope,
478
+ method: method,
479
+ url: url,
480
+ status: status,
481
+ statusText: statusText,
482
+ responseData: data,
483
+ responseHeaders: e.response.headers,
484
+ });
485
+ }
486
+ else {
487
+ // Non-Axios error: preserve today's behaviour, add debug detail
488
+ let message;
489
+ if (e instanceof Error)
490
+ message = e.message;
491
+ else
492
+ message = String(e);
493
+ Logger_1.logger.error("Error from " + instance + ": " + message);
494
+ FileLogger_1.fileLogger.debug("Non-Axios error detail", {
495
+ instance: instance,
496
+ errorName: e instanceof Error ? e.name : undefined,
497
+ errorStack: e instanceof Error ? e.stack : undefined,
498
+ message: message,
499
+ });
500
+ }
501
+ throw e;
502
+ }
503
+ };
504
+ exports.unwrapSNResponse = unwrapSNResponse;
505
+ async function unwrapTableAPIFirstItem(clientPromise, extractField) {
506
+ try {
507
+ const resp = await (0, exports.unwrapSNResponse)(clientPromise);
508
+ if (resp.length === 0) {
509
+ throw new Error("Response was not a populated array!");
510
+ }
511
+ if (!extractField) {
512
+ return resp[0];
513
+ }
514
+ return resp[0][extractField];
515
+ }
516
+ catch (e) {
517
+ throw e;
518
+ }
519
+ }