@tenonhq/dovetail-core 0.0.100 → 0.0.102
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/snClient.js +55 -3
- package/dist/tests/snClient.createUpdateSet.test.js +116 -0
- package/package.json +2 -2
- package/release-manifest.json +53 -1
package/dist/snClient.js
CHANGED
|
@@ -111,6 +111,22 @@ function _enrichDovetailApiError(e, attemptedEndpoint) {
|
|
|
111
111
|
hint;
|
|
112
112
|
return e;
|
|
113
113
|
}
|
|
114
|
+
// True when an error indicates the Dovetail Scripted REST endpoint does not
|
|
115
|
+
// exist on this instance (404, or the 400 "Requested URI does not represent any
|
|
116
|
+
// resource" body). Used to degrade createUpdateSet to the Table API path on
|
|
117
|
+
// instances that do not have the createUpdateSet op deployed yet.
|
|
118
|
+
function _isMissingDovetailEndpoint(e) {
|
|
119
|
+
if (!axios_1.default.isAxiosError(e))
|
|
120
|
+
return false;
|
|
121
|
+
var status = e.response ? e.response.status : undefined;
|
|
122
|
+
if (status === 404)
|
|
123
|
+
return true;
|
|
124
|
+
if (status === 400) {
|
|
125
|
+
var body = e.response ? _summarizeResponseBody(e.response.data) : "";
|
|
126
|
+
return body.indexOf(_SN_MISSING_ENDPOINT_BODY) !== -1;
|
|
127
|
+
}
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
114
130
|
async function _callDovetailApi(op, call) {
|
|
115
131
|
var endpoint = _dovetailEndpoint(op);
|
|
116
132
|
try {
|
|
@@ -353,8 +369,12 @@ const snClient = (baseURL, username, password) => {
|
|
|
353
369
|
const endpoint = "api/cadso/dovetail_sync/getCurrentScope";
|
|
354
370
|
return client.get(endpoint);
|
|
355
371
|
};
|
|
356
|
-
const createUpdateSet = (updateSetName, scopeSysId, description) => {
|
|
357
|
-
|
|
372
|
+
const createUpdateSet = async (updateSetName, scopeSysId, description) => {
|
|
373
|
+
// Prefer the scope-safe Dovetail server op. It switches the current
|
|
374
|
+
// application and inserts the set in one server call, so the set always
|
|
375
|
+
// lands in `scopeSysId`. A raw Table API POST ignores `application` (the
|
|
376
|
+
// field defaults to the session's current app), and the old
|
|
377
|
+
// changeScope-then-POST path raced and mis-scoped sets created back to back.
|
|
358
378
|
const data = {
|
|
359
379
|
name: updateSetName,
|
|
360
380
|
state: "in progress",
|
|
@@ -365,7 +385,39 @@ const snClient = (baseURL, username, password) => {
|
|
|
365
385
|
if (description) {
|
|
366
386
|
data.description = description;
|
|
367
387
|
}
|
|
368
|
-
|
|
388
|
+
try {
|
|
389
|
+
const resp = await _callDovetailApi("createUpdateSet", (endpoint) => client.post(endpoint, data));
|
|
390
|
+
const payload = resp.data || {};
|
|
391
|
+
const sysId = payload.sys_id || (payload.result && payload.result.sys_id);
|
|
392
|
+
if (!sysId) {
|
|
393
|
+
throw new Error("createUpdateSet endpoint returned no sys_id");
|
|
394
|
+
}
|
|
395
|
+
// Normalize to the Table-API shape that unwrapSNResponse expects
|
|
396
|
+
// (resp.data.result), so callers stay unchanged.
|
|
397
|
+
return {
|
|
398
|
+
...resp,
|
|
399
|
+
data: {
|
|
400
|
+
result: {
|
|
401
|
+
sys_id: sysId,
|
|
402
|
+
name: payload.name || updateSetName,
|
|
403
|
+
application: payload.application,
|
|
404
|
+
},
|
|
405
|
+
},
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
catch (e) {
|
|
409
|
+
if (!_isMissingDovetailEndpoint(e)) {
|
|
410
|
+
throw e;
|
|
411
|
+
}
|
|
412
|
+
// Instance without the createUpdateSet op yet: degrade to the legacy
|
|
413
|
+
// Table API path so existing flows keep working during rollout. Sets
|
|
414
|
+
// created this way may be mis-scoped until the op is deployed.
|
|
415
|
+
Logger_1.logger.warn("Dovetail createUpdateSet endpoint not found on this instance; falling " +
|
|
416
|
+
"back to the Table API. Update sets may be mis-scoped until the " +
|
|
417
|
+
"Dovetail createUpdateSet REST op is installed on this instance.");
|
|
418
|
+
const endpoint = `api/now/table/sys_update_set`;
|
|
419
|
+
return client.post(endpoint, data);
|
|
420
|
+
}
|
|
369
421
|
};
|
|
370
422
|
const getCurrentUpdateSetUserPref = (userSysId) => {
|
|
371
423
|
const endpoint = `api/now/table/sys_user_preference`;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Tests for snClient.createUpdateSet: it must prefer the scope-safe Dovetail
|
|
3
|
+
// `createUpdateSet` server op (normalizing its {sys_id,...} body to the
|
|
4
|
+
// {result:{...}} shape unwrapSNResponse expects), and fall back to the raw
|
|
5
|
+
// Table API ONLY when the endpoint is missing (404 / 400 "Requested URI ...").
|
|
6
|
+
// A 500 or an unrelated 400 must NOT trigger the fallback.
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
var mockLogger = {
|
|
9
|
+
info: jest.fn(),
|
|
10
|
+
debug: jest.fn(),
|
|
11
|
+
warn: jest.fn(),
|
|
12
|
+
error: jest.fn(),
|
|
13
|
+
getLogLevel: function () { return "debug"; },
|
|
14
|
+
};
|
|
15
|
+
jest.mock("../Logger", function () {
|
|
16
|
+
return { logger: mockLogger };
|
|
17
|
+
});
|
|
18
|
+
jest.mock("../FileLogger", function () {
|
|
19
|
+
return { fileLogger: { debug: jest.fn(), info: jest.fn(), warn: jest.fn(), error: jest.fn() } };
|
|
20
|
+
});
|
|
21
|
+
jest.mock("../genericUtils", function () {
|
|
22
|
+
return { wait: jest.fn().mockResolvedValue(undefined) };
|
|
23
|
+
});
|
|
24
|
+
const snClient_1 = require("../snClient");
|
|
25
|
+
function makeAxiosError(status, data) {
|
|
26
|
+
var error = new Error("Request failed with status " + status);
|
|
27
|
+
error.isAxiosError = true;
|
|
28
|
+
error.response = { status: status, headers: {}, data: data === undefined ? {} : data };
|
|
29
|
+
return error;
|
|
30
|
+
}
|
|
31
|
+
function makeAxiosResponse(data) {
|
|
32
|
+
return { status: 200, data: data, headers: {}, statusText: "OK", config: {} };
|
|
33
|
+
}
|
|
34
|
+
var MISSING_ENDPOINT_BODY = {
|
|
35
|
+
error: { message: "Requested URI does not represent any resource", detail: null },
|
|
36
|
+
status: "failure",
|
|
37
|
+
};
|
|
38
|
+
describe("snClient.createUpdateSet", function () {
|
|
39
|
+
beforeEach(function () {
|
|
40
|
+
(0, snClient_1._resetDovetailApiFallback)();
|
|
41
|
+
jest.clearAllMocks();
|
|
42
|
+
});
|
|
43
|
+
it("calls the createUpdateSet endpoint and normalizes the response to {result}", async function () {
|
|
44
|
+
var c = (0, snClient_1.snClient)("https://x.service-now.com/", "u", "p");
|
|
45
|
+
var post = jest.spyOn(c.client, "post").mockImplementation(async function () {
|
|
46
|
+
return makeAxiosResponse({ success: true, sys_id: "abc123", name: "My Set", application: "scopeSys" });
|
|
47
|
+
});
|
|
48
|
+
var resp = await c.createUpdateSet("My Set", "scopeSys", "a description");
|
|
49
|
+
expect(post).toHaveBeenCalledTimes(1);
|
|
50
|
+
expect(post.mock.calls[0][0]).toContain("dovetail_core/createUpdateSet");
|
|
51
|
+
expect(post.mock.calls[0][1]).toMatchObject({
|
|
52
|
+
name: "My Set",
|
|
53
|
+
application: "scopeSys",
|
|
54
|
+
description: "a description",
|
|
55
|
+
state: "in progress",
|
|
56
|
+
});
|
|
57
|
+
expect(resp.data.result.sys_id).toBe("abc123");
|
|
58
|
+
expect(resp.data.result.application).toBe("scopeSys");
|
|
59
|
+
});
|
|
60
|
+
it("falls back to the Table API when the endpoint 404s", async function () {
|
|
61
|
+
var c = (0, snClient_1.snClient)("https://x.service-now.com/", "u", "p");
|
|
62
|
+
var urls = [];
|
|
63
|
+
jest.spyOn(c.client, "post").mockImplementation(async function (url) {
|
|
64
|
+
urls.push(url);
|
|
65
|
+
if (url.indexOf("dovetail") !== -1) {
|
|
66
|
+
throw makeAxiosError(404, {});
|
|
67
|
+
}
|
|
68
|
+
return makeAxiosResponse({ result: { sys_id: "tbl1", name: "My Set" } });
|
|
69
|
+
});
|
|
70
|
+
var resp = await c.createUpdateSet("My Set", "scopeSys");
|
|
71
|
+
// dovetail_core -> legacy dovetail (404 fallback) -> Table API
|
|
72
|
+
expect(urls[urls.length - 1]).toBe("api/now/table/sys_update_set");
|
|
73
|
+
expect(resp.data.result.sys_id).toBe("tbl1");
|
|
74
|
+
expect(mockLogger.warn).toHaveBeenCalled();
|
|
75
|
+
});
|
|
76
|
+
it("falls back to the Table API on the 400 missing-endpoint body", async function () {
|
|
77
|
+
var c = (0, snClient_1.snClient)("https://x.service-now.com/", "u", "p");
|
|
78
|
+
var urls = [];
|
|
79
|
+
jest.spyOn(c.client, "post").mockImplementation(async function (url) {
|
|
80
|
+
urls.push(url);
|
|
81
|
+
if (url.indexOf("dovetail") !== -1) {
|
|
82
|
+
throw makeAxiosError(400, MISSING_ENDPOINT_BODY);
|
|
83
|
+
}
|
|
84
|
+
return makeAxiosResponse({ result: { sys_id: "tbl2", name: "My Set" } });
|
|
85
|
+
});
|
|
86
|
+
var resp = await c.createUpdateSet("My Set", "scopeSys");
|
|
87
|
+
expect(urls[urls.length - 1]).toBe("api/now/table/sys_update_set");
|
|
88
|
+
expect(resp.data.result.sys_id).toBe("tbl2");
|
|
89
|
+
});
|
|
90
|
+
it("does NOT fall back on an unrelated 400 (rethrows)", async function () {
|
|
91
|
+
var c = (0, snClient_1.snClient)("https://x.service-now.com/", "u", "p");
|
|
92
|
+
var tableHit = false;
|
|
93
|
+
jest.spyOn(c.client, "post").mockImplementation(async function (url) {
|
|
94
|
+
if (url.indexOf("dovetail") !== -1) {
|
|
95
|
+
throw makeAxiosError(400, { error: { message: "Invalid update set name" } });
|
|
96
|
+
}
|
|
97
|
+
tableHit = true;
|
|
98
|
+
return makeAxiosResponse({ result: { sys_id: "should-not-happen" } });
|
|
99
|
+
});
|
|
100
|
+
await expect(c.createUpdateSet("My Set", "scopeSys")).rejects.toBeDefined();
|
|
101
|
+
expect(tableHit).toBe(false);
|
|
102
|
+
});
|
|
103
|
+
it("does NOT fall back on a 500 (rethrows)", async function () {
|
|
104
|
+
var c = (0, snClient_1.snClient)("https://x.service-now.com/", "u", "p");
|
|
105
|
+
var tableHit = false;
|
|
106
|
+
jest.spyOn(c.client, "post").mockImplementation(async function (url) {
|
|
107
|
+
if (url.indexOf("dovetail") !== -1) {
|
|
108
|
+
throw makeAxiosError(500, { error: { message: "boom" } });
|
|
109
|
+
}
|
|
110
|
+
tableHit = true;
|
|
111
|
+
return makeAxiosResponse({ result: { sys_id: "should-not-happen" } });
|
|
112
|
+
});
|
|
113
|
+
await expect(c.createUpdateSet("My Set", "scopeSys")).rejects.toBeDefined();
|
|
114
|
+
expect(tableHit).toBe(false);
|
|
115
|
+
});
|
|
116
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tenonhq/dovetail-core",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.102",
|
|
4
4
|
"description": "Next-gen file syncer",
|
|
5
5
|
"license": "GPL-3.0",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
41
|
"@tenonhq/dovetail-clickup": "^0.0.10",
|
|
42
|
-
"@tenonhq/dovetail-schema": "^0.0.
|
|
42
|
+
"@tenonhq/dovetail-schema": "^0.0.10",
|
|
43
43
|
"axios": "^1.5.1",
|
|
44
44
|
"axios-cookiejar-support": "^4.0.7",
|
|
45
45
|
"axios-rate-limit": "^1.3.0",
|
package/release-manifest.json
CHANGED
|
@@ -1,7 +1,59 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "dovetail-manifest/v1",
|
|
3
|
-
"generated_at": "2026-06-
|
|
3
|
+
"generated_at": "2026-06-04T07:05:38.978Z",
|
|
4
4
|
"events": [
|
|
5
|
+
{
|
|
6
|
+
"$schema": "dovetail-release-event/v1",
|
|
7
|
+
"event_id": "dovetail-core@0.0.101",
|
|
8
|
+
"package": "@tenonhq/dovetail-core",
|
|
9
|
+
"version": "0.0.101",
|
|
10
|
+
"prev_version": "0.0.100",
|
|
11
|
+
"published_at": "2026-06-04T07:05:38.971Z",
|
|
12
|
+
"git": {
|
|
13
|
+
"sha": "7c78a10933956739a7a234e48362cd8dc0b7ad71",
|
|
14
|
+
"tag": "dovetail-core@0.0.101"
|
|
15
|
+
},
|
|
16
|
+
"semver_bump": "patch",
|
|
17
|
+
"commits": [
|
|
18
|
+
{
|
|
19
|
+
"sha": "7c78a1093",
|
|
20
|
+
"subject": "fix(core): bump dovetail-schema pin ^0.0.9 -> ^0.0.10 (#142)",
|
|
21
|
+
"pr": 142
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"sha": "6d7b59a14",
|
|
25
|
+
"subject": "chore(release): @tenonhq/dovetail-core@0.0.100 [skip ci]"
|
|
26
|
+
}
|
|
27
|
+
],
|
|
28
|
+
"exports_delta": null,
|
|
29
|
+
"narrative": null
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"$schema": "dovetail-release-event/v1",
|
|
33
|
+
"event_id": "dovetail-core@0.0.100",
|
|
34
|
+
"package": "@tenonhq/dovetail-core",
|
|
35
|
+
"version": "0.0.100",
|
|
36
|
+
"prev_version": "0.0.99",
|
|
37
|
+
"published_at": "2026-06-02T21:39:46.687Z",
|
|
38
|
+
"git": {
|
|
39
|
+
"sha": "602efb3b4ae7a1c450bdcfacdecea12ee618f085",
|
|
40
|
+
"tag": "dovetail-core@0.0.100"
|
|
41
|
+
},
|
|
42
|
+
"semver_bump": "patch",
|
|
43
|
+
"commits": [
|
|
44
|
+
{
|
|
45
|
+
"sha": "602efb3b4",
|
|
46
|
+
"subject": "fix(core): Windows-safe folder names in the download writer (#140)",
|
|
47
|
+
"pr": 140
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"sha": "d3fd80bf7",
|
|
51
|
+
"subject": "chore(release): @tenonhq/dovetail-core@0.0.99 [skip ci]"
|
|
52
|
+
}
|
|
53
|
+
],
|
|
54
|
+
"exports_delta": null,
|
|
55
|
+
"narrative": null
|
|
56
|
+
},
|
|
5
57
|
{
|
|
6
58
|
"$schema": "dovetail-release-event/v1",
|
|
7
59
|
"event_id": "dovetail-core@0.0.99",
|