piper-utils 1.1.69 → 1.1.71

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.
@@ -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
+ }