@unito/integration-cli 0.64.5 → 0.65.0
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/boilerplate/package-lock.json +11 -24
- package/dist/src/commands/graph.d.ts +21 -0
- package/dist/src/commands/graph.js +144 -0
- package/dist/src/commands/schema-snapshot.d.ts +9 -0
- package/dist/src/commands/schema-snapshot.js +425 -0
- package/dist/src/hooks/init/displayLogo.js +3 -1
- package/dist/src/resources/coverageCalculator.d.ts +12 -0
- package/dist/src/resources/coverageCalculator.js +50 -0
- package/dist/src/resources/fieldExtractor.d.ts +7 -0
- package/dist/src/resources/fieldExtractor.js +45 -0
- package/dist/src/resources/jsonlReader.d.ts +11 -0
- package/dist/src/resources/jsonlReader.js +29 -0
- package/dist/test/commands/graph.test.d.ts +1 -0
- package/dist/test/commands/graph.test.js +188 -0
- package/dist/test/resources/coverageCalculator.test.d.ts +1 -0
- package/dist/test/resources/coverageCalculator.test.js +71 -0
- package/dist/test/resources/fieldExtractor.test.d.ts +1 -0
- package/dist/test/resources/fieldExtractor.test.js +60 -0
- package/dist/test/resources/jsonlReader.test.d.ts +1 -0
- package/dist/test/resources/jsonlReader.test.js +66 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/oclif.manifest.json +214 -1
- package/package.json +1 -1
|
@@ -4,7 +4,9 @@ const tslib_1 = require("tslib");
|
|
|
4
4
|
const core_1 = require("@oclif/core");
|
|
5
5
|
const gradient = tslib_1.__importStar(require("gradient-string"));
|
|
6
6
|
const updateNotifier_1 = require("./updateNotifier");
|
|
7
|
-
const displayLogo = async function () {
|
|
7
|
+
const displayLogo = async function ({ id }) {
|
|
8
|
+
if (id === 'graph')
|
|
9
|
+
return;
|
|
8
10
|
const gradients = [
|
|
9
11
|
gradient.atlas,
|
|
10
12
|
gradient.mind,
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface CoverageResult {
|
|
2
|
+
rawApiFields: string[];
|
|
3
|
+
mappedFields: string[];
|
|
4
|
+
unmappedFields: string[];
|
|
5
|
+
coveragePercent: number;
|
|
6
|
+
}
|
|
7
|
+
export interface AllowlistEntry {
|
|
8
|
+
field: string;
|
|
9
|
+
reason: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function computeCoverage(rawApiFields: string[], schemaFields: string[]): CoverageResult;
|
|
12
|
+
export declare function applyAllowlist(unmappedFields: string[], allowlist: AllowlistEntry[]): string[];
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.computeCoverage = computeCoverage;
|
|
4
|
+
exports.applyAllowlist = applyAllowlist;
|
|
5
|
+
function computeCoverage(rawApiFields, schemaFields) {
|
|
6
|
+
if (rawApiFields.length === 0) {
|
|
7
|
+
return { rawApiFields: [], mappedFields: [], unmappedFields: [], coveragePercent: 100 };
|
|
8
|
+
}
|
|
9
|
+
const schemaSet = new Set(schemaFields);
|
|
10
|
+
const mapped = [];
|
|
11
|
+
const unmapped = [];
|
|
12
|
+
for (const field of [...rawApiFields].sort()) {
|
|
13
|
+
if (schemaSet.has(field)) {
|
|
14
|
+
mapped.push(field);
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
unmapped.push(field);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
const coveragePercent = Math.round((mapped.length / rawApiFields.length) * 100);
|
|
21
|
+
return {
|
|
22
|
+
rawApiFields: [...rawApiFields].sort(),
|
|
23
|
+
mappedFields: mapped,
|
|
24
|
+
unmappedFields: unmapped,
|
|
25
|
+
coveragePercent,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function applyAllowlist(unmappedFields, allowlist) {
|
|
29
|
+
if (allowlist.length === 0)
|
|
30
|
+
return [...unmappedFields].sort();
|
|
31
|
+
return unmappedFields
|
|
32
|
+
.filter(field => {
|
|
33
|
+
return !allowlist.some(entry => matchPattern(field, entry.field));
|
|
34
|
+
})
|
|
35
|
+
.sort();
|
|
36
|
+
}
|
|
37
|
+
// Matches dot-notation field paths against allowlist patterns:
|
|
38
|
+
// "priority" → exact match
|
|
39
|
+
// "metadata.*" → matches one level (metadata.created_at, not metadata.a.b)
|
|
40
|
+
// "meta.**" → matches any depth (meta.a, meta.a.b, meta.a.b.c)
|
|
41
|
+
function matchPattern(field, pattern) {
|
|
42
|
+
if (pattern.endsWith('.**')) {
|
|
43
|
+
return field.startsWith(pattern.slice(0, -2));
|
|
44
|
+
}
|
|
45
|
+
if (pattern.endsWith('.*')) {
|
|
46
|
+
const prefix = pattern.slice(0, -1);
|
|
47
|
+
return field.startsWith(prefix) && !field.slice(prefix.length).includes('.');
|
|
48
|
+
}
|
|
49
|
+
return field === pattern;
|
|
50
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recursively extract field paths from a JSON body using dot notation.
|
|
3
|
+
* Arrays of objects: extract keys from first element.
|
|
4
|
+
* Top-level arrays: extract from first item.
|
|
5
|
+
* Returns sorted, unique field paths.
|
|
6
|
+
*/
|
|
7
|
+
export declare function extractFields(body: unknown): string[];
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.extractFields = extractFields;
|
|
4
|
+
/**
|
|
5
|
+
* Recursively extract field paths from a JSON body using dot notation.
|
|
6
|
+
* Arrays of objects: extract keys from first element.
|
|
7
|
+
* Top-level arrays: extract from first item.
|
|
8
|
+
* Returns sorted, unique field paths.
|
|
9
|
+
*/
|
|
10
|
+
function extractFields(body) {
|
|
11
|
+
if (body == null)
|
|
12
|
+
return [];
|
|
13
|
+
if (Array.isArray(body)) {
|
|
14
|
+
if (body.length === 0)
|
|
15
|
+
return [];
|
|
16
|
+
if (typeof body[0] === 'object' && body[0] !== null) {
|
|
17
|
+
return extractFields(body[0]);
|
|
18
|
+
}
|
|
19
|
+
return [];
|
|
20
|
+
}
|
|
21
|
+
if (typeof body !== 'object')
|
|
22
|
+
return [];
|
|
23
|
+
const fields = new Set();
|
|
24
|
+
collectFields(body, '', fields);
|
|
25
|
+
return [...fields].sort();
|
|
26
|
+
}
|
|
27
|
+
function collectFields(obj, prefix, fields) {
|
|
28
|
+
for (const key of Object.keys(obj)) {
|
|
29
|
+
const path = prefix ? `${prefix}.${key}` : key;
|
|
30
|
+
const value = obj[key];
|
|
31
|
+
if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
|
|
32
|
+
fields.add(path);
|
|
33
|
+
collectFields(value, path, fields);
|
|
34
|
+
}
|
|
35
|
+
else if (Array.isArray(value)) {
|
|
36
|
+
fields.add(path);
|
|
37
|
+
if (value.length > 0 && typeof value[0] === 'object' && value[0] !== null) {
|
|
38
|
+
collectFields(value[0], path, fields);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
fields.add(path);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface RawResponseEntry {
|
|
2
|
+
seq: number;
|
|
3
|
+
url: string;
|
|
4
|
+
method: string;
|
|
5
|
+
status: number;
|
|
6
|
+
body: unknown;
|
|
7
|
+
}
|
|
8
|
+
export declare function readNewEntries(filePath: string, afterSeq: number): {
|
|
9
|
+
entries: RawResponseEntry[];
|
|
10
|
+
lastSeq: number;
|
|
11
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.readNewEntries = readNewEntries;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const fs_1 = tslib_1.__importDefault(require("fs"));
|
|
6
|
+
function readNewEntries(filePath, afterSeq) {
|
|
7
|
+
if (!fs_1.default.existsSync(filePath)) {
|
|
8
|
+
return { entries: [], lastSeq: afterSeq };
|
|
9
|
+
}
|
|
10
|
+
const content = fs_1.default.readFileSync(filePath, 'utf-8');
|
|
11
|
+
const lines = content.split('\n').filter(line => line.trim().length > 0);
|
|
12
|
+
const entries = [];
|
|
13
|
+
let lastSeq = afterSeq;
|
|
14
|
+
for (const line of lines) {
|
|
15
|
+
try {
|
|
16
|
+
const entry = JSON.parse(line);
|
|
17
|
+
if (entry.seq > afterSeq) {
|
|
18
|
+
entries.push(entry);
|
|
19
|
+
if (entry.seq > lastSeq) {
|
|
20
|
+
lastSeq = entry.seq;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
// Skip malformed lines.
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return { entries, lastSeq };
|
|
29
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const tslib_1 = require("tslib");
|
|
4
|
+
const fs_1 = tslib_1.__importDefault(require("fs"));
|
|
5
|
+
const test_1 = require("@oclif/test");
|
|
6
|
+
const sinon_1 = tslib_1.__importDefault(require("sinon"));
|
|
7
|
+
const Configuration = tslib_1.__importStar(require("../../src/resources/configuration"));
|
|
8
|
+
const Decryption = tslib_1.__importStar(require("../../src/resources/decryption"));
|
|
9
|
+
const Integrations = tslib_1.__importStar(require("../../src/resources/integrations"));
|
|
10
|
+
const MOCK_CONFIG = {
|
|
11
|
+
name: 'salesforce-v2',
|
|
12
|
+
testAccounts: {
|
|
13
|
+
development: { token: 'test-token' },
|
|
14
|
+
},
|
|
15
|
+
secrets: {},
|
|
16
|
+
};
|
|
17
|
+
const MOCK_RESPONSE = {
|
|
18
|
+
label: 'Opportunity',
|
|
19
|
+
fields: [{ name: 'Name', type: 'string' }],
|
|
20
|
+
relations: [{ name: 'tasks', semantic: 'subtasks', path: '/sobjects/Task/records?filter=WhatId=abc' }],
|
|
21
|
+
};
|
|
22
|
+
describe('graph', () => {
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
sinon_1.default.stub(Integrations, 'validateIsIntegrationDirectory');
|
|
25
|
+
sinon_1.default.stub(Decryption, 'decryptEntries').resolves({
|
|
26
|
+
successful: { token: 'test-token' },
|
|
27
|
+
failed: [],
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
afterEach(() => sinon_1.default.restore());
|
|
31
|
+
test_1.test
|
|
32
|
+
.stub(Configuration, 'getConfiguration', stub => stub.resolves(MOCK_CONFIG))
|
|
33
|
+
.stub(global, 'fetch', stub => stub.resolves({
|
|
34
|
+
ok: true,
|
|
35
|
+
status: 200,
|
|
36
|
+
json: () => Promise.resolve(MOCK_RESPONSE),
|
|
37
|
+
}))
|
|
38
|
+
.stdout()
|
|
39
|
+
.command(['graph', '--path=/sobjects/Opportunity/records/abc123'])
|
|
40
|
+
.it('prints pretty-printed JSON response to stdout', ctx => {
|
|
41
|
+
(0, test_1.expect)(ctx.stdout).to.contain('"label": "Opportunity"');
|
|
42
|
+
(0, test_1.expect)(ctx.stdout).to.contain('"semantic": "subtasks"');
|
|
43
|
+
});
|
|
44
|
+
test_1.test
|
|
45
|
+
.stub(Configuration, 'getConfiguration', stub => stub.resolves(MOCK_CONFIG))
|
|
46
|
+
.stub(global, 'fetch', stub => stub.resolves({
|
|
47
|
+
ok: true,
|
|
48
|
+
status: 200,
|
|
49
|
+
json: () => Promise.resolve(MOCK_RESPONSE),
|
|
50
|
+
}))
|
|
51
|
+
.stdout()
|
|
52
|
+
.command(['graph', '--path=/sobjects/Opportunity/records/abc123'])
|
|
53
|
+
.it('calls fetch with the correct default-port URL', () => {
|
|
54
|
+
const fetchStub = global.fetch;
|
|
55
|
+
(0, test_1.expect)(fetchStub.calledOnce).to.be.true;
|
|
56
|
+
const [calledUrl] = fetchStub.firstCall.args;
|
|
57
|
+
(0, test_1.expect)(calledUrl).to.equal('http://localhost:9200/sobjects/Opportunity/records/abc123');
|
|
58
|
+
});
|
|
59
|
+
test_1.test
|
|
60
|
+
.stub(Configuration, 'getConfiguration', stub => stub.resolves(MOCK_CONFIG))
|
|
61
|
+
.stub(global, 'fetch', stub => stub.resolves({
|
|
62
|
+
ok: true,
|
|
63
|
+
status: 200,
|
|
64
|
+
json: () => Promise.resolve(MOCK_RESPONSE),
|
|
65
|
+
}))
|
|
66
|
+
.stdout()
|
|
67
|
+
.command(['graph', '--path=/sobjects/Opportunity', '--port=9201'])
|
|
68
|
+
.it('uses custom port when specified', () => {
|
|
69
|
+
const fetchStub = global.fetch;
|
|
70
|
+
const [calledUrl] = fetchStub.firstCall.args;
|
|
71
|
+
(0, test_1.expect)(calledUrl).to.equal('http://localhost:9201/sobjects/Opportunity');
|
|
72
|
+
});
|
|
73
|
+
test_1.test
|
|
74
|
+
.stub(Configuration, 'getConfiguration', stub => stub.resolves(MOCK_CONFIG))
|
|
75
|
+
.stub(global, 'fetch', stub => stub.resolves({
|
|
76
|
+
ok: false,
|
|
77
|
+
status: 404,
|
|
78
|
+
json: () => Promise.resolve({ error: 'Not found' }),
|
|
79
|
+
}))
|
|
80
|
+
.stdout()
|
|
81
|
+
.stderr()
|
|
82
|
+
.command(['graph', '--path=/sobjects/NonExistent/records/abc'])
|
|
83
|
+
.catch(err => {
|
|
84
|
+
(0, test_1.expect)(err.message).to.contain('404');
|
|
85
|
+
(0, test_1.expect)(err.oclif?.exit).to.equal(404);
|
|
86
|
+
})
|
|
87
|
+
.it('exits with HTTP status code and prints error body to stderr', ctx => {
|
|
88
|
+
(0, test_1.expect)(ctx.stderr).to.contain('"error": "Not found"');
|
|
89
|
+
});
|
|
90
|
+
test_1.test
|
|
91
|
+
.stub(Configuration, 'getConfiguration', stub => stub.resolves(MOCK_CONFIG))
|
|
92
|
+
.stub(global, 'fetch', stub => stub.resolves({
|
|
93
|
+
ok: true,
|
|
94
|
+
status: 200,
|
|
95
|
+
json: () => Promise.resolve(MOCK_RESPONSE),
|
|
96
|
+
}))
|
|
97
|
+
.stdout()
|
|
98
|
+
.command(['graph', '--path=/sobjects/Opportunity'])
|
|
99
|
+
.it('sets base64-encoded X-Unito-Credentials header', () => {
|
|
100
|
+
const fetchStub = global.fetch;
|
|
101
|
+
const [, options] = fetchStub.firstCall.args;
|
|
102
|
+
const decoded = Buffer.from(options.headers['X-Unito-Credentials'], 'base64').toString('utf8');
|
|
103
|
+
const credentials = JSON.parse(decoded);
|
|
104
|
+
(0, test_1.expect)(credentials).to.have.property('token', 'test-token');
|
|
105
|
+
});
|
|
106
|
+
test_1.test
|
|
107
|
+
.stub(Configuration, 'getConfiguration', stub => stub.resolves(MOCK_CONFIG))
|
|
108
|
+
.stub(global, 'fetch', stub => stub.resolves({
|
|
109
|
+
ok: true,
|
|
110
|
+
status: 200,
|
|
111
|
+
json: () => Promise.resolve(MOCK_RESPONSE),
|
|
112
|
+
}))
|
|
113
|
+
.stub(fs_1.default.promises, 'writeFile', stub => stub.resolves())
|
|
114
|
+
.stdout()
|
|
115
|
+
.command(['graph', '--path=/sobjects/Opportunity', '--output=/tmp/response.json'])
|
|
116
|
+
.it('writes response to file when --output is provided', ctx => {
|
|
117
|
+
const writeStub = fs_1.default.promises.writeFile;
|
|
118
|
+
(0, test_1.expect)(writeStub.calledOnce).to.be.true;
|
|
119
|
+
const [path, content] = writeStub.firstCall.args;
|
|
120
|
+
(0, test_1.expect)(path).to.equal('/tmp/response.json');
|
|
121
|
+
(0, test_1.expect)(content).to.contain('"label": "Opportunity"');
|
|
122
|
+
(0, test_1.expect)(ctx.stdout).to.equal('');
|
|
123
|
+
});
|
|
124
|
+
test_1.test
|
|
125
|
+
.stub(Configuration, 'getConfiguration', stub => stub.resolves(MOCK_CONFIG))
|
|
126
|
+
.stub(global, 'fetch', stub => stub.resolves({
|
|
127
|
+
ok: true,
|
|
128
|
+
status: 200,
|
|
129
|
+
json: () => Promise.resolve(MOCK_RESPONSE),
|
|
130
|
+
}))
|
|
131
|
+
.stdout()
|
|
132
|
+
.command(['graph', '--path=/sobjects/Opportunity'])
|
|
133
|
+
.it('prints to stdout when --output is not provided', ctx => {
|
|
134
|
+
(0, test_1.expect)(ctx.stdout).to.contain('"label": "Opportunity"');
|
|
135
|
+
});
|
|
136
|
+
test_1.test
|
|
137
|
+
.stub(Configuration, 'getConfiguration', stub => stub.resolves(MOCK_CONFIG))
|
|
138
|
+
.stub(global, 'fetch', stub => stub.resolves({
|
|
139
|
+
ok: true,
|
|
140
|
+
status: 200,
|
|
141
|
+
json: () => Promise.resolve(MOCK_RESPONSE),
|
|
142
|
+
}))
|
|
143
|
+
.stdout()
|
|
144
|
+
.command(['graph', 'get', '--path=/sobjects/Opportunity'])
|
|
145
|
+
.it('accepts explicit "get" operation arg', ctx => {
|
|
146
|
+
(0, test_1.expect)(ctx.stdout).to.contain('"label": "Opportunity"');
|
|
147
|
+
});
|
|
148
|
+
test_1.test
|
|
149
|
+
.stub(Configuration, 'getConfiguration', stub => stub.resolves(MOCK_CONFIG))
|
|
150
|
+
.stub(global, 'fetch', stub => stub.resolves({
|
|
151
|
+
ok: true,
|
|
152
|
+
status: 200,
|
|
153
|
+
json: () => Promise.resolve(MOCK_RESPONSE),
|
|
154
|
+
}))
|
|
155
|
+
.stdout()
|
|
156
|
+
.command(['graph', 'getItem', '--path=/sobjects/Opportunity'])
|
|
157
|
+
.it('treats "getItem" as a get operation', ctx => {
|
|
158
|
+
(0, test_1.expect)(ctx.stdout).to.contain('"label": "Opportunity"');
|
|
159
|
+
});
|
|
160
|
+
test_1.test
|
|
161
|
+
.stub(Configuration, 'getConfiguration', stub => stub.resolves(MOCK_CONFIG))
|
|
162
|
+
.stub(global, 'fetch', stub => stub.resolves({
|
|
163
|
+
ok: true,
|
|
164
|
+
status: 200,
|
|
165
|
+
json: () => Promise.resolve(MOCK_RESPONSE),
|
|
166
|
+
}))
|
|
167
|
+
.stdout()
|
|
168
|
+
.command(['graph', 'getCollection', '--path=/sobjects/Opportunity'])
|
|
169
|
+
.it('treats "getCollection" as a get operation', ctx => {
|
|
170
|
+
(0, test_1.expect)(ctx.stdout).to.contain('"label": "Opportunity"');
|
|
171
|
+
});
|
|
172
|
+
for (const op of ['createItem', 'updateItem', 'deleteItem']) {
|
|
173
|
+
test_1.test
|
|
174
|
+
.stub(Configuration, 'getConfiguration', stub => stub.resolves(MOCK_CONFIG))
|
|
175
|
+
.command(['graph', op, '--path=/sobjects/Opportunity'])
|
|
176
|
+
.catch(err => {
|
|
177
|
+
(0, test_1.expect)(err.message).to.contain(`Operation "${op}" is not yet implemented`);
|
|
178
|
+
})
|
|
179
|
+
.it(`rejects "${op}" as not yet implemented`);
|
|
180
|
+
}
|
|
181
|
+
test_1.test
|
|
182
|
+
.stderr()
|
|
183
|
+
.command(['graph', 'put', '--path=/sobjects/Opportunity'])
|
|
184
|
+
.catch(err => {
|
|
185
|
+
(0, test_1.expect)(err.message).to.contain('Expected put to be one of');
|
|
186
|
+
})
|
|
187
|
+
.it('rejects invalid operation arg');
|
|
188
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const tslib_1 = require("tslib");
|
|
4
|
+
const strict_1 = tslib_1.__importDefault(require("node:assert/strict"));
|
|
5
|
+
const coverageCalculator_1 = require("../../src/resources/coverageCalculator");
|
|
6
|
+
describe('computeCoverage', () => {
|
|
7
|
+
it('computes coverage for fully mapped fields', () => {
|
|
8
|
+
const rawApiFields = ['id', 'name', 'status'];
|
|
9
|
+
const schemaFields = ['id', 'name', 'status'];
|
|
10
|
+
const result = (0, coverageCalculator_1.computeCoverage)(rawApiFields, schemaFields);
|
|
11
|
+
strict_1.default.deepEqual(result.mappedFields, ['id', 'name', 'status']);
|
|
12
|
+
strict_1.default.deepEqual(result.unmappedFields, []);
|
|
13
|
+
strict_1.default.equal(result.coveragePercent, 100);
|
|
14
|
+
});
|
|
15
|
+
it('computes coverage with unmapped fields', () => {
|
|
16
|
+
const rawApiFields = ['id', 'name', 'status', 'priority', 'created_at'];
|
|
17
|
+
const schemaFields = ['id', 'name', 'status'];
|
|
18
|
+
const result = (0, coverageCalculator_1.computeCoverage)(rawApiFields, schemaFields);
|
|
19
|
+
strict_1.default.deepEqual(result.mappedFields, ['id', 'name', 'status']);
|
|
20
|
+
strict_1.default.deepEqual(result.unmappedFields, ['created_at', 'priority']);
|
|
21
|
+
strict_1.default.equal(result.coveragePercent, 60);
|
|
22
|
+
});
|
|
23
|
+
it('handles no raw API fields', () => {
|
|
24
|
+
const result = (0, coverageCalculator_1.computeCoverage)([], ['id', 'name']);
|
|
25
|
+
strict_1.default.deepEqual(result.mappedFields, []);
|
|
26
|
+
strict_1.default.deepEqual(result.unmappedFields, []);
|
|
27
|
+
strict_1.default.equal(result.coveragePercent, 100);
|
|
28
|
+
});
|
|
29
|
+
it('handles no schema fields', () => {
|
|
30
|
+
const result = (0, coverageCalculator_1.computeCoverage)(['id', 'name'], []);
|
|
31
|
+
strict_1.default.deepEqual(result.mappedFields, []);
|
|
32
|
+
strict_1.default.deepEqual(result.unmappedFields, ['id', 'name']);
|
|
33
|
+
strict_1.default.equal(result.coveragePercent, 0);
|
|
34
|
+
});
|
|
35
|
+
it('matches nested dot-notation fields against flat schema names', () => {
|
|
36
|
+
const rawApiFields = ['id', 'assignee', 'assignee.id', 'assignee.name'];
|
|
37
|
+
const schemaFields = ['id', 'assignee'];
|
|
38
|
+
const result = (0, coverageCalculator_1.computeCoverage)(rawApiFields, schemaFields);
|
|
39
|
+
strict_1.default.deepEqual(result.mappedFields, ['assignee', 'id']);
|
|
40
|
+
strict_1.default.deepEqual(result.unmappedFields, ['assignee.id', 'assignee.name']);
|
|
41
|
+
strict_1.default.equal(result.coveragePercent, 50);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
describe('applyAllowlist', () => {
|
|
45
|
+
it('suppresses exact field matches', () => {
|
|
46
|
+
const unmappedFields = ['priority', 'created_at', 'labels'];
|
|
47
|
+
const allowlist = [{ field: 'priority', reason: 'Not useful' }];
|
|
48
|
+
const result = (0, coverageCalculator_1.applyAllowlist)(unmappedFields, allowlist);
|
|
49
|
+
strict_1.default.deepEqual(result, ['created_at', 'labels']);
|
|
50
|
+
});
|
|
51
|
+
it('suppresses glob patterns with *', () => {
|
|
52
|
+
const unmappedFields = ['metadata', 'metadata.created_at', 'metadata.updated_at', 'labels'];
|
|
53
|
+
const allowlist = [{ field: 'metadata.*', reason: 'Internal tracking' }];
|
|
54
|
+
const result = (0, coverageCalculator_1.applyAllowlist)(unmappedFields, allowlist);
|
|
55
|
+
strict_1.default.deepEqual(result, ['labels', 'metadata']);
|
|
56
|
+
});
|
|
57
|
+
it('suppresses glob patterns with **', () => {
|
|
58
|
+
const unmappedFields = ['meta', 'meta.a', 'meta.b.c', 'meta.b.c.d', 'name'];
|
|
59
|
+
const allowlist = [{ field: 'meta.**', reason: 'All metadata' }];
|
|
60
|
+
const result = (0, coverageCalculator_1.applyAllowlist)(unmappedFields, allowlist);
|
|
61
|
+
strict_1.default.deepEqual(result, ['meta', 'name']);
|
|
62
|
+
});
|
|
63
|
+
it('returns all fields when allowlist is empty', () => {
|
|
64
|
+
const unmappedFields = ['a', 'b', 'c'];
|
|
65
|
+
strict_1.default.deepEqual((0, coverageCalculator_1.applyAllowlist)(unmappedFields, []), ['a', 'b', 'c']);
|
|
66
|
+
});
|
|
67
|
+
it('sorts output even with empty allowlist', () => {
|
|
68
|
+
const unmappedFields = ['z', 'a', 'name'];
|
|
69
|
+
strict_1.default.deepEqual((0, coverageCalculator_1.applyAllowlist)(unmappedFields, []), ['a', 'name', 'z']);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const tslib_1 = require("tslib");
|
|
4
|
+
const strict_1 = tslib_1.__importDefault(require("node:assert/strict"));
|
|
5
|
+
const fieldExtractor_1 = require("../../src/resources/fieldExtractor");
|
|
6
|
+
describe('extractFields', () => {
|
|
7
|
+
it('extracts top-level primitive fields', () => {
|
|
8
|
+
const body = { id: '123', name: 'Task', status: 'open' };
|
|
9
|
+
strict_1.default.deepEqual((0, fieldExtractor_1.extractFields)(body), ['id', 'name', 'status']);
|
|
10
|
+
});
|
|
11
|
+
it('extracts nested object fields with dot notation', () => {
|
|
12
|
+
const body = { id: '123', assignee: { id: '456', name: 'Alice' } };
|
|
13
|
+
strict_1.default.deepEqual((0, fieldExtractor_1.extractFields)(body), ['assignee', 'assignee.id', 'assignee.name', 'id']);
|
|
14
|
+
});
|
|
15
|
+
it('extracts deeply nested fields', () => {
|
|
16
|
+
const body = { meta: { creator: { id: '1', name: 'Bob' } } };
|
|
17
|
+
strict_1.default.deepEqual((0, fieldExtractor_1.extractFields)(body), ['meta', 'meta.creator', 'meta.creator.id', 'meta.creator.name']);
|
|
18
|
+
});
|
|
19
|
+
it('handles arrays of primitives as a single field', () => {
|
|
20
|
+
const body = { id: '123', labels: ['bug', 'urgent'] };
|
|
21
|
+
strict_1.default.deepEqual((0, fieldExtractor_1.extractFields)(body), ['id', 'labels']);
|
|
22
|
+
});
|
|
23
|
+
it('extracts fields from first item of array of objects', () => {
|
|
24
|
+
const body = { id: '123', items: [{ name: 'A', value: 1 }, { name: 'B' }] };
|
|
25
|
+
strict_1.default.deepEqual((0, fieldExtractor_1.extractFields)(body), ['id', 'items', 'items.name', 'items.value']);
|
|
26
|
+
});
|
|
27
|
+
it('handles top-level array by extracting from first item', () => {
|
|
28
|
+
const body = [{ id: '1', name: 'Task' }, { id: '2' }];
|
|
29
|
+
strict_1.default.deepEqual((0, fieldExtractor_1.extractFields)(body), ['id', 'name']);
|
|
30
|
+
});
|
|
31
|
+
it('returns empty array for null/undefined', () => {
|
|
32
|
+
strict_1.default.deepEqual((0, fieldExtractor_1.extractFields)(null), []);
|
|
33
|
+
strict_1.default.deepEqual((0, fieldExtractor_1.extractFields)(undefined), []);
|
|
34
|
+
});
|
|
35
|
+
it('returns empty array for empty object', () => {
|
|
36
|
+
strict_1.default.deepEqual((0, fieldExtractor_1.extractFields)({}), []);
|
|
37
|
+
});
|
|
38
|
+
it('returns empty array for empty array', () => {
|
|
39
|
+
strict_1.default.deepEqual((0, fieldExtractor_1.extractFields)([]), []);
|
|
40
|
+
});
|
|
41
|
+
it('handles mixed nested structures', () => {
|
|
42
|
+
const body = {
|
|
43
|
+
id: '123',
|
|
44
|
+
owner: { id: '456', profile: { avatar: 'url', bio: 'hello' } },
|
|
45
|
+
tags: ['a', 'b'],
|
|
46
|
+
settings: { color: 'blue' },
|
|
47
|
+
};
|
|
48
|
+
strict_1.default.deepEqual((0, fieldExtractor_1.extractFields)(body), [
|
|
49
|
+
'id',
|
|
50
|
+
'owner',
|
|
51
|
+
'owner.id',
|
|
52
|
+
'owner.profile',
|
|
53
|
+
'owner.profile.avatar',
|
|
54
|
+
'owner.profile.bio',
|
|
55
|
+
'settings',
|
|
56
|
+
'settings.color',
|
|
57
|
+
'tags',
|
|
58
|
+
]);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const tslib_1 = require("tslib");
|
|
4
|
+
const strict_1 = tslib_1.__importDefault(require("node:assert/strict"));
|
|
5
|
+
const fs_1 = tslib_1.__importDefault(require("fs"));
|
|
6
|
+
const tmp_1 = tslib_1.__importDefault(require("tmp"));
|
|
7
|
+
const jsonlReader_1 = require("../../src/resources/jsonlReader");
|
|
8
|
+
describe('readNewEntries', () => {
|
|
9
|
+
let tempFile;
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
tempFile = tmp_1.default.fileSync({ postfix: '.jsonl' });
|
|
12
|
+
});
|
|
13
|
+
afterEach(() => {
|
|
14
|
+
tempFile.removeCallback();
|
|
15
|
+
});
|
|
16
|
+
it('reads all entries from a new file', () => {
|
|
17
|
+
const entries = [
|
|
18
|
+
{ seq: 1, url: 'https://api.example.com/items', method: 'GET', status: 200, body: { id: '1', name: 'A' } },
|
|
19
|
+
{
|
|
20
|
+
seq: 2,
|
|
21
|
+
url: 'https://api.example.com/items/1',
|
|
22
|
+
method: 'GET',
|
|
23
|
+
status: 200,
|
|
24
|
+
body: { id: '1', name: 'A', desc: 'B' },
|
|
25
|
+
},
|
|
26
|
+
];
|
|
27
|
+
fs_1.default.writeFileSync(tempFile.name, entries.map(e => JSON.stringify(e)).join('\n') + '\n');
|
|
28
|
+
const result = (0, jsonlReader_1.readNewEntries)(tempFile.name, 0);
|
|
29
|
+
strict_1.default.equal(result.entries.length, 2);
|
|
30
|
+
strict_1.default.equal(result.entries[0].seq, 1);
|
|
31
|
+
strict_1.default.equal(result.entries[1].seq, 2);
|
|
32
|
+
strict_1.default.equal(result.lastSeq, 2);
|
|
33
|
+
});
|
|
34
|
+
it('reads only entries after a given seq', () => {
|
|
35
|
+
const entries = [
|
|
36
|
+
{ seq: 1, url: 'https://api.example.com/a', method: 'GET', status: 200, body: {} },
|
|
37
|
+
{ seq: 2, url: 'https://api.example.com/b', method: 'GET', status: 200, body: {} },
|
|
38
|
+
{ seq: 3, url: 'https://api.example.com/c', method: 'GET', status: 200, body: {} },
|
|
39
|
+
];
|
|
40
|
+
fs_1.default.writeFileSync(tempFile.name, entries.map(e => JSON.stringify(e)).join('\n') + '\n');
|
|
41
|
+
const result = (0, jsonlReader_1.readNewEntries)(tempFile.name, 1);
|
|
42
|
+
strict_1.default.equal(result.entries.length, 2);
|
|
43
|
+
strict_1.default.equal(result.entries[0].seq, 2);
|
|
44
|
+
strict_1.default.equal(result.lastSeq, 3);
|
|
45
|
+
});
|
|
46
|
+
it('returns empty when no new entries', () => {
|
|
47
|
+
const entries = [
|
|
48
|
+
{ seq: 1, url: 'https://api.example.com/a', method: 'GET', status: 200, body: {} },
|
|
49
|
+
];
|
|
50
|
+
fs_1.default.writeFileSync(tempFile.name, entries.map(e => JSON.stringify(e)).join('\n') + '\n');
|
|
51
|
+
const result = (0, jsonlReader_1.readNewEntries)(tempFile.name, 1);
|
|
52
|
+
strict_1.default.equal(result.entries.length, 0);
|
|
53
|
+
strict_1.default.equal(result.lastSeq, 1);
|
|
54
|
+
});
|
|
55
|
+
it('returns empty when file does not exist', () => {
|
|
56
|
+
const result = (0, jsonlReader_1.readNewEntries)('/nonexistent/path.jsonl', 0);
|
|
57
|
+
strict_1.default.equal(result.entries.length, 0);
|
|
58
|
+
strict_1.default.equal(result.lastSeq, 0);
|
|
59
|
+
});
|
|
60
|
+
it('skips malformed lines gracefully', () => {
|
|
61
|
+
fs_1.default.writeFileSync(tempFile.name, '{"seq":1,"url":"a","method":"GET","status":200,"body":{}}\nnot-json\n{"seq":2,"url":"b","method":"GET","status":200,"body":{}}\n');
|
|
62
|
+
const result = (0, jsonlReader_1.readNewEntries)(tempFile.name, 0);
|
|
63
|
+
strict_1.default.equal(result.entries.length, 2);
|
|
64
|
+
strict_1.default.equal(result.lastSeq, 2);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"root":["../src/baseCommand.ts","../src/configurationTypes.ts","../src/errors.ts","../src/index.ts","../src/commands/activity.ts","../src/commands/dev.ts","../src/commands/encrypt.ts","../src/commands/init.ts","../src/commands/invite.ts","../src/commands/login.ts","../src/commands/oauth2.ts","../src/commands/publish.ts","../src/commands/test.ts","../src/commands/upgrade.ts","../src/hooks/init/displayLogo.ts","../src/hooks/init/updateNotifier.ts","../src/resources/configuration.ts","../src/resources/credentials.ts","../src/resources/decryption.ts","../src/resources/fileSystem.ts","../src/resources/globalConfiguration.ts","../src/resources/integrations.ts","../src/resources/integrationsPlatform.ts","../src/resources/oauth2.ts","../src/resources/template.ts","../src/services/integrationsPlatform.ts","../src/services/integrationsPlatformClient.ts","../src/services/oauth2.ts","../test/errors.test.ts","../test/commands/activity.test.ts","../test/commands/dev.test.ts","../test/commands/encrypt.test.ts","../test/commands/init.test.ts","../test/commands/invite.test.ts","../test/commands/login.test.ts","../test/commands/oauth2.test.ts","../test/commands/publish.test.ts","../test/commands/test.test.ts","../test/commands/upgrade.test.ts","../test/helpers/init.js","../test/helpers/integrations.ts","../test/helpers/styles.ts","../test/hooks/updateNotifier.test.ts","../test/resources/configuration.test.ts","../test/resources/decryption.test.ts","../test/resources/globalConfiguration.test.ts","../test/resources/integrations.test.ts","../test/resources/oauth2.test.ts","../test/resources/template.test.ts","../test/services/integrationsPlatform.test.ts","../test/services/oauth2.test.ts","../scripts/generateTypes.ts","../.eslintrc.js"],"version":"5.9.3"}
|
|
1
|
+
{"root":["../src/baseCommand.ts","../src/configurationTypes.ts","../src/errors.ts","../src/index.ts","../src/commands/activity.ts","../src/commands/dev.ts","../src/commands/encrypt.ts","../src/commands/graph.ts","../src/commands/init.ts","../src/commands/invite.ts","../src/commands/login.ts","../src/commands/oauth2.ts","../src/commands/publish.ts","../src/commands/schema-snapshot.ts","../src/commands/test.ts","../src/commands/upgrade.ts","../src/hooks/init/displayLogo.ts","../src/hooks/init/updateNotifier.ts","../src/resources/configuration.ts","../src/resources/coverageCalculator.ts","../src/resources/credentials.ts","../src/resources/decryption.ts","../src/resources/fieldExtractor.ts","../src/resources/fileSystem.ts","../src/resources/globalConfiguration.ts","../src/resources/integrations.ts","../src/resources/integrationsPlatform.ts","../src/resources/jsonlReader.ts","../src/resources/oauth2.ts","../src/resources/template.ts","../src/services/integrationsPlatform.ts","../src/services/integrationsPlatformClient.ts","../src/services/oauth2.ts","../test/errors.test.ts","../test/commands/activity.test.ts","../test/commands/dev.test.ts","../test/commands/encrypt.test.ts","../test/commands/graph.test.ts","../test/commands/init.test.ts","../test/commands/invite.test.ts","../test/commands/login.test.ts","../test/commands/oauth2.test.ts","../test/commands/publish.test.ts","../test/commands/test.test.ts","../test/commands/upgrade.test.ts","../test/helpers/init.js","../test/helpers/integrations.ts","../test/helpers/styles.ts","../test/hooks/updateNotifier.test.ts","../test/resources/configuration.test.ts","../test/resources/coverageCalculator.test.ts","../test/resources/decryption.test.ts","../test/resources/fieldExtractor.test.ts","../test/resources/globalConfiguration.test.ts","../test/resources/integrations.test.ts","../test/resources/jsonlReader.test.ts","../test/resources/oauth2.test.ts","../test/resources/template.test.ts","../test/services/integrationsPlatform.test.ts","../test/services/oauth2.test.ts","../scripts/generateTypes.ts","../.eslintrc.js"],"version":"5.9.3"}
|