piper-utils 1.1.70 → 1.1.72
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/bin/main.js +73 -3
- package/bin/main.js.map +1 -1
- package/nul +82 -0
- package/package.json +1 -1
- package/src/contract/contract.js +42 -0
- package/src/contract/contract.test.js +36 -0
- package/src/database/dbUtils/partnerAccess/accessScope.test.js +287 -287
- package/src/database/dbUtils/queryStringUtils/getPagination.js +29 -0
- package/src/database/dbUtils/queryStringUtils/getPagination.test.js +83 -0
- package/src/eventManager/handleFile.js +129 -115
- package/src/eventManager/handleFile.test.js +463 -460
- package/src/index.js +8 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { getPagination } from './getPagination.js';
|
|
2
|
+
|
|
3
|
+
describe('getPagination', function () {
|
|
4
|
+
it('should default to limit 10 offset 0 when nothing is supplied', function () {
|
|
5
|
+
// Act
|
|
6
|
+
const result = getPagination({});
|
|
7
|
+
|
|
8
|
+
// Assert
|
|
9
|
+
expect(result).toEqual({ limit: 10, offset: 0 });
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('should tolerate a missing event', function () {
|
|
13
|
+
// Act
|
|
14
|
+
const result = getPagination(undefined);
|
|
15
|
+
|
|
16
|
+
// Assert
|
|
17
|
+
expect(result).toEqual({ limit: 10, offset: 0 });
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('should use the supplied limit and offset', function () {
|
|
21
|
+
// Arrange
|
|
22
|
+
const event = { queryStringParameters: { limit: '25', offset: '50' } };
|
|
23
|
+
|
|
24
|
+
// Act
|
|
25
|
+
const result = getPagination(event);
|
|
26
|
+
|
|
27
|
+
// Assert
|
|
28
|
+
expect(result).toEqual({ limit: 25, offset: 50 });
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('should fall back to the default when limit is not a number', function () {
|
|
32
|
+
// Arrange - parseInt('abc') is NaN, and findAll turns a falsy limit into
|
|
33
|
+
// Sequelize `limit: 1`, so this used to return exactly one row.
|
|
34
|
+
const event = { queryStringParameters: { limit: 'abc' } };
|
|
35
|
+
|
|
36
|
+
// Act
|
|
37
|
+
const result = getPagination(event);
|
|
38
|
+
|
|
39
|
+
// Assert
|
|
40
|
+
expect(result.limit).toBe(10);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should fall back to the default when limit is empty or zero', function () {
|
|
44
|
+
// Act + Assert
|
|
45
|
+
expect(getPagination({ queryStringParameters: { limit: '' } }).limit).toBe(10);
|
|
46
|
+
expect(getPagination({ queryStringParameters: { limit: '0' } }).limit).toBe(10);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should clamp an oversized limit rather than running it', function () {
|
|
50
|
+
// Arrange
|
|
51
|
+
const event = { queryStringParameters: { limit: '100000' } };
|
|
52
|
+
|
|
53
|
+
// Act
|
|
54
|
+
const result = getPagination(event);
|
|
55
|
+
|
|
56
|
+
// Assert
|
|
57
|
+
expect(result.limit).toBe(500);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should never return a negative offset', function () {
|
|
61
|
+
// Arrange
|
|
62
|
+
const event = { queryStringParameters: { offset: '-20' } };
|
|
63
|
+
|
|
64
|
+
// Act
|
|
65
|
+
const result = getPagination(event);
|
|
66
|
+
|
|
67
|
+
// Assert
|
|
68
|
+
expect(result.offset).toBe(0);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('should honor caller supplied defaultLimit and maxLimit', function () {
|
|
72
|
+
// Arrange
|
|
73
|
+
const event = { queryStringParameters: { limit: '999' } };
|
|
74
|
+
|
|
75
|
+
// Act
|
|
76
|
+
const clamped = getPagination(event, { maxLimit: 50 });
|
|
77
|
+
const defaulted = getPagination({}, { defaultLimit: 20 });
|
|
78
|
+
|
|
79
|
+
// Assert
|
|
80
|
+
expect(clamped.limit).toBe(50);
|
|
81
|
+
expect(defaulted.limit).toBe(20);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
@@ -1,115 +1,129 @@
|
|
|
1
|
-
import _ from 'lodash';
|
|
2
|
-
import S3Util from '../s3/S3Utils/S3Utils.js';
|
|
3
|
-
|
|
4
|
-
export async function handleFile(path, s3Bucket, transformer, options) {
|
|
5
|
-
const shouldSkipFailedFolders = _.get(options, 'shouldSkipFailedFolders');
|
|
6
|
-
const userImportTypes = _.get(options, 'userImportTypes');
|
|
7
|
-
const params = {
|
|
8
|
-
Bucket: s3Bucket,
|
|
9
|
-
Key: path
|
|
10
|
-
};
|
|
11
|
-
let body;
|
|
12
|
-
|
|
13
|
-
try {
|
|
14
|
-
body = await S3Util.getObject(params);
|
|
15
|
-
} catch (e) {
|
|
16
|
-
if (e.Code === 'NoSuchKey' || e.code === 'NoSuchKey' || e.name === 'NoSuchKey') {
|
|
17
|
-
return;
|
|
18
|
-
}
|
|
19
|
-
console.error('S3 getObject error:', e);
|
|
20
|
-
throw e;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
try {
|
|
24
|
-
// exit early for null/undefined or blank string bodies
|
|
25
|
-
if (_.isNil(body) || (_.isString(body) && _.isEmpty(body.trim()))) {
|
|
26
|
-
await S3Util.deleteObject(params);
|
|
27
|
-
return;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const parsedBody = JSON.parse(body);
|
|
31
|
-
|
|
32
|
-
// exit early for objects like {} or []
|
|
33
|
-
if (_.isEmpty(parsedBody)) {
|
|
34
|
-
await S3Util.deleteObject(params);
|
|
35
|
-
return;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const returnedValue = await transformer(parsedBody);
|
|
39
|
-
await S3Util.deleteObject(params);
|
|
40
|
-
|
|
41
|
-
return returnedValue;
|
|
42
|
-
} catch (error) {
|
|
43
|
-
console.error('HANDLE-FILE-ERROR', error);
|
|
44
|
-
|
|
45
|
-
if (!body) {
|
|
46
|
-
return;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
let newPath = '';
|
|
50
|
-
|
|
51
|
-
if (userImportTypes) {
|
|
52
|
-
console.log('USER IMPORT');
|
|
53
|
-
// look for items in the userImportTypes
|
|
54
|
-
const usersImports = Object.values(userImportTypes);
|
|
55
|
-
const items = path.split('/');
|
|
56
|
-
const fileName = _.last(items);
|
|
57
|
-
const folderName = _.first(items);
|
|
58
|
-
|
|
59
|
-
if (usersImports.indexOf(folderName) > -1) {
|
|
60
|
-
if (shouldSkipFailedFolders) {
|
|
61
|
-
newPath = `${folderName}/error/${fileName}`;
|
|
62
|
-
} else if (_.startsWith(path, `${folderName}/failed-once/`)) {
|
|
63
|
-
newPath = _.replace(path, `${folderName}/failed-once/`, `${folderName}/failed-twice/`);
|
|
64
|
-
} else if (_.startsWith(path, `${folderName}/failed-twice/`)) {
|
|
65
|
-
newPath = _.replace(path, `${folderName}/failed-twice/`, `${folderName}/error/`);
|
|
66
|
-
} else {
|
|
67
|
-
newPath = `${folderName}/failed-once/${fileName}`;
|
|
68
|
-
}
|
|
69
|
-
} else {
|
|
70
|
-
newPath = `pathAndEventMisMatchError/${path}`;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
} else {
|
|
74
|
-
if (shouldSkipFailedFolders) {
|
|
75
|
-
newPath = `error/${path}`;
|
|
76
|
-
} else if (_.startsWith(path, 'failed-once/')) {
|
|
77
|
-
newPath = _.replace(path, 'failed-once/', 'failed-twice/');
|
|
78
|
-
} else if (_.startsWith(path, 'failed-twice/')) {
|
|
79
|
-
newPath = _.replace(path, 'failed-twice/', 'error/');
|
|
80
|
-
} else {
|
|
81
|
-
if (_.includes(path, 'delayUntil/')) {
|
|
82
|
-
path = _.last(path.split('/'));
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
newPath = `failed-once/${path}`;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const newParams = {
|
|
90
|
-
Bucket: s3Bucket,
|
|
91
|
-
Key: newPath,
|
|
92
|
-
Body: body
|
|
93
|
-
};
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
)
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
1
|
+
import _ from 'lodash';
|
|
2
|
+
import S3Util from '../s3/S3Utils/S3Utils.js';
|
|
3
|
+
|
|
4
|
+
export async function handleFile(path, s3Bucket, transformer, options) {
|
|
5
|
+
const shouldSkipFailedFolders = _.get(options, 'shouldSkipFailedFolders');
|
|
6
|
+
const userImportTypes = _.get(options, 'userImportTypes');
|
|
7
|
+
const params = {
|
|
8
|
+
Bucket: s3Bucket,
|
|
9
|
+
Key: path
|
|
10
|
+
};
|
|
11
|
+
let body;
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
body = await S3Util.getObject(params);
|
|
15
|
+
} catch (e) {
|
|
16
|
+
if (e.Code === 'NoSuchKey' || e.code === 'NoSuchKey' || e.name === 'NoSuchKey') {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
console.error('S3 getObject error:', e);
|
|
20
|
+
throw e;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
// exit early for null/undefined or blank string bodies
|
|
25
|
+
if (_.isNil(body) || (_.isString(body) && _.isEmpty(body.trim()))) {
|
|
26
|
+
await S3Util.deleteObject(params);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const parsedBody = JSON.parse(body);
|
|
31
|
+
|
|
32
|
+
// exit early for objects like {} or []
|
|
33
|
+
if (_.isEmpty(parsedBody)) {
|
|
34
|
+
await S3Util.deleteObject(params);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const returnedValue = await transformer(parsedBody);
|
|
39
|
+
await S3Util.deleteObject(params);
|
|
40
|
+
|
|
41
|
+
return returnedValue;
|
|
42
|
+
} catch (error) {
|
|
43
|
+
console.error('HANDLE-FILE-ERROR', error);
|
|
44
|
+
|
|
45
|
+
if (!body) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
let newPath = '';
|
|
50
|
+
|
|
51
|
+
if (userImportTypes) {
|
|
52
|
+
console.log('USER IMPORT');
|
|
53
|
+
// look for items in the userImportTypes
|
|
54
|
+
const usersImports = Object.values(userImportTypes);
|
|
55
|
+
const items = path.split('/');
|
|
56
|
+
const fileName = _.last(items);
|
|
57
|
+
const folderName = _.first(items);
|
|
58
|
+
|
|
59
|
+
if (usersImports.indexOf(folderName) > -1) {
|
|
60
|
+
if (shouldSkipFailedFolders) {
|
|
61
|
+
newPath = `${folderName}/error/${fileName}`;
|
|
62
|
+
} else if (_.startsWith(path, `${folderName}/failed-once/`)) {
|
|
63
|
+
newPath = _.replace(path, `${folderName}/failed-once/`, `${folderName}/failed-twice/`);
|
|
64
|
+
} else if (_.startsWith(path, `${folderName}/failed-twice/`)) {
|
|
65
|
+
newPath = _.replace(path, `${folderName}/failed-twice/`, `${folderName}/error/`);
|
|
66
|
+
} else {
|
|
67
|
+
newPath = `${folderName}/failed-once/${fileName}`;
|
|
68
|
+
}
|
|
69
|
+
} else {
|
|
70
|
+
newPath = `pathAndEventMisMatchError/${path}`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
} else {
|
|
74
|
+
if (shouldSkipFailedFolders) {
|
|
75
|
+
newPath = `error/${path}`;
|
|
76
|
+
} else if (_.startsWith(path, 'failed-once/')) {
|
|
77
|
+
newPath = _.replace(path, 'failed-once/', 'failed-twice/');
|
|
78
|
+
} else if (_.startsWith(path, 'failed-twice/')) {
|
|
79
|
+
newPath = _.replace(path, 'failed-twice/', 'error/');
|
|
80
|
+
} else {
|
|
81
|
+
if (_.includes(path, 'delayUntil/')) {
|
|
82
|
+
path = _.last(path.split('/'));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
newPath = `failed-once/${path}`;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const newParams = {
|
|
90
|
+
Bucket: s3Bucket,
|
|
91
|
+
Key: newPath,
|
|
92
|
+
Body: body
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
// Guard against genuine prefix nesting (e.g. `failed-once/error/<file>`) — a file that has
|
|
96
|
+
// already traversed the failure ladder. Detect this on the FOLDER SEGMENTS only, never the
|
|
97
|
+
// filename, so a user-uploaded file named e.g. `errors.csv` or `failed-once-list.csv` is
|
|
98
|
+
// not mistaken for a nested path. The previous check counted the token anywhere in the key
|
|
99
|
+
// (including the filename) and threw BEFORE the move/delete below, so the file was neither
|
|
100
|
+
// dead-lettered nor removed — publishEvents.loop() then re-listed it every tick, an
|
|
101
|
+
// infinite reprocess loop. If the destination really is nested, still DELETE the source so
|
|
102
|
+
// it dead-letters instead of looping forever. See audit finding #8.
|
|
103
|
+
if (isNestedFailurePath(newPath)) {
|
|
104
|
+
await S3Util.deleteObject(params);
|
|
105
|
+
throw new Error('NESTING ERROR');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
await S3Util.putObject(newParams);
|
|
109
|
+
await S3Util.deleteObject(params);
|
|
110
|
+
|
|
111
|
+
throw error;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* True when a key has already traversed the failure ladder more than once, i.e. its FOLDER
|
|
117
|
+
* portion contains more than one of the ladder stages (failed-once / failed-twice / error).
|
|
118
|
+
* Only whole path SEGMENTS are considered, never the filename, so a file literally named
|
|
119
|
+
* `errors.csv` is not treated as nested.
|
|
120
|
+
*
|
|
121
|
+
* @param {string} key - The destination S3 key
|
|
122
|
+
* @returns {boolean}
|
|
123
|
+
*/
|
|
124
|
+
export function isNestedFailurePath(key) {
|
|
125
|
+
const ladderStages = [ 'failed-once', 'failed-twice', 'error' ];
|
|
126
|
+
const folderSegments = String(key).split('/').slice(0, -1);
|
|
127
|
+
const ladderCount = folderSegments.filter((segment) => ladderStages.includes(segment)).length;
|
|
128
|
+
return ladderCount > 1;
|
|
129
|
+
}
|