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.
@@ -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
- if (
96
- containsError(newPath, 'failed-once') ||
97
- containsError(newPath, 'failed-twice') ||
98
- containsError(newPath, 'error')
99
- ) {
100
- throw new Error('NESTING ERROR');
101
- }
102
-
103
- await S3Util.putObject(newParams);
104
- await S3Util.deleteObject(params);
105
-
106
- throw error;
107
- }
108
- }
109
-
110
- export function containsError(str, val) {
111
- const firstIndex = str.toLowerCase().indexOf(val);
112
- if (firstIndex === -1) return false;
113
- const secondIndex = str.toLowerCase().indexOf(val, firstIndex + 1);
114
- return secondIndex !== -1;
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
+ }