nango 0.69.21 → 0.69.26
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/index.js +122 -20
- package/dist/index.js.map +1 -1
- package/dist/services/clone.service.d.ts +12 -0
- package/dist/services/clone.service.js +440 -0
- package/dist/services/clone.service.js.map +1 -0
- package/dist/services/dryrun.service.js +23 -82
- package/dist/services/dryrun.service.js.map +1 -1
- package/dist/services/ensure.service.d.ts +18 -0
- package/dist/services/ensure.service.js +60 -0
- package/dist/services/ensure.service.js.map +1 -0
- package/dist/services/function-create.service.d.ts +3 -4
- package/dist/services/function-create.service.js +2 -12
- package/dist/services/function-create.service.js.map +1 -1
- package/dist/services/interactive.service.d.ts +13 -0
- package/dist/services/interactive.service.js +147 -0
- package/dist/services/interactive.service.js.map +1 -0
- package/dist/services/response-collector.service.d.ts +23 -0
- package/dist/services/response-collector.service.js +233 -0
- package/dist/services/response-collector.service.js.map +1 -0
- package/dist/services/sdk.js +2 -1
- package/dist/services/sdk.js.map +1 -1
- package/dist/testMocks/utils.d.ts +137 -51
- package/dist/testMocks/utils.js +755 -172
- package/dist/testMocks/utils.js.map +1 -1
- package/dist/types.d.ts +3 -0
- package/dist/types.js +1 -1
- package/dist/types.js.map +1 -1
- package/dist/utils/errors.d.ts +3 -0
- package/dist/utils/errors.js +6 -0
- package/dist/utils/errors.js.map +1 -1
- package/dist/utils.d.ts +2 -1
- package/dist/utils.js +15 -0
- package/dist/utils.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +11 -8
- package/dist/migrations/toZeroYaml.unit.test.d.ts +0 -1
- package/dist/migrations/toZeroYaml.unit.test.js +0 -307
- package/dist/migrations/toZeroYaml.unit.test.js.map +0 -1
- package/dist/services/compile.service.unit.test.d.ts +0 -1
- package/dist/services/compile.service.unit.test.js +0 -44
- package/dist/services/compile.service.unit.test.js.map +0 -1
- package/dist/services/config.service.unit.test.d.ts +0 -1
- package/dist/services/config.service.unit.test.js +0 -162
- package/dist/services/config.service.unit.test.js.map +0 -1
- package/dist/services/docs.service.unit.test.d.ts +0 -1
- package/dist/services/docs.service.unit.test.js +0 -51
- package/dist/services/docs.service.unit.test.js.map +0 -1
- package/dist/services/model.service.unit.test.d.ts +0 -1
- package/dist/services/model.service.unit.test.js +0 -240
- package/dist/services/model.service.unit.test.js.map +0 -1
- package/dist/services/response-saver.service.d.ts +0 -17
- package/dist/services/response-saver.service.js +0 -169
- package/dist/services/response-saver.service.js.map +0 -1
- package/dist/services/sdk.service.unit.test.d.ts +0 -1
- package/dist/services/sdk.service.unit.test.js +0 -66
- package/dist/services/sdk.service.unit.test.js.map +0 -1
- package/dist/zeroYaml/definitions.unit.test.d.ts +0 -1
- package/dist/zeroYaml/definitions.unit.test.js +0 -111
- package/dist/zeroYaml/definitions.unit.test.js.map +0 -1
- package/dist/zeroYaml/zodToNango.unit.test.d.ts +0 -1
- package/dist/zeroYaml/zodToNango.unit.test.js +0 -185
- package/dist/zeroYaml/zodToNango.unit.test.js.map +0 -1
package/dist/testMocks/utils.js
CHANGED
|
@@ -1,180 +1,600 @@
|
|
|
1
1
|
import * as crypto from 'crypto';
|
|
2
2
|
import { promises as fs } from 'fs';
|
|
3
3
|
import * as path from 'path';
|
|
4
|
+
import parseLinksHeader from 'parse-link-header';
|
|
4
5
|
import { vi } from 'vitest';
|
|
5
6
|
import { getProvider } from '@nangohq/providers';
|
|
6
|
-
import
|
|
7
|
-
class
|
|
7
|
+
import paginateService from '@nangohq/runner-sdk/lib/paginate.service.js';
|
|
8
|
+
class LegacyFixtureProvider {
|
|
8
9
|
dirname;
|
|
9
10
|
name;
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
activityLogId = 'test-activity-log-id';
|
|
14
|
-
syncId;
|
|
15
|
-
nangoConnectionId;
|
|
16
|
-
environmentId = 1;
|
|
17
|
-
environmentName;
|
|
18
|
-
syncJobId;
|
|
19
|
-
abortSignal;
|
|
20
|
-
syncConfig;
|
|
21
|
-
runnerFlags = {};
|
|
22
|
-
scriptType = 'action';
|
|
23
|
-
startTime = Date.now();
|
|
24
|
-
isCLI = false;
|
|
25
|
-
connectionId;
|
|
26
|
-
providerConfigKey;
|
|
27
|
-
provider;
|
|
28
|
-
ActionError = ActionError;
|
|
29
|
-
telemetryBag = {
|
|
30
|
-
customLogs: 0,
|
|
31
|
-
proxyCalls: 0,
|
|
32
|
-
durationMs: 0,
|
|
33
|
-
memoryGb: 1
|
|
34
|
-
};
|
|
35
|
-
logger = { level: 'debug' };
|
|
36
|
-
setLogger;
|
|
37
|
-
log;
|
|
38
|
-
getConnection;
|
|
39
|
-
getMetadata;
|
|
40
|
-
updateMetadata;
|
|
41
|
-
paginate;
|
|
42
|
-
get;
|
|
43
|
-
post;
|
|
44
|
-
patch;
|
|
45
|
-
put;
|
|
46
|
-
delete;
|
|
47
|
-
proxy;
|
|
48
|
-
getWebhookURL;
|
|
49
|
-
getToken;
|
|
50
|
-
getIntegration;
|
|
51
|
-
setMetadata;
|
|
52
|
-
setFieldMapping;
|
|
53
|
-
getFieldMapping;
|
|
54
|
-
getEnvironmentVariables;
|
|
55
|
-
getFlowAttributes;
|
|
56
|
-
triggerAction;
|
|
57
|
-
triggerSync;
|
|
58
|
-
zodValidateInput;
|
|
59
|
-
startSync;
|
|
60
|
-
uncontrolledFetch;
|
|
61
|
-
tryAcquireLock;
|
|
62
|
-
releaseLock;
|
|
63
|
-
releaseAllLocks;
|
|
64
|
-
constructor({ dirname, name, Model }) {
|
|
11
|
+
fileCache = new Map();
|
|
12
|
+
mockFileCache = new Map();
|
|
13
|
+
constructor(dirname, name) {
|
|
65
14
|
this.dirname = dirname;
|
|
66
|
-
this.nango = {
|
|
67
|
-
secretKey: 'secret-key'
|
|
68
|
-
};
|
|
69
|
-
this.providerConfigKey = dirname;
|
|
70
|
-
this.connectionId = 'test-connection-id';
|
|
71
15
|
this.name = name;
|
|
72
|
-
this.Model = Model;
|
|
73
|
-
this.paginationService = PaginationService;
|
|
74
|
-
this.setLogger = vi.fn();
|
|
75
|
-
this.log = vi.fn();
|
|
76
|
-
this.getConnection = vi.fn(this.getConnectionData.bind(this));
|
|
77
|
-
this.getMetadata = vi.fn(this.getMetadataData.bind(this));
|
|
78
|
-
this.paginate = vi.fn(this.getProxyPaginateData.bind(this));
|
|
79
|
-
this.get = vi.fn(this.proxyGetData.bind(this));
|
|
80
|
-
this.post = vi.fn(this.proxyPostData.bind(this));
|
|
81
|
-
this.patch = vi.fn(this.proxyPatchData.bind(this));
|
|
82
|
-
this.put = vi.fn(this.proxyPutData.bind(this));
|
|
83
|
-
this.delete = vi.fn(this.proxyDeleteData.bind(this));
|
|
84
|
-
this.proxy = vi.fn(this.proxyData.bind(this));
|
|
85
|
-
this.getWebhookURL = vi.fn(() => 'https://example.com/webhook');
|
|
86
|
-
this.updateMetadata = vi.fn();
|
|
87
|
-
this.getToken = vi.fn();
|
|
88
|
-
this.getIntegration = vi.fn();
|
|
89
|
-
this.setMetadata = vi.fn();
|
|
90
|
-
this.setFieldMapping = vi.fn();
|
|
91
|
-
this.getFieldMapping = vi.fn();
|
|
92
|
-
this.getEnvironmentVariables = vi.fn();
|
|
93
|
-
this.getFlowAttributes = vi.fn();
|
|
94
|
-
this.triggerAction = vi.fn();
|
|
95
|
-
this.triggerSync = vi.fn();
|
|
96
|
-
this.zodValidateInput = vi.fn();
|
|
97
|
-
this.startSync = vi.fn();
|
|
98
|
-
this.uncontrolledFetch = vi.fn();
|
|
99
|
-
this.tryAcquireLock = vi.fn();
|
|
100
|
-
this.releaseLock = vi.fn();
|
|
101
|
-
this.releaseAllLocks = vi.fn();
|
|
102
16
|
}
|
|
103
17
|
async getMockFile(fileName, throwOnMissing, identity) {
|
|
104
|
-
const filePath = path.resolve(this.dirname,
|
|
18
|
+
const filePath = path.resolve(this.dirname, `../mocks/${fileName}.json`);
|
|
105
19
|
try {
|
|
106
20
|
const fileContent = await fs.readFile(filePath, 'utf-8');
|
|
107
21
|
const data = JSON.parse(fileContent);
|
|
108
22
|
return data;
|
|
109
23
|
}
|
|
110
24
|
catch (err) {
|
|
111
|
-
if (identity && identity.requestIdentity && fileName.includes(identity.requestIdentityHash)) {
|
|
112
|
-
const normalizedHash = computeNormalizedHash(identity.requestIdentity);
|
|
113
|
-
if (normalizedHash !== identity.requestIdentityHash) {
|
|
114
|
-
const fallbackFileName = fileName.replace(identity.requestIdentityHash, normalizedHash);
|
|
115
|
-
const fallbackFilePath = path.resolve(this.dirname, `mocks/${fallbackFileName}.json`);
|
|
116
|
-
try {
|
|
117
|
-
const fileContent = await fs.readFile(fallbackFilePath, 'utf-8');
|
|
118
|
-
const data = JSON.parse(fileContent);
|
|
119
|
-
return data;
|
|
120
|
-
// eslint-disable-next-line no-empty
|
|
121
|
-
}
|
|
122
|
-
catch { }
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
25
|
if (throwOnMissing) {
|
|
126
26
|
throw new Error(`Failed to load mock data from ${filePath}: ${err.message} ${identity ? JSON.stringify(identity, null, 2) : ''}`);
|
|
127
27
|
}
|
|
28
|
+
return undefined;
|
|
128
29
|
}
|
|
129
30
|
}
|
|
130
31
|
async hashDirExists(hashDir) {
|
|
131
|
-
const filePath = path.resolve(this.dirname,
|
|
32
|
+
const filePath = path.resolve(this.dirname, `../mocks/${hashDir}/`);
|
|
132
33
|
try {
|
|
133
34
|
await fs.stat(filePath);
|
|
134
35
|
return true;
|
|
135
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
136
36
|
}
|
|
137
|
-
catch (
|
|
37
|
+
catch (_) {
|
|
138
38
|
return false;
|
|
139
39
|
}
|
|
140
40
|
}
|
|
41
|
+
async getFilesInDir(dir) {
|
|
42
|
+
if (this.fileCache.has(dir)) {
|
|
43
|
+
return this.fileCache.get(dir);
|
|
44
|
+
}
|
|
45
|
+
const dirPath = path.resolve(this.dirname, `../mocks/${dir}`);
|
|
46
|
+
try {
|
|
47
|
+
const entries = await fs.readdir(dirPath);
|
|
48
|
+
const jsonFiles = entries.filter((f) => f.endsWith('.json'));
|
|
49
|
+
this.fileCache.set(dir, jsonFiles);
|
|
50
|
+
return jsonFiles;
|
|
51
|
+
}
|
|
52
|
+
catch (_) {
|
|
53
|
+
this.fileCache.set(dir, []);
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
async parseMockFile(dir, filename) {
|
|
58
|
+
const cacheKey = `${dir}/${filename}`;
|
|
59
|
+
if (this.mockFileCache.has(cacheKey)) {
|
|
60
|
+
return this.mockFileCache.get(cacheKey);
|
|
61
|
+
}
|
|
62
|
+
const filePath = path.resolve(this.dirname, `../mocks/${dir}/${filename}`);
|
|
63
|
+
try {
|
|
64
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
65
|
+
const parsed = JSON.parse(content);
|
|
66
|
+
this.mockFileCache.set(cacheKey, parsed);
|
|
67
|
+
return parsed;
|
|
68
|
+
}
|
|
69
|
+
catch (_) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
paramsMatch(stored, incoming) {
|
|
74
|
+
if (stored.length !== incoming.length) {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
const sortedStored = [...stored].sort((a, b) => a[0].localeCompare(b[0]));
|
|
78
|
+
const sortedIncoming = [...incoming].sort((a, b) => a[0].localeCompare(b[0]));
|
|
79
|
+
for (const [index, storedEntry] of sortedStored.entries()) {
|
|
80
|
+
const incomingEntry = sortedIncoming[index];
|
|
81
|
+
if (!incomingEntry) {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
if (storedEntry[0] !== incomingEntry[0]) {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
if (String(storedEntry[1]) !== String(incomingEntry[1])) {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
async findMockByParams(dir, identity) {
|
|
94
|
+
const files = await this.getFilesInDir(dir);
|
|
95
|
+
for (const filename of files) {
|
|
96
|
+
const mockFile = await this.parseMockFile(dir, filename);
|
|
97
|
+
if (!mockFile || !mockFile.requestIdentity) {
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (this.paramsMatch(mockFile.requestIdentity.params, identity.requestIdentity.params)) {
|
|
101
|
+
if (mockFile.requestIdentity.headers.length > 0 || identity.requestIdentity.headers.length > 0) {
|
|
102
|
+
if (!this.paramsMatch(mockFile.requestIdentity.headers, identity.requestIdentity.headers)) {
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (mockFile.requestIdentity.data !== undefined || identity.requestIdentity.data !== undefined) {
|
|
107
|
+
const storedDataIdentity = mockFile.requestIdentity.data;
|
|
108
|
+
const incomingDataIdentity = identity.requestIdentity.data;
|
|
109
|
+
if (String(storedDataIdentity) !== String(incomingDataIdentity)) {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return mockFile;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
141
118
|
async getCachedResponse(identity) {
|
|
142
119
|
const dir = `nango/${identity.method}/proxy/${identity.endpoint}/${this.name}/`;
|
|
143
120
|
const hashBasedPath = `${dir}/${identity.requestIdentityHash}`;
|
|
144
121
|
if (await this.hashDirExists(dir)) {
|
|
145
|
-
const
|
|
146
|
-
|
|
122
|
+
const exactMatch = await this.getMockFile(hashBasedPath, false, identity);
|
|
123
|
+
if (exactMatch) {
|
|
124
|
+
return exactMatch;
|
|
125
|
+
}
|
|
126
|
+
// Hash didn't match - try to find by params (handles legacy pagination bug)
|
|
127
|
+
const paramMatch = await this.findMockByParams(dir, identity);
|
|
128
|
+
if (paramMatch) {
|
|
129
|
+
return paramMatch;
|
|
130
|
+
}
|
|
131
|
+
throw new Error(`Failed to load mock data for ${identity.method.toUpperCase()} ${identity.endpoint}\n` +
|
|
132
|
+
`Hash ${identity.requestIdentityHash} not found in ${dir}\n` +
|
|
133
|
+
`Request params: ${JSON.stringify(identity.requestIdentity.params)}\n` +
|
|
134
|
+
`This may be due to missing pagination mock files. Re-record with: nango dryrun <sync> <connection> --save`);
|
|
147
135
|
}
|
|
148
136
|
else {
|
|
149
|
-
return {
|
|
150
|
-
|
|
137
|
+
return { response: await this.getMockFile(`nango/${identity.method}/proxy/${identity.endpoint}/${this.name}`, true, identity) };
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
async getBatchSaveData(modelName) {
|
|
141
|
+
return this.getMockFile(`${this.name}/${modelName}/batchSave`, true);
|
|
142
|
+
}
|
|
143
|
+
async getBatchDeleteData(modelName) {
|
|
144
|
+
return this.getMockFile(`${this.name}/${modelName}/batchDelete`, true);
|
|
145
|
+
}
|
|
146
|
+
async getInput() {
|
|
147
|
+
return this.getMockFile(`${this.name}/input`, false);
|
|
148
|
+
}
|
|
149
|
+
async getOutput() {
|
|
150
|
+
return this.getMockFile(`${this.name}/output`, true);
|
|
151
|
+
}
|
|
152
|
+
async getConnectionData() {
|
|
153
|
+
return this.getMockFile(`nango/getConnection`, true);
|
|
154
|
+
}
|
|
155
|
+
async getMetadataData() {
|
|
156
|
+
return this.getMockFile('nango/getMetadata', true);
|
|
157
|
+
}
|
|
158
|
+
async getUpdateMetadata() {
|
|
159
|
+
return this.getMockFile('nango/updateMetadata', false);
|
|
160
|
+
}
|
|
161
|
+
async getDeleteRecordsFromPreviousExecutions() {
|
|
162
|
+
return this.getMockFile('nango/deleteRecordsFromPreviousExecutions', false);
|
|
163
|
+
}
|
|
164
|
+
isUnifiedMocks() {
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
async getAllMocksForEndpoint(method, endpoint) {
|
|
168
|
+
const normalizedEndpoint = endpoint.startsWith('/') ? endpoint.slice(1) : endpoint;
|
|
169
|
+
const dir = `nango/${method.toLowerCase()}/proxy/${normalizedEndpoint}/${this.name}/`;
|
|
170
|
+
// Check for hash-based directory format (used for paginated endpoints)
|
|
171
|
+
if (await this.hashDirExists(dir)) {
|
|
172
|
+
const files = await this.getFilesInDir(dir);
|
|
173
|
+
const mocks = [];
|
|
174
|
+
for (const filename of files) {
|
|
175
|
+
const mockFile = await this.parseMockFile(dir, filename);
|
|
176
|
+
if (mockFile) {
|
|
177
|
+
mocks.push(mockFile);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return mocks;
|
|
181
|
+
}
|
|
182
|
+
const nameBasedPath = `nango/${method.toLowerCase()}/proxy/${normalizedEndpoint}/${this.name}`;
|
|
183
|
+
const response = await this.getMockFile(nameBasedPath, false);
|
|
184
|
+
if (response) {
|
|
185
|
+
const syntheticMock = {
|
|
186
|
+
method: method.toLowerCase(),
|
|
187
|
+
endpoint: normalizedEndpoint,
|
|
188
|
+
requestIdentityHash: '',
|
|
189
|
+
requestIdentity: {
|
|
190
|
+
method: method.toLowerCase(),
|
|
191
|
+
endpoint: normalizedEndpoint,
|
|
192
|
+
params: [],
|
|
193
|
+
headers: []
|
|
194
|
+
},
|
|
195
|
+
response
|
|
151
196
|
};
|
|
197
|
+
return [syntheticMock];
|
|
198
|
+
}
|
|
199
|
+
return [];
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
class UnifiedFixtureProvider {
|
|
203
|
+
mockData;
|
|
204
|
+
isMigrated;
|
|
205
|
+
constructor(mockData) {
|
|
206
|
+
this.mockData = mockData;
|
|
207
|
+
this.isMigrated = mockData._migrated === true;
|
|
208
|
+
}
|
|
209
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
210
|
+
async getBatchSaveData(modelName) {
|
|
211
|
+
return this.mockData.nango?.batchSave?.[modelName];
|
|
212
|
+
}
|
|
213
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
214
|
+
async getBatchDeleteData(modelName) {
|
|
215
|
+
return this.mockData.nango?.batchDelete?.[modelName];
|
|
216
|
+
}
|
|
217
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
218
|
+
async getInput() {
|
|
219
|
+
return this.mockData.input;
|
|
220
|
+
}
|
|
221
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
222
|
+
async getOutput() {
|
|
223
|
+
const data = this.mockData.output;
|
|
224
|
+
if (data === undefined) {
|
|
225
|
+
throw new Error(`Missing mock data for output`);
|
|
226
|
+
}
|
|
227
|
+
return data;
|
|
228
|
+
}
|
|
229
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
230
|
+
async getConnectionData() {
|
|
231
|
+
const data = this.mockData.nango?.getConnection;
|
|
232
|
+
if (data === undefined) {
|
|
233
|
+
throw new Error(`Missing mock data for getConnection`);
|
|
234
|
+
}
|
|
235
|
+
return data;
|
|
236
|
+
}
|
|
237
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
238
|
+
async getMetadataData() {
|
|
239
|
+
const data = this.mockData.nango?.getMetadata;
|
|
240
|
+
if (data === undefined) {
|
|
241
|
+
throw new Error(`Missing mock data for getMetadata`);
|
|
152
242
|
}
|
|
243
|
+
return data;
|
|
244
|
+
}
|
|
245
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
246
|
+
async getUpdateMetadata() {
|
|
247
|
+
return this.mockData.nango?.updateMetadata;
|
|
248
|
+
}
|
|
249
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
250
|
+
async getDeleteRecordsFromPreviousExecutions() {
|
|
251
|
+
return this.mockData.nango?.deleteRecordsFromPreviousExecutions;
|
|
252
|
+
}
|
|
253
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
254
|
+
async getCachedResponse(identity) {
|
|
255
|
+
const { method, endpoint, requestIdentityHash } = identity;
|
|
256
|
+
const normalizedEndpoint = endpoint.startsWith('/') ? endpoint.slice(1) : endpoint;
|
|
257
|
+
let apiMock = this.mockData.api?.[method.toUpperCase()]?.[normalizedEndpoint];
|
|
258
|
+
if (!apiMock) {
|
|
259
|
+
apiMock = this.mockData.api?.[method.toUpperCase()]?.[`/${normalizedEndpoint}`];
|
|
260
|
+
}
|
|
261
|
+
if (apiMock) {
|
|
262
|
+
// Normalize to array for consistent matching logic
|
|
263
|
+
const mocks = Array.isArray(apiMock) ? apiMock : [apiMock];
|
|
264
|
+
const matchedMock = mocks.find((mock) => {
|
|
265
|
+
// Try exact hash match first
|
|
266
|
+
if (mock.hash && mock.hash === requestIdentityHash) {
|
|
267
|
+
return true;
|
|
268
|
+
}
|
|
269
|
+
// Try matching by request params/headers/data
|
|
270
|
+
if (mock.request) {
|
|
271
|
+
const mockParamCount = mock.request.params ? Object.keys(mock.request.params).length : 0;
|
|
272
|
+
const requestParamCount = identity.requestIdentity.params.length;
|
|
273
|
+
// Params must match exactly (same count and same values)
|
|
274
|
+
if (mockParamCount !== requestParamCount) {
|
|
275
|
+
return false;
|
|
276
|
+
}
|
|
277
|
+
if (mock.request.params) {
|
|
278
|
+
for (const [key, value] of Object.entries(mock.request.params)) {
|
|
279
|
+
const actualParam = identity.requestIdentity.params.find(([k]) => k === key);
|
|
280
|
+
if (!actualParam || String(actualParam[1]) !== String(value)) {
|
|
281
|
+
return false;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
const mockHeaderCount = mock.request.headers ? Object.keys(mock.request.headers).length : 0;
|
|
286
|
+
const requestHeaderCount = identity.requestIdentity.headers.length;
|
|
287
|
+
// Headers must match exactly (same count and same values)
|
|
288
|
+
if (mockHeaderCount !== requestHeaderCount) {
|
|
289
|
+
return false;
|
|
290
|
+
}
|
|
291
|
+
if (mock.request.headers) {
|
|
292
|
+
for (const [key, value] of Object.entries(mock.request.headers)) {
|
|
293
|
+
const actualHeader = identity.requestIdentity.headers.find(([k]) => k.toLowerCase() === key.toLowerCase());
|
|
294
|
+
if (!actualHeader || String(actualHeader[1]) !== String(value)) {
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
if (mock.request.data !== undefined) {
|
|
300
|
+
const expectedDataIdentity = computeDataIdentity({ data: mock.request.data });
|
|
301
|
+
if (expectedDataIdentity !== identity.requestIdentity.data) {
|
|
302
|
+
return false;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
return true;
|
|
306
|
+
}
|
|
307
|
+
// For mocks without request info or hash, only match if there's exactly one mock
|
|
308
|
+
// and no params in the request (i.e., it's an unpaginated endpoint)
|
|
309
|
+
if (mocks.length === 1 && identity.requestIdentity.params.length === 0) {
|
|
310
|
+
return true;
|
|
311
|
+
}
|
|
312
|
+
return false;
|
|
313
|
+
});
|
|
314
|
+
if (matchedMock) {
|
|
315
|
+
return matchedMock;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
const baseError = `No mock found for ${method.toUpperCase()} ${endpoint} (normalized: ${normalizedEndpoint}) with hash ${requestIdentityHash}`;
|
|
319
|
+
if (this.isMigrated) {
|
|
320
|
+
throw new Error(`${baseError}\n\n` +
|
|
321
|
+
`This mock file was migrated from the legacy format.\n` +
|
|
322
|
+
`The migration only captures mocks that were accessed during the test run.\n` +
|
|
323
|
+
`Due to a pagination bug in the legacy format, some pagination responses may not have been recorded.\n\n` +
|
|
324
|
+
`To fix this, re-record your mocks by running:\n` +
|
|
325
|
+
` nango dryrun <sync-name> <connection-id> --save`);
|
|
326
|
+
}
|
|
327
|
+
throw new Error(`${baseError}\n\n` + `To fix this, record the missing mock by running:\n` + ` nango dryrun <sync-name> <connection-id> --save`);
|
|
328
|
+
}
|
|
329
|
+
isUnifiedMocks() {
|
|
330
|
+
return true;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
class RecordingFixtureProvider {
|
|
334
|
+
delegate;
|
|
335
|
+
outputPath;
|
|
336
|
+
recordedData = {
|
|
337
|
+
input: null,
|
|
338
|
+
output: null,
|
|
339
|
+
nango: {},
|
|
340
|
+
api: {},
|
|
341
|
+
_migrated: true
|
|
342
|
+
};
|
|
343
|
+
constructor(delegate, outputPath) {
|
|
344
|
+
this.delegate = delegate;
|
|
345
|
+
this.outputPath = outputPath;
|
|
346
|
+
}
|
|
347
|
+
async save() {
|
|
348
|
+
const dataToSave = JSON.parse(JSON.stringify(this.recordedData));
|
|
349
|
+
// Prevent empty keys
|
|
350
|
+
if (Object.keys(dataToSave.nango?.batchSave || {}).length === 0)
|
|
351
|
+
delete dataToSave.nango?.batchSave;
|
|
352
|
+
if (Object.keys(dataToSave.nango?.batchDelete || {}).length === 0)
|
|
353
|
+
delete dataToSave.nango?.batchDelete;
|
|
354
|
+
if (Object.keys(dataToSave.nango || {}).length === 0)
|
|
355
|
+
delete dataToSave.nango;
|
|
356
|
+
if (Object.keys(dataToSave.api || {}).length === 0)
|
|
357
|
+
delete dataToSave.api;
|
|
358
|
+
if (dataToSave.input === null)
|
|
359
|
+
delete dataToSave.input;
|
|
360
|
+
if (dataToSave.output === null)
|
|
361
|
+
delete dataToSave.output;
|
|
362
|
+
await fs.writeFile(this.outputPath, JSON.stringify(dataToSave, null, 4));
|
|
153
363
|
}
|
|
154
364
|
async getBatchSaveData(modelName) {
|
|
155
|
-
const data = await this.
|
|
365
|
+
const data = await this.delegate.getBatchSaveData(modelName);
|
|
366
|
+
if (!this.recordedData.nango)
|
|
367
|
+
this.recordedData.nango = {};
|
|
368
|
+
if (!this.recordedData.nango.batchSave)
|
|
369
|
+
this.recordedData.nango.batchSave = {};
|
|
370
|
+
this.recordedData.nango.batchSave[modelName] = data;
|
|
371
|
+
await this.save();
|
|
156
372
|
return data;
|
|
157
373
|
}
|
|
158
374
|
async getBatchDeleteData(modelName) {
|
|
159
|
-
const data = await this.
|
|
375
|
+
const data = await this.delegate.getBatchDeleteData(modelName);
|
|
376
|
+
if (!this.recordedData.nango)
|
|
377
|
+
this.recordedData.nango = {};
|
|
378
|
+
if (!this.recordedData.nango.batchDelete)
|
|
379
|
+
this.recordedData.nango.batchDelete = {};
|
|
380
|
+
this.recordedData.nango.batchDelete[modelName] = data;
|
|
381
|
+
await this.save();
|
|
160
382
|
return data;
|
|
161
383
|
}
|
|
162
384
|
async getInput() {
|
|
163
|
-
const data = await this.
|
|
385
|
+
const data = await this.delegate.getInput();
|
|
386
|
+
this.recordedData.input = data;
|
|
387
|
+
await this.save();
|
|
164
388
|
return data;
|
|
165
389
|
}
|
|
166
390
|
async getOutput() {
|
|
167
|
-
const data = await this.
|
|
391
|
+
const data = await this.delegate.getOutput();
|
|
392
|
+
this.recordedData.output = data;
|
|
393
|
+
await this.save();
|
|
168
394
|
return data;
|
|
169
395
|
}
|
|
170
396
|
async getConnectionData() {
|
|
171
|
-
const data = await this.
|
|
397
|
+
const data = await this.delegate.getConnectionData();
|
|
398
|
+
if (!this.recordedData.nango)
|
|
399
|
+
this.recordedData.nango = {};
|
|
400
|
+
this.recordedData.nango.getConnection = data;
|
|
401
|
+
await this.save();
|
|
172
402
|
return data;
|
|
173
403
|
}
|
|
174
404
|
async getMetadataData() {
|
|
175
|
-
const data = await this.
|
|
405
|
+
const data = await this.delegate.getMetadataData();
|
|
406
|
+
if (!this.recordedData.nango)
|
|
407
|
+
this.recordedData.nango = {};
|
|
408
|
+
this.recordedData.nango.getMetadata = data;
|
|
409
|
+
await this.save();
|
|
410
|
+
return data;
|
|
411
|
+
}
|
|
412
|
+
async getUpdateMetadata() {
|
|
413
|
+
const data = await this.delegate.getUpdateMetadata();
|
|
414
|
+
if (!this.recordedData.nango)
|
|
415
|
+
this.recordedData.nango = {};
|
|
416
|
+
this.recordedData.nango.updateMetadata = data;
|
|
417
|
+
await this.save();
|
|
418
|
+
return data;
|
|
419
|
+
}
|
|
420
|
+
async getDeleteRecordsFromPreviousExecutions() {
|
|
421
|
+
const data = await this.delegate.getDeleteRecordsFromPreviousExecutions();
|
|
422
|
+
if (!this.recordedData.nango)
|
|
423
|
+
this.recordedData.nango = {};
|
|
424
|
+
this.recordedData.nango.deleteRecordsFromPreviousExecutions = data;
|
|
425
|
+
await this.save();
|
|
426
|
+
return data;
|
|
427
|
+
}
|
|
428
|
+
async getCachedResponse(identity) {
|
|
429
|
+
const data = await this.delegate.getCachedResponse(identity);
|
|
430
|
+
const method = identity.method.toUpperCase();
|
|
431
|
+
const endpoint = identity.endpoint;
|
|
432
|
+
if (!this.recordedData.api)
|
|
433
|
+
this.recordedData.api = {};
|
|
434
|
+
if (!this.recordedData.api[method])
|
|
435
|
+
this.recordedData.api[method] = {};
|
|
436
|
+
if (!this.recordedData.api[method][endpoint])
|
|
437
|
+
this.recordedData.api[method][endpoint] = [];
|
|
438
|
+
// Fetch ALL mocks for this endpoint from the legacy provider
|
|
439
|
+
// This captures pagination pages that weren't accessed due to the legacy pagination bug
|
|
440
|
+
const allMocksForEndpoint = await this.delegate.getAllMocksForEndpoint(identity.method, endpoint);
|
|
441
|
+
for (const legacyMock of allMocksForEndpoint) {
|
|
442
|
+
const mockParams = Object.fromEntries(legacyMock.requestIdentity.params);
|
|
443
|
+
const mockHeaders = Object.fromEntries(legacyMock.requestIdentity.headers);
|
|
444
|
+
const record = {
|
|
445
|
+
request: {
|
|
446
|
+
params: Object.keys(mockParams).length > 0 ? mockParams : undefined,
|
|
447
|
+
headers: Object.keys(mockHeaders).length > 0 ? mockHeaders : undefined,
|
|
448
|
+
data: legacyMock.requestIdentity.data
|
|
449
|
+
},
|
|
450
|
+
response: legacyMock.response,
|
|
451
|
+
hash: legacyMock.requestIdentityHash,
|
|
452
|
+
...(legacyMock.headers && { headers: legacyMock.headers }),
|
|
453
|
+
...(legacyMock.status !== undefined && { status: legacyMock.status })
|
|
454
|
+
};
|
|
455
|
+
const currentMocks = this.recordedData.api[method][endpoint];
|
|
456
|
+
const mocksArray = Array.isArray(currentMocks) ? currentMocks : [currentMocks];
|
|
457
|
+
// Use requestIdentityHash from the file content (not filename) for deduplication
|
|
458
|
+
const exists = mocksArray.some((r) => r.hash === record.hash);
|
|
459
|
+
if (!exists) {
|
|
460
|
+
this.recordedData.api[method][endpoint].push(record);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
await this.save();
|
|
176
464
|
return data;
|
|
177
465
|
}
|
|
466
|
+
isUnifiedMocks() {
|
|
467
|
+
return false;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
async function getFixtureProvider(dirname, name) {
|
|
471
|
+
let testFileName = name;
|
|
472
|
+
// Inspect the stack trace to automatically determine the name of the test file that is currently running.
|
|
473
|
+
// This allows it to locate the corresponding `.test.json` mock file (e.g., `my-test.test.ts` uses `my-test.test.json`).
|
|
474
|
+
// The `name` parameter serves as a fallback if the test file cannot be determined from the stack trace, or its missing
|
|
475
|
+
try {
|
|
476
|
+
const stack = new Error().stack;
|
|
477
|
+
if (stack) {
|
|
478
|
+
const lines = stack.split('\n');
|
|
479
|
+
for (const line of lines) {
|
|
480
|
+
if (line.includes(dirname) && line.includes('.test.ts')) {
|
|
481
|
+
const match = line.match(/\/([^/]+\.test\.ts)/);
|
|
482
|
+
if (match && match[1]) {
|
|
483
|
+
testFileName = match[1].replace('.test.ts', '');
|
|
484
|
+
break;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
catch (err) {
|
|
491
|
+
// eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors
|
|
492
|
+
return Promise.reject(err);
|
|
493
|
+
}
|
|
494
|
+
let unifiedMockPath = path.resolve(dirname, `${testFileName}.test.json`);
|
|
495
|
+
let unifiedFileExists = false;
|
|
496
|
+
try {
|
|
497
|
+
await fs.stat(unifiedMockPath);
|
|
498
|
+
unifiedFileExists = true;
|
|
499
|
+
}
|
|
500
|
+
catch (_) {
|
|
501
|
+
if (testFileName !== name) {
|
|
502
|
+
const fallbackPath = path.resolve(dirname, `${name}.test.json`);
|
|
503
|
+
try {
|
|
504
|
+
await fs.stat(fallbackPath);
|
|
505
|
+
unifiedMockPath = fallbackPath;
|
|
506
|
+
unifiedFileExists = true;
|
|
507
|
+
}
|
|
508
|
+
catch (_) {
|
|
509
|
+
unifiedFileExists = false;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
if (unifiedFileExists) {
|
|
514
|
+
const fileContent = await fs.readFile(unifiedMockPath, 'utf-8');
|
|
515
|
+
return new UnifiedFixtureProvider(JSON.parse(fileContent));
|
|
516
|
+
}
|
|
517
|
+
// This section supports the migration of tests from the legacy mock format (multiple files per test)
|
|
518
|
+
// to the new unified format (a single `.test.json` file). When the `MIGRATE_MOCKS` environment variable is set,
|
|
519
|
+
// it uses the `RecordingFixtureProvider` to intercept calls to the old mock loader, run the test, and then
|
|
520
|
+
// save all the mock data that was accessed into a single new `.test.json` file.
|
|
521
|
+
if (process.env?.['MIGRATE_MOCKS']) {
|
|
522
|
+
const legacyLoader = new LegacyFixtureProvider(dirname, name);
|
|
523
|
+
return new RecordingFixtureProvider(legacyLoader, unifiedMockPath);
|
|
524
|
+
}
|
|
525
|
+
return new LegacyFixtureProvider(dirname, name);
|
|
526
|
+
}
|
|
527
|
+
class NangoActionMock {
|
|
528
|
+
dirname;
|
|
529
|
+
name;
|
|
530
|
+
Model;
|
|
531
|
+
providerConfigKey;
|
|
532
|
+
fixtureProvider;
|
|
533
|
+
log;
|
|
534
|
+
ActionError = vi.fn();
|
|
535
|
+
getConnection;
|
|
536
|
+
getMetadata;
|
|
537
|
+
updateMetadata;
|
|
538
|
+
paginate;
|
|
539
|
+
get;
|
|
540
|
+
post;
|
|
541
|
+
patch;
|
|
542
|
+
put;
|
|
543
|
+
delete;
|
|
544
|
+
proxy;
|
|
545
|
+
getWebhookURL;
|
|
546
|
+
zodValidateInput;
|
|
547
|
+
deleteRecordsFromPreviousExecutions;
|
|
548
|
+
constructor({ dirname, name, Model }) {
|
|
549
|
+
this.dirname = dirname;
|
|
550
|
+
this.providerConfigKey = path.basename(path.dirname(dirname));
|
|
551
|
+
this.name = name;
|
|
552
|
+
this.Model = Model;
|
|
553
|
+
this.fixtureProvider = getFixtureProvider(dirname, name);
|
|
554
|
+
this.log = vi.fn();
|
|
555
|
+
this.getConnection = vi.fn(this.getConnectionData.bind(this));
|
|
556
|
+
this.getMetadata = vi.fn(this.getMetadataData.bind(this));
|
|
557
|
+
this.updateMetadata = vi.fn(this.getUpdateMetadata.bind(this));
|
|
558
|
+
this.deleteRecordsFromPreviousExecutions = vi.fn(this.getDeleteRecordsFromPreviousExecutions.bind(this));
|
|
559
|
+
this.paginate = vi.fn(this.getProxyPaginateData.bind(this));
|
|
560
|
+
this.get = vi.fn(this.proxyGetData.bind(this));
|
|
561
|
+
this.post = vi.fn(this.proxyPostData.bind(this));
|
|
562
|
+
this.patch = vi.fn(this.proxyPatchData.bind(this));
|
|
563
|
+
this.put = vi.fn(this.proxyPutData.bind(this));
|
|
564
|
+
this.delete = vi.fn(this.proxyDeleteData.bind(this));
|
|
565
|
+
this.proxy = vi.fn(this.proxyData.bind(this));
|
|
566
|
+
this.getWebhookURL = vi.fn(() => 'https://example.com/webhook');
|
|
567
|
+
this.zodValidateInput = vi.fn(this.mockZodValidateInput.bind(this));
|
|
568
|
+
}
|
|
569
|
+
mockZodValidateInput({ input }) {
|
|
570
|
+
return {
|
|
571
|
+
data: input
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
async getBatchSaveData(modelName) {
|
|
575
|
+
return (await this.fixtureProvider).getBatchSaveData(modelName);
|
|
576
|
+
}
|
|
577
|
+
async getBatchDeleteData(modelName) {
|
|
578
|
+
return (await this.fixtureProvider).getBatchDeleteData(modelName);
|
|
579
|
+
}
|
|
580
|
+
async getInput() {
|
|
581
|
+
return (await this.fixtureProvider).getInput();
|
|
582
|
+
}
|
|
583
|
+
async getOutput() {
|
|
584
|
+
return (await this.fixtureProvider).getOutput();
|
|
585
|
+
}
|
|
586
|
+
async getConnectionData() {
|
|
587
|
+
return (await this.fixtureProvider).getConnectionData();
|
|
588
|
+
}
|
|
589
|
+
async getMetadataData() {
|
|
590
|
+
return (await this.fixtureProvider).getMetadataData();
|
|
591
|
+
}
|
|
592
|
+
async getUpdateMetadata() {
|
|
593
|
+
return (await this.fixtureProvider).getUpdateMetadata();
|
|
594
|
+
}
|
|
595
|
+
async getDeleteRecordsFromPreviousExecutions() {
|
|
596
|
+
return (await this.fixtureProvider).getDeleteRecordsFromPreviousExecutions();
|
|
597
|
+
}
|
|
178
598
|
async *getProxyPaginateData(args) {
|
|
179
599
|
const providerConfig = getProvider(this.providerConfigKey);
|
|
180
600
|
if (!providerConfig) {
|
|
@@ -186,36 +606,213 @@ class NangoActionMock {
|
|
|
186
606
|
...args.paginate
|
|
187
607
|
};
|
|
188
608
|
const paginateInBody = ['post', 'put', 'patch'].includes(args.method.toLowerCase());
|
|
189
|
-
const updatedBodyOrParams = paginateInBody ? args.data || {} : args.params || {};
|
|
609
|
+
const updatedBodyOrParams = (paginateInBody ? args.data || {} : args.params || {});
|
|
190
610
|
if (args.paginate['limit']) {
|
|
191
611
|
const limitParameterName = args.paginate.limit_name_in_request;
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
612
|
+
updatedBodyOrParams[limitParameterName] = args.paginate['limit'];
|
|
613
|
+
}
|
|
614
|
+
const fixtureProvider = await this.fixtureProvider;
|
|
615
|
+
// For unified mocks, use Nango's pagination implementation to be consistent on how Nango would do it on an actual run
|
|
616
|
+
// For legacy mocks, use the legacy (bugged) pagination implementation to avoid breaking existing tests
|
|
617
|
+
if (fixtureProvider.isUnifiedMocks()) {
|
|
618
|
+
const paginationConfig = args.paginate;
|
|
619
|
+
paginateService.validateConfiguration(paginationConfig);
|
|
620
|
+
const proxyAdapter = async (config) => {
|
|
621
|
+
const response = await this.proxyData(config);
|
|
622
|
+
return {
|
|
623
|
+
data: response.data,
|
|
624
|
+
status: response.status || 200,
|
|
625
|
+
statusText: 'OK',
|
|
626
|
+
headers: response.headers || {},
|
|
627
|
+
config: {}
|
|
628
|
+
};
|
|
204
629
|
};
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
630
|
+
switch (paginationConfig.type) {
|
|
631
|
+
case 'cursor':
|
|
632
|
+
return yield* paginateService.cursor(args, paginationConfig, updatedBodyOrParams, paginateInBody, proxyAdapter);
|
|
633
|
+
case 'link':
|
|
634
|
+
return yield* paginateService.link(args, paginationConfig, updatedBodyOrParams, paginateInBody, proxyAdapter);
|
|
635
|
+
case 'offset':
|
|
636
|
+
return yield* paginateService.offset(args, paginationConfig, updatedBodyOrParams, paginateInBody, proxyAdapter);
|
|
637
|
+
default:
|
|
638
|
+
throw new Error(`Invalid pagination type: ${paginationConfig.type}`);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
// Legacy pagination implementation for tests using legacy mocks format
|
|
642
|
+
if (args.paginate?.type === 'cursor') {
|
|
643
|
+
yield* this.cursorPaginate(args, updatedBodyOrParams, paginateInBody);
|
|
208
644
|
}
|
|
209
|
-
else if (args.paginate
|
|
210
|
-
yield* this.
|
|
645
|
+
else if (args.paginate?.type === 'link') {
|
|
646
|
+
yield* this.linkPaginate(args, updatedBodyOrParams, paginateInBody);
|
|
211
647
|
}
|
|
212
|
-
else if (args.paginate
|
|
213
|
-
yield* this.
|
|
648
|
+
else if (args.paginate?.type === 'offset') {
|
|
649
|
+
yield* this.offsetPaginate(args, updatedBodyOrParams, paginateInBody);
|
|
214
650
|
}
|
|
215
651
|
else {
|
|
216
652
|
throw new Error(`Invalid pagination type: ${args.paginate?.type}`);
|
|
217
653
|
}
|
|
218
654
|
}
|
|
655
|
+
async *cursorPaginate(args, updatedBodyOrParams, paginateInBody) {
|
|
656
|
+
const cursorPagination = args.paginate;
|
|
657
|
+
let nextCursor;
|
|
658
|
+
do {
|
|
659
|
+
if (typeof nextCursor !== 'undefined') {
|
|
660
|
+
updatedBodyOrParams[cursorPagination.cursor_name_in_request] = nextCursor;
|
|
661
|
+
}
|
|
662
|
+
if (paginateInBody) {
|
|
663
|
+
args.data = updatedBodyOrParams;
|
|
664
|
+
}
|
|
665
|
+
else {
|
|
666
|
+
args.params = updatedBodyOrParams;
|
|
667
|
+
}
|
|
668
|
+
const response = await this.proxyData(args);
|
|
669
|
+
if (!response.headers) {
|
|
670
|
+
const data = response.data;
|
|
671
|
+
const paginate = args.paginate;
|
|
672
|
+
if (Array.isArray(data)) {
|
|
673
|
+
yield data;
|
|
674
|
+
}
|
|
675
|
+
if (paginate && paginate.response_path) {
|
|
676
|
+
yield data[paginate.response_path];
|
|
677
|
+
}
|
|
678
|
+
else {
|
|
679
|
+
const keys = Object.keys(data);
|
|
680
|
+
for (const key of keys) {
|
|
681
|
+
if (Array.isArray(data[key])) {
|
|
682
|
+
yield data[key];
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
const responseData = cursorPagination.response_path ? response.data[cursorPagination.response_path] : response.data;
|
|
689
|
+
if (!responseData || !responseData.length) {
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
yield responseData;
|
|
693
|
+
nextCursor = response.data[cursorPagination.cursor_path_in_response];
|
|
694
|
+
if (typeof nextCursor === 'string') {
|
|
695
|
+
nextCursor = nextCursor.trim();
|
|
696
|
+
if (!nextCursor) {
|
|
697
|
+
nextCursor = undefined;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
else if (typeof nextCursor !== 'number') {
|
|
701
|
+
nextCursor = undefined;
|
|
702
|
+
}
|
|
703
|
+
} while (typeof nextCursor !== 'undefined');
|
|
704
|
+
}
|
|
705
|
+
async *linkPaginate(args, updatedBodyOrParams, paginateInBody) {
|
|
706
|
+
const linkPagination = args.paginate;
|
|
707
|
+
if (paginateInBody) {
|
|
708
|
+
args.data = updatedBodyOrParams;
|
|
709
|
+
}
|
|
710
|
+
else {
|
|
711
|
+
args.params = updatedBodyOrParams;
|
|
712
|
+
}
|
|
713
|
+
while (true) {
|
|
714
|
+
const response = await this.proxyData(args);
|
|
715
|
+
if (!response.headers) {
|
|
716
|
+
const data = response.data;
|
|
717
|
+
const paginate = args.paginate;
|
|
718
|
+
if (Array.isArray(data)) {
|
|
719
|
+
yield data;
|
|
720
|
+
}
|
|
721
|
+
if (paginate && paginate.response_path) {
|
|
722
|
+
yield data[paginate.response_path];
|
|
723
|
+
}
|
|
724
|
+
else {
|
|
725
|
+
const keys = Object.keys(data);
|
|
726
|
+
for (const key of keys) {
|
|
727
|
+
if (Array.isArray(data[key])) {
|
|
728
|
+
yield data[key];
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
734
|
+
const data = response.data;
|
|
735
|
+
const responseData = linkPagination.response_path ? data[linkPagination.response_path] : data;
|
|
736
|
+
if (!responseData.length) {
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
yield responseData;
|
|
740
|
+
const nextPageLink = this.getNextPageLinkFromBodyOrHeaders(linkPagination, response, args.paginate);
|
|
741
|
+
if (!nextPageLink) {
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
if (!isValidHttpUrl(nextPageLink)) {
|
|
745
|
+
args.endpoint = nextPageLink;
|
|
746
|
+
}
|
|
747
|
+
else {
|
|
748
|
+
const url = new URL(nextPageLink);
|
|
749
|
+
args.endpoint = url.pathname + url.search;
|
|
750
|
+
}
|
|
751
|
+
args.params = {};
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
getNextPageLinkFromBodyOrHeaders(linkPagination, response, paginationConfig) {
|
|
755
|
+
if (linkPagination.link_rel_in_response_header) {
|
|
756
|
+
const linkHeader = parseLinksHeader(response.headers['link']);
|
|
757
|
+
return linkHeader?.[linkPagination.link_rel_in_response_header]?.url;
|
|
758
|
+
}
|
|
759
|
+
else if (linkPagination.link_path_in_response_body) {
|
|
760
|
+
return get(response.data, linkPagination.link_path_in_response_body);
|
|
761
|
+
}
|
|
762
|
+
throw Error(`Either 'link_rel_in_response_header' or 'link_path_in_response_body' should be specified for '${paginationConfig.type}' pagination`);
|
|
763
|
+
}
|
|
764
|
+
async *offsetPaginate(args, updatedBodyOrParams, paginateInBody) {
|
|
765
|
+
const offsetPagination = args.paginate;
|
|
766
|
+
const offsetParameterName = offsetPagination.offset_name_in_request;
|
|
767
|
+
const offsetCalculationMethod = offsetPagination.offset_calculation_method || 'by-response-size';
|
|
768
|
+
let offset = offsetPagination.offset_start_value || 0;
|
|
769
|
+
while (true) {
|
|
770
|
+
updatedBodyOrParams[offsetParameterName] = paginateInBody ? offset : String(offset);
|
|
771
|
+
if (paginateInBody) {
|
|
772
|
+
args.data = updatedBodyOrParams;
|
|
773
|
+
}
|
|
774
|
+
else {
|
|
775
|
+
args.params = updatedBodyOrParams;
|
|
776
|
+
}
|
|
777
|
+
const response = await this.proxyData(args);
|
|
778
|
+
if (!response.headers) {
|
|
779
|
+
const data = response.data;
|
|
780
|
+
const paginate = args.paginate;
|
|
781
|
+
if (Array.isArray(data)) {
|
|
782
|
+
yield data;
|
|
783
|
+
}
|
|
784
|
+
if (paginate && paginate.response_path) {
|
|
785
|
+
yield data[paginate.response_path];
|
|
786
|
+
}
|
|
787
|
+
else {
|
|
788
|
+
const keys = Object.keys(data);
|
|
789
|
+
for (const key of keys) {
|
|
790
|
+
if (Array.isArray(data[key])) {
|
|
791
|
+
yield data[key];
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
797
|
+
const responseData = args.paginate?.response_path ? get(response.data, args.paginate?.response_path) : response.data;
|
|
798
|
+
if (!responseData || !responseData.length) {
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
yield responseData;
|
|
802
|
+
if (args.paginate?.limit && responseData.length < args.paginate?.limit) {
|
|
803
|
+
return;
|
|
804
|
+
}
|
|
805
|
+
if (responseData.length < 1) {
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
if (offsetCalculationMethod === 'per-page') {
|
|
809
|
+
offset++;
|
|
810
|
+
}
|
|
811
|
+
else {
|
|
812
|
+
offset += responseData.length;
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
}
|
|
219
816
|
async proxyGetData(args) {
|
|
220
817
|
return this.proxyData({ ...args, method: 'get' });
|
|
221
818
|
}
|
|
@@ -233,39 +830,18 @@ class NangoActionMock {
|
|
|
233
830
|
}
|
|
234
831
|
async proxyData(args) {
|
|
235
832
|
const identity = computeConfigIdentity(args);
|
|
236
|
-
const cached = await this.getCachedResponse(identity);
|
|
237
|
-
return {
|
|
238
|
-
data: cached.response,
|
|
239
|
-
headers: cached.headers,
|
|
240
|
-
status: cached.status
|
|
241
|
-
};
|
|
833
|
+
const cached = await (await this.fixtureProvider).getCachedResponse(identity);
|
|
834
|
+
return { data: cached.response, headers: cached.headers, status: cached.status };
|
|
242
835
|
}
|
|
243
836
|
}
|
|
244
837
|
class NangoSyncMock extends NangoActionMock {
|
|
245
|
-
|
|
246
|
-
track_deletes = false;
|
|
838
|
+
lastSyncDate = null;
|
|
247
839
|
batchSave;
|
|
248
840
|
batchDelete;
|
|
249
|
-
batchUpdate;
|
|
250
|
-
getRecordsByIds;
|
|
251
|
-
deleteRecordsFromPreviousExecutions;
|
|
252
|
-
setMergingStrategy;
|
|
253
|
-
batchSend;
|
|
254
|
-
modelFullName(model) {
|
|
255
|
-
if (this.variant === 'base') {
|
|
256
|
-
return model;
|
|
257
|
-
}
|
|
258
|
-
return `${model}::${this.variant}`;
|
|
259
|
-
}
|
|
260
841
|
constructor({ dirname, name, Model }) {
|
|
261
842
|
super({ dirname, name, Model });
|
|
262
843
|
this.batchSave = vi.fn();
|
|
263
844
|
this.batchDelete = vi.fn();
|
|
264
|
-
this.batchUpdate = vi.fn();
|
|
265
|
-
this.getRecordsByIds = vi.fn();
|
|
266
|
-
this.deleteRecordsFromPreviousExecutions = vi.fn();
|
|
267
|
-
this.setMergingStrategy = vi.fn();
|
|
268
|
-
this.batchSend = vi.fn();
|
|
269
845
|
}
|
|
270
846
|
}
|
|
271
847
|
const FILTER_HEADERS = ['authorization', 'user-agent', 'nango-proxy-user-agent', 'accept-encoding', 'retries', 'host', 'connection-id', 'provider-config-key'];
|
|
@@ -295,13 +871,6 @@ function computeConfigIdentity(config) {
|
|
|
295
871
|
function sortEntries(entries) {
|
|
296
872
|
return entries.sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0));
|
|
297
873
|
}
|
|
298
|
-
function computeNormalizedHash(requestIdentity) {
|
|
299
|
-
const normalized = {
|
|
300
|
-
...requestIdentity,
|
|
301
|
-
params: requestIdentity.params.map(([k, v]) => [k, String(v)])
|
|
302
|
-
};
|
|
303
|
-
return crypto.createHash('sha1').update(JSON.stringify(normalized)).digest('hex');
|
|
304
|
-
}
|
|
305
874
|
function computeDataIdentity(config) {
|
|
306
875
|
const data = config.data;
|
|
307
876
|
if (!data) {
|
|
@@ -334,5 +903,19 @@ function computeDataIdentity(config) {
|
|
|
334
903
|
return dataString;
|
|
335
904
|
}
|
|
336
905
|
}
|
|
337
|
-
|
|
906
|
+
function isValidHttpUrl(str) {
|
|
907
|
+
try {
|
|
908
|
+
const url = new URL(str);
|
|
909
|
+
return url.protocol === 'http:' || url.protocol === 'https:';
|
|
910
|
+
}
|
|
911
|
+
catch (_) {
|
|
912
|
+
return false;
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
function get(obj, path, defaultValue = undefined) {
|
|
916
|
+
const pathArray = path.split('.').filter(Boolean);
|
|
917
|
+
const result = pathArray.reduce((acc, part) => acc && acc[part], obj);
|
|
918
|
+
return result === undefined ? defaultValue : result;
|
|
919
|
+
}
|
|
920
|
+
export { LegacyFixtureProvider, NangoActionMock, NangoSyncMock, RecordingFixtureProvider, UnifiedFixtureProvider };
|
|
338
921
|
//# sourceMappingURL=utils.js.map
|