@tenonhq/dovetail-core 0.0.96 → 0.0.97
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/appUtils.js +14 -3
- package/dist/tests/refreshSafeFolderNames.test.js +216 -0
- package/package.json +1 -1
- package/release-manifest.json +103 -1
package/dist/appUtils.js
CHANGED
|
@@ -118,7 +118,13 @@ const processFilesInManRec = async (recPath, rec, forceWrite) => {
|
|
|
118
118
|
const processRecsInManTable = async (tablePath, table, forceWrite, onRecordProcessed) => {
|
|
119
119
|
const { records } = table;
|
|
120
120
|
const recKeys = Object.keys(records);
|
|
121
|
-
|
|
121
|
+
// Derive the folder name via toSafeFolderName, not the raw record.name. The
|
|
122
|
+
// download path normalizes manifest keys upstream (processManifest), but the
|
|
123
|
+
// refresh path (processMissingFiles) feeds this writer the raw bulkDownload
|
|
124
|
+
// response, whose .name is the unmodified ServiceNow display name — which can
|
|
125
|
+
// be filesystem-unsafe (e.g. a `<table>.*` ACL). toSafeFolderName is
|
|
126
|
+
// idempotent on already-safe names, so re-applying it here is harmless.
|
|
127
|
+
const recKeyToPath = (key) => path_1.default.join(tablePath, (0, exports.toSafeFolderName)(records[key]));
|
|
122
128
|
const recPathPromises = recKeys
|
|
123
129
|
.map(recKeyToPath)
|
|
124
130
|
.map(fUtils.createDirRecursively);
|
|
@@ -511,11 +517,16 @@ const refreshAllFiles = async (newManifest, sourcePath, options = {}) => {
|
|
|
511
517
|
var recs = filesToProcess[tableName].records;
|
|
512
518
|
var recKeys = Object.keys(recs);
|
|
513
519
|
await Promise.all(recKeys.map(function (k) {
|
|
514
|
-
|
|
520
|
+
// toSafeFolderName, not raw recs[k].name: the bulkDownload response
|
|
521
|
+
// carries the unmodified ServiceNow display name, which may be
|
|
522
|
+
// filesystem-unsafe (e.g. a `<table>.*` ACL). Without this, refresh
|
|
523
|
+
// recreates Windows-illegal folders that normalizeManifestKeys
|
|
524
|
+
// already removed from the manifest.
|
|
525
|
+
return fUtils.createDirRecursively(path_1.default.join(tablePath, (0, exports.toSafeFolderName)(recs[k])));
|
|
515
526
|
}));
|
|
516
527
|
await (0, genericUtils_1.processBatched)(recKeys, constants_1.CONCURRENCY_RECORDS, async function (recKey) {
|
|
517
528
|
var rec = recs[recKey];
|
|
518
|
-
var recPath = path_1.default.join(tablePath,
|
|
529
|
+
var recPath = path_1.default.join(tablePath, (0, exports.toSafeFolderName)(rec));
|
|
519
530
|
// Split server-provided metadata off from the regular files so we can
|
|
520
531
|
// track whether any regular file actually changed — metaData shouldn't
|
|
521
532
|
// be the trigger for "this record changed" since we stamp it on every
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Regression: the refresh path must write filesystem-SAFE folder names.
|
|
4
|
+
*
|
|
5
|
+
* normalizeManifestKeys() (PR #117) re-keys the manifest object so a record
|
|
6
|
+
* with a Windows-illegal display name (e.g. a table-wildcard ACL named
|
|
7
|
+
* `x_cadso_work_campaign.*`) maps to its sys_id. But the refresh writer
|
|
8
|
+
* (`refreshAllFiles`) builds folders from the raw `getMissingFiles`
|
|
9
|
+
* (bulkDownload) server response, whose `.name` is the unmodified display
|
|
10
|
+
* name — so it recreated the illegal `<table>.*` folders on every refresh,
|
|
11
|
+
* silently breaking the folder ≡ manifest-key invariant and aborting
|
|
12
|
+
* `git clone` on Windows.
|
|
13
|
+
*
|
|
14
|
+
* These tests assert the writer routes through toSafeFolderName: a `.*` ACL
|
|
15
|
+
* record lands in a sys_id folder, while a safe name stays human-readable.
|
|
16
|
+
*/
|
|
17
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
18
|
+
if (k2 === undefined) k2 = k;
|
|
19
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
20
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
21
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
22
|
+
}
|
|
23
|
+
Object.defineProperty(o, k2, desc);
|
|
24
|
+
}) : (function(o, m, k, k2) {
|
|
25
|
+
if (k2 === undefined) k2 = k;
|
|
26
|
+
o[k2] = m[k];
|
|
27
|
+
}));
|
|
28
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
29
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
30
|
+
}) : function(o, v) {
|
|
31
|
+
o["default"] = v;
|
|
32
|
+
});
|
|
33
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
34
|
+
var ownKeys = function(o) {
|
|
35
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
36
|
+
var ar = [];
|
|
37
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
38
|
+
return ar;
|
|
39
|
+
};
|
|
40
|
+
return ownKeys(o);
|
|
41
|
+
};
|
|
42
|
+
return function (mod) {
|
|
43
|
+
if (mod && mod.__esModule) return mod;
|
|
44
|
+
var result = {};
|
|
45
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
46
|
+
__setModuleDefault(result, mod);
|
|
47
|
+
return result;
|
|
48
|
+
};
|
|
49
|
+
})();
|
|
50
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
51
|
+
const fs = __importStar(require("fs"));
|
|
52
|
+
const os = __importStar(require("os"));
|
|
53
|
+
const path = __importStar(require("path"));
|
|
54
|
+
// ---------- mocks (must come before importing the module under test) ----------
|
|
55
|
+
var mockLogger = {
|
|
56
|
+
info: jest.fn(),
|
|
57
|
+
debug: jest.fn(),
|
|
58
|
+
warn: jest.fn(),
|
|
59
|
+
error: jest.fn(),
|
|
60
|
+
success: jest.fn(),
|
|
61
|
+
getLogLevel: function () { return "warn"; },
|
|
62
|
+
};
|
|
63
|
+
var mockFileLogger = {
|
|
64
|
+
debug: jest.fn(),
|
|
65
|
+
info: jest.fn(),
|
|
66
|
+
warn: jest.fn(),
|
|
67
|
+
error: jest.fn(),
|
|
68
|
+
};
|
|
69
|
+
var mockClient = {
|
|
70
|
+
getManifest: jest.fn(),
|
|
71
|
+
getMissingFiles: jest.fn(),
|
|
72
|
+
};
|
|
73
|
+
jest.mock("../Logger", function () { return { logger: mockLogger }; });
|
|
74
|
+
jest.mock("../FileLogger", function () { return { fileLogger: mockFileLogger }; });
|
|
75
|
+
jest.mock("../snClient", function () {
|
|
76
|
+
return {
|
|
77
|
+
defaultClient: function () { return mockClient; },
|
|
78
|
+
unwrapSNResponse: function (p) { return Promise.resolve(p).then(function (r) { return r; }); },
|
|
79
|
+
processPushResponse: jest.fn(),
|
|
80
|
+
retryOnErr: jest.fn(),
|
|
81
|
+
retryOnHttpErr: jest.fn(),
|
|
82
|
+
unwrapTableAPIFirstItem: jest.fn(),
|
|
83
|
+
};
|
|
84
|
+
});
|
|
85
|
+
var mockConfig = {
|
|
86
|
+
getConfig: jest.fn().mockReturnValue({
|
|
87
|
+
scopes: { x_cadso_work: {} },
|
|
88
|
+
tableOptions: {},
|
|
89
|
+
}),
|
|
90
|
+
getManifest: jest.fn().mockResolvedValue({
|
|
91
|
+
x_cadso_work: { scope: "x_cadso_work", tables: {} },
|
|
92
|
+
}),
|
|
93
|
+
getSourcePathForScope: jest.fn(),
|
|
94
|
+
getSourcePath: jest.fn(),
|
|
95
|
+
getManifestPath: jest.fn().mockReturnValue("/tmp/dove.manifest.json"),
|
|
96
|
+
resolveConfigForScope: jest.fn().mockImplementation(function () {
|
|
97
|
+
return {
|
|
98
|
+
tables: ["sys_security_acl"],
|
|
99
|
+
fieldOverrides: {},
|
|
100
|
+
apiIncludes: {},
|
|
101
|
+
apiExcludes: {},
|
|
102
|
+
};
|
|
103
|
+
}),
|
|
104
|
+
isMultiScopeManifest: jest.fn().mockReturnValue(true),
|
|
105
|
+
updateManifest: jest.fn(),
|
|
106
|
+
};
|
|
107
|
+
jest.mock("../config", function () { return mockConfig; });
|
|
108
|
+
jest.mock("progress", function () {
|
|
109
|
+
return jest.fn().mockImplementation(function () {
|
|
110
|
+
return { tick: jest.fn() };
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
// Keep real FileUtils so we assert against the actual file system; stub only
|
|
114
|
+
// writeScopeManifest so no manifest file is written.
|
|
115
|
+
jest.mock("../FileUtils", function () {
|
|
116
|
+
var actual = jest.requireActual("../FileUtils");
|
|
117
|
+
return Object.assign({}, actual, {
|
|
118
|
+
writeScopeManifest: jest.fn().mockResolvedValue(undefined),
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
const AppUtils = __importStar(require("../appUtils"));
|
|
122
|
+
// ---------- helpers ----------
|
|
123
|
+
var tmpRoot;
|
|
124
|
+
var ACL_SYS_ID = "bc91c2a5f334621048dff31590812771";
|
|
125
|
+
function listAclFolders() {
|
|
126
|
+
var dir = path.join(tmpRoot, "sys_security_acl");
|
|
127
|
+
try {
|
|
128
|
+
return fs.readdirSync(dir).sort();
|
|
129
|
+
}
|
|
130
|
+
catch (e) {
|
|
131
|
+
return [];
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
describe("refresh — folder names are filesystem-safe (sys_id fallback)", function () {
|
|
135
|
+
beforeEach(function () {
|
|
136
|
+
jest.clearAllMocks();
|
|
137
|
+
tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "dove-safe-folder-"));
|
|
138
|
+
mockConfig.getSourcePathForScope.mockReturnValue(tmpRoot);
|
|
139
|
+
mockConfig.getSourcePath.mockReturnValue(tmpRoot);
|
|
140
|
+
});
|
|
141
|
+
afterEach(function () {
|
|
142
|
+
try {
|
|
143
|
+
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
|
144
|
+
}
|
|
145
|
+
catch (e) { }
|
|
146
|
+
});
|
|
147
|
+
test("a `<table>.*` ACL record is written to its sys_id folder, never `.*`", async function () {
|
|
148
|
+
// Manifest as the server returns it: keyed by the illegal display name.
|
|
149
|
+
// normalizeManifestKeys re-keys this to sys_id inside syncManifest.
|
|
150
|
+
mockClient.getManifest.mockResolvedValue({
|
|
151
|
+
scope: "x_cadso_work",
|
|
152
|
+
tables: {
|
|
153
|
+
sys_security_acl: {
|
|
154
|
+
records: {
|
|
155
|
+
"x_cadso_work_campaign.*": {
|
|
156
|
+
name: "x_cadso_work_campaign.*",
|
|
157
|
+
sys_id: ACL_SYS_ID,
|
|
158
|
+
files: [{ name: "script", type: "js" }],
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
// bulkDownload echoes the RAW display name back — the bug source. The
|
|
165
|
+
// writer must not trust it for the folder path.
|
|
166
|
+
mockClient.getMissingFiles.mockResolvedValue({
|
|
167
|
+
sys_security_acl: {
|
|
168
|
+
records: {
|
|
169
|
+
"x_cadso_work_campaign.*": {
|
|
170
|
+
name: "x_cadso_work_campaign.*",
|
|
171
|
+
sys_id: ACL_SYS_ID,
|
|
172
|
+
files: [{ name: "script", type: "js", content: "// acl" }],
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
await AppUtils.syncManifest("x_cadso_work");
|
|
178
|
+
var folders = listAclFolders();
|
|
179
|
+
expect(folders).toContain(ACL_SYS_ID);
|
|
180
|
+
expect(folders).not.toContain("x_cadso_work_campaign.*");
|
|
181
|
+
// The file landed inside the sys_id folder.
|
|
182
|
+
expect(fs.readFileSync(path.join(tmpRoot, "sys_security_acl", ACL_SYS_ID, "script.js"), "utf8")).toBe("// acl");
|
|
183
|
+
});
|
|
184
|
+
test("a filesystem-safe ACL name stays human-readable (no needless sys_id fallback)", async function () {
|
|
185
|
+
var SAFE_SYS_ID = "aa11bb22cc33dd44ee55ff6600112233";
|
|
186
|
+
mockClient.getManifest.mockResolvedValue({
|
|
187
|
+
scope: "x_cadso_work",
|
|
188
|
+
tables: {
|
|
189
|
+
sys_security_acl: {
|
|
190
|
+
records: {
|
|
191
|
+
x_cadso_work_campaign: {
|
|
192
|
+
name: "x_cadso_work_campaign",
|
|
193
|
+
sys_id: SAFE_SYS_ID,
|
|
194
|
+
files: [{ name: "script", type: "js" }],
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
mockClient.getMissingFiles.mockResolvedValue({
|
|
201
|
+
sys_security_acl: {
|
|
202
|
+
records: {
|
|
203
|
+
x_cadso_work_campaign: {
|
|
204
|
+
name: "x_cadso_work_campaign",
|
|
205
|
+
sys_id: SAFE_SYS_ID,
|
|
206
|
+
files: [{ name: "script", type: "js", content: "// safe" }],
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
},
|
|
210
|
+
});
|
|
211
|
+
await AppUtils.syncManifest("x_cadso_work");
|
|
212
|
+
var folders = listAclFolders();
|
|
213
|
+
expect(folders).toContain("x_cadso_work_campaign");
|
|
214
|
+
expect(folders).not.toContain(SAFE_SYS_ID);
|
|
215
|
+
});
|
|
216
|
+
});
|
package/package.json
CHANGED
package/release-manifest.json
CHANGED
|
@@ -1,7 +1,109 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "dovetail-manifest/v1",
|
|
3
|
-
"generated_at": "2026-
|
|
3
|
+
"generated_at": "2026-06-01T21:28:08.741Z",
|
|
4
4
|
"events": [
|
|
5
|
+
{
|
|
6
|
+
"$schema": "dovetail-release-event/v1",
|
|
7
|
+
"event_id": "dovetail-core@0.0.96",
|
|
8
|
+
"package": "@tenonhq/dovetail-core",
|
|
9
|
+
"version": "0.0.96",
|
|
10
|
+
"prev_version": "0.0.95",
|
|
11
|
+
"published_at": "2026-06-01T21:28:08.724Z",
|
|
12
|
+
"git": {
|
|
13
|
+
"sha": "a3ea5ead48b0bd4c324e0847e7e768a43520c7ec",
|
|
14
|
+
"tag": "dovetail-core@0.0.96"
|
|
15
|
+
},
|
|
16
|
+
"semver_bump": "patch",
|
|
17
|
+
"commits": [
|
|
18
|
+
{
|
|
19
|
+
"sha": "a3ea5ead4",
|
|
20
|
+
"subject": "feat(client): point Dovetail clients at the scoped dovetail_* APIs (#132)",
|
|
21
|
+
"pr": 132
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"sha": "93d180eef",
|
|
25
|
+
"subject": "chore(release): @tenonhq/dovetail-servicenow@0.0.9 [skip ci]"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"sha": "d32523991",
|
|
29
|
+
"subject": "chore(release): @tenonhq/dovetail-servicenow@0.0.8 [skip ci]"
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"sha": "ba1c67775",
|
|
33
|
+
"subject": "chore(release): @tenonhq/dovetail-dashboard@0.0.36 [skip ci]"
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"sha": "9901285c6",
|
|
37
|
+
"subject": "chore(release): @tenonhq/dovetail-dashboard@0.0.35 [skip ci]"
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"sha": "55de31527",
|
|
41
|
+
"subject": "chore(release): @tenonhq/dovetail-dashboard@0.0.34 [skip ci]"
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"sha": "776f1fc53",
|
|
45
|
+
"subject": "chore(release): @tenonhq/dovetail-sawmill@0.0.7 [skip ci]"
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"sha": "407d6d75e",
|
|
49
|
+
"subject": "chore(release): publish 2 packages [skip ci]"
|
|
50
|
+
}
|
|
51
|
+
],
|
|
52
|
+
"exports_delta": null,
|
|
53
|
+
"narrative": null
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"$schema": "dovetail-release-event/v1",
|
|
57
|
+
"event_id": "dovetail-servicenow@0.0.10",
|
|
58
|
+
"package": "@tenonhq/dovetail-servicenow",
|
|
59
|
+
"version": "0.0.10",
|
|
60
|
+
"prev_version": "0.0.9",
|
|
61
|
+
"published_at": "2026-06-01T21:28:08.724Z",
|
|
62
|
+
"git": {
|
|
63
|
+
"sha": "a3ea5ead48b0bd4c324e0847e7e768a43520c7ec",
|
|
64
|
+
"tag": "dovetail-servicenow@0.0.10"
|
|
65
|
+
},
|
|
66
|
+
"semver_bump": "patch",
|
|
67
|
+
"commits": [
|
|
68
|
+
{
|
|
69
|
+
"sha": "a3ea5ead4",
|
|
70
|
+
"subject": "feat(client): point Dovetail clients at the scoped dovetail_* APIs (#132)",
|
|
71
|
+
"pr": 132
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
"sha": "93d180eef",
|
|
75
|
+
"subject": "chore(release): @tenonhq/dovetail-servicenow@0.0.9 [skip ci]"
|
|
76
|
+
}
|
|
77
|
+
],
|
|
78
|
+
"exports_delta": null,
|
|
79
|
+
"narrative": null
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
"$schema": "dovetail-release-event/v1",
|
|
83
|
+
"event_id": "dovetail-sawmill@0.0.8",
|
|
84
|
+
"package": "@tenonhq/dovetail-sawmill",
|
|
85
|
+
"version": "0.0.8",
|
|
86
|
+
"prev_version": "0.0.7",
|
|
87
|
+
"published_at": "2026-06-01T21:28:08.724Z",
|
|
88
|
+
"git": {
|
|
89
|
+
"sha": "a3ea5ead48b0bd4c324e0847e7e768a43520c7ec",
|
|
90
|
+
"tag": "dovetail-sawmill@0.0.8"
|
|
91
|
+
},
|
|
92
|
+
"semver_bump": "patch",
|
|
93
|
+
"commits": [
|
|
94
|
+
{
|
|
95
|
+
"sha": "a3ea5ead4",
|
|
96
|
+
"subject": "feat(client): point Dovetail clients at the scoped dovetail_* APIs (#132)",
|
|
97
|
+
"pr": 132
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
"sha": "776f1fc53",
|
|
101
|
+
"subject": "chore(release): @tenonhq/dovetail-sawmill@0.0.7 [skip ci]"
|
|
102
|
+
}
|
|
103
|
+
],
|
|
104
|
+
"exports_delta": null,
|
|
105
|
+
"narrative": null
|
|
106
|
+
},
|
|
5
107
|
{
|
|
6
108
|
"$schema": "dovetail-release-event/v1",
|
|
7
109
|
"event_id": "dovetail-servicenow@0.0.9",
|