piper-utils 1.1.62 → 1.1.63
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/CLAUDE.md +102 -0
- package/README.md +846 -759
- package/bin/main.js +1 -1
- package/bin/main.js.map +1 -1
- package/package.json +1 -1
- package/src/requestResponse/requestResponse.js +1 -2
- package/src/requestResponse/requestResponse.test.js +1 -1
- package/WHITE_LABEL.md +0 -60
package/README.md
CHANGED
|
@@ -1,759 +1,846 @@
|
|
|
1
|
-
# Piper Utils
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
## Table of Contents
|
|
6
|
-
|
|
7
|
-
- [
|
|
8
|
-
- [
|
|
9
|
-
- [
|
|
10
|
-
- [
|
|
11
|
-
- [
|
|
12
|
-
- [
|
|
13
|
-
- [
|
|
14
|
-
- [
|
|
15
|
-
- [
|
|
16
|
-
- [
|
|
17
|
-
|
|
18
|
-
- [
|
|
19
|
-
- [
|
|
20
|
-
- [
|
|
21
|
-
- [
|
|
22
|
-
- [
|
|
23
|
-
- [
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
-
|
|
30
|
-
-
|
|
31
|
-
-
|
|
32
|
-
-
|
|
33
|
-
-
|
|
34
|
-
-
|
|
35
|
-
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
//
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
```
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
}
|
|
332
|
-
```
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
```
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
##
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
1
|
+
# Piper Utils
|
|
2
|
+
|
|
3
|
+
Utility library for building AWS Lambda microservices with Cognito authentication, Sequelize ORM queries, S3 file processing pipelines, and standardized API Gateway responses.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
- [Installation](#installation)
|
|
8
|
+
- [Exports](#exports)
|
|
9
|
+
- [Request / Response](#request--response)
|
|
10
|
+
- [success](#successbody-options)
|
|
11
|
+
- [successHtml](#successhtmlhtml-options)
|
|
12
|
+
- [failure](#failurebody-options)
|
|
13
|
+
- [parseBody](#parsebodyevent)
|
|
14
|
+
- [parseEvent](#parseeventevent-callback)
|
|
15
|
+
- [getCurrentUser](#getcurrentuserevent)
|
|
16
|
+
- [getCurrentUserNameFromCognitoEvent](#getcurrentusernameFromcognitoeventevent)
|
|
17
|
+
- [Authentication & Access Control](#authentication--access-control)
|
|
18
|
+
- [accessRightsUtils](#accessrightsutiilsevent-options)
|
|
19
|
+
- [checkWriteAccess](#checkwriteaccessevent-options)
|
|
20
|
+
- [checkModule](#checkmodulemodulename-event)
|
|
21
|
+
- [checkIsSuper](#checkissuperevent)
|
|
22
|
+
- [isSuperUser / isSystemUser](#issuperuserevent--issystemuserevent)
|
|
23
|
+
- [isPartnerUser / getBelongsToPartnerId / getEffectivePartnerId](#ispartneruserevent)
|
|
24
|
+
- [enrichEventWithPartnerAccess](#enricheventwithpartneraccessevent-partnerbusinessids-role)
|
|
25
|
+
- [userDefaultBid](#userdefaultbidevent)
|
|
26
|
+
- [getBusinessesInfo](#getbusinessesinfoevent-usecognitobid)
|
|
27
|
+
- [getAccessRightsInfo / getDefaultBusinessIDInfo / getModuleInfo](#low-level-claim-helpers)
|
|
28
|
+
- [getCompanySettings](#getcompanysettingsevent)
|
|
29
|
+
- [JWT Claims Reference](#jwt-claims-reference)
|
|
30
|
+
- [Database Query Helpers](#database-query-helpers)
|
|
31
|
+
- [defaultFilters](#defaultfiltersschema-subschemas)
|
|
32
|
+
- [createFilters](#createfiltersevent-objectfilters)
|
|
33
|
+
- [createSort](#createsortevent-defaultfilter)
|
|
34
|
+
- [createIncludes](#createincludesevent-objectfilters)
|
|
35
|
+
- [findAll](#findallmodel-options)
|
|
36
|
+
- [Query String DSL](#query-string-dsl)
|
|
37
|
+
- [Event Manager (S3 Pipeline)](#event-manager-s3-pipeline)
|
|
38
|
+
- [watchBucket](#watchbucketparams)
|
|
39
|
+
- [handleFile](#handlefilepath-s3bucket-transformer-options)
|
|
40
|
+
- [handleEvents](#handleeventsevent-transformer-errorhandlerperfile-shouldskipfailedfolders-userimporttypes)
|
|
41
|
+
- [publishEvents](#publisheventsconfigtable-tablekey-bucket-snstopic-userimporttypes)
|
|
42
|
+
- [Retry / Error Folder Strategy](#retry--error-folder-strategy)
|
|
43
|
+
- [Database Migrations](#database-migrations)
|
|
44
|
+
- [Built-in Error Codes](#built-in-error-codes)
|
|
45
|
+
- [Examples](#examples)
|
|
46
|
+
- [Peer Dependencies](#peer-dependencies)
|
|
47
|
+
- [Testing](#testing)
|
|
48
|
+
|
|
49
|
+
## Installation
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
npm install piper-utils
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Exports
|
|
56
|
+
|
|
57
|
+
Every public function is re-exported from the package root:
|
|
58
|
+
|
|
59
|
+
```js
|
|
60
|
+
import {
|
|
61
|
+
// Request / Response
|
|
62
|
+
success, successHtml, failure, parseBody, parseEvent,
|
|
63
|
+
getCurrentUser, getCurrentUserNameFromCognitoEvent,
|
|
64
|
+
|
|
65
|
+
// Authentication & Access Control
|
|
66
|
+
accessRightsUtils, checkWriteAccess, checkModule, checkIsSuper,
|
|
67
|
+
isSystemUser, isSuperUser,
|
|
68
|
+
isPartnerUser, getBelongsToPartnerId, getEffectivePartnerId,
|
|
69
|
+
enrichEventWithPartnerAccess,
|
|
70
|
+
userDefaultBid, getBusinessesInfo,
|
|
71
|
+
getAccessRightsInfo, getDefaultBusinessIDInfo, getModuleInfo,
|
|
72
|
+
getCompanySettings,
|
|
73
|
+
|
|
74
|
+
// Database Query Helpers (Sequelize)
|
|
75
|
+
defaultFilters, createFilters, createSort, createIncludes, findAll,
|
|
76
|
+
|
|
77
|
+
// Event Manager (S3 Pipeline)
|
|
78
|
+
watchBucket, handleFile, publishEvents,
|
|
79
|
+
|
|
80
|
+
// Database Migrations
|
|
81
|
+
runMigrations
|
|
82
|
+
} from 'piper-utils';
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
> **Note:** S3Utils, SNSUtils, and dynamoUtil are internal modules used by the event manager. They are not exported from the package root.
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## Request / Response
|
|
90
|
+
|
|
91
|
+
Functions for formatting API Gateway Lambda proxy responses with CORS and security headers.
|
|
92
|
+
|
|
93
|
+
All JSON responses include these security headers:
|
|
94
|
+
- `Strict-Transport-Security` (HSTS)
|
|
95
|
+
- `X-Content-Type-Options: nosniff`
|
|
96
|
+
- `X-Frame-Options: DENY`
|
|
97
|
+
- `Content-Security-Policy: default-src 'none'; frame-ancestors 'none'`
|
|
98
|
+
- `Referrer-Policy: strict-origin-when-cross-origin`
|
|
99
|
+
- `Permissions-Policy` (camera, microphone, geolocation disabled)
|
|
100
|
+
- `Cache-Control: no-store`
|
|
101
|
+
|
|
102
|
+
### `success(body, options?)`
|
|
103
|
+
|
|
104
|
+
Format a 200 JSON response.
|
|
105
|
+
|
|
106
|
+
```js
|
|
107
|
+
return success({ id: 1, name: 'Widget' });
|
|
108
|
+
// => { statusCode: 200, headers: {...}, body: '{"id":1,"name":"Widget"}' }
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
| Param | Type | Description |
|
|
112
|
+
|-------|------|-------------|
|
|
113
|
+
| `body` | any | Response data (will be JSON.stringify'd) |
|
|
114
|
+
| `options.dbClose` | function | Optional callback invoked before returning (e.g. close DB connection) |
|
|
115
|
+
|
|
116
|
+
### `successHtml(html, options?)`
|
|
117
|
+
|
|
118
|
+
Format a 200 HTML response with a relaxed CSP that allows payment provider scripts (TokenEx, NMI, Apple Pay, Sentry).
|
|
119
|
+
|
|
120
|
+
```js
|
|
121
|
+
return successHtml('<html>...</html>');
|
|
122
|
+
// => { statusCode: 200, headers: { 'Content-Type': 'text/html', ... }, body: '<html>...</html>' }
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
| Param | Type | Description |
|
|
126
|
+
|-------|------|-------------|
|
|
127
|
+
| `html` | string | Raw HTML content |
|
|
128
|
+
| `options.dbClose` | function | Optional callback invoked before returning |
|
|
129
|
+
|
|
130
|
+
### `failure(body?, options?)`
|
|
131
|
+
|
|
132
|
+
Format an error response. Automatically detects and normalizes Joi, Sequelize, and Dynamoose errors.
|
|
133
|
+
|
|
134
|
+
```js
|
|
135
|
+
// Throw a known error
|
|
136
|
+
throw { statusCode: 404, errorCode: '4004', message: 'ITEM NOT FOUND' };
|
|
137
|
+
|
|
138
|
+
// failure() catches and formats it
|
|
139
|
+
return failure(err);
|
|
140
|
+
// => { statusCode: 404, headers: {...}, body: '{"statusCode":404,"errorCode":"4004","message":"ITEM NOT FOUND"}' }
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
**Auto-detection logic:**
|
|
144
|
+
|
|
145
|
+
| Error type | Detection | Resulting statusCode / errorCode |
|
|
146
|
+
|------------|-----------|----------------------------------|
|
|
147
|
+
| Joi validation | `body.details` exists | 400 / `4000` |
|
|
148
|
+
| Sequelize ForeignKeyConstraint | `body.name === 'SequelizeForeignKeyConstraintError'` | 409 / `4090` |
|
|
149
|
+
| Sequelize UniqueConstraint | `body.name === 'SequelizeUniqueConstraintError'` | 409 / `4091` |
|
|
150
|
+
| Sequelize ValidationError | `body.name === 'SequelizeValidationError'` | 400 / `4001` |
|
|
151
|
+
| Unknown | fallback | 500 / `5XX` |
|
|
152
|
+
|
|
153
|
+
| Param | Type | Description |
|
|
154
|
+
|-------|------|-------------|
|
|
155
|
+
| `body` | object/Error | Error object. If it has `statusCode` and `errorCode`, used as-is. |
|
|
156
|
+
| `options.dbClose` | function | Optional callback invoked before returning |
|
|
157
|
+
|
|
158
|
+
### `parseBody(event)`
|
|
159
|
+
|
|
160
|
+
Parse the JSON body from an API Gateway Lambda proxy event. Throws `errorList.invalidJson` (400) on malformed JSON.
|
|
161
|
+
|
|
162
|
+
```js
|
|
163
|
+
const body = parseBody(event);
|
|
164
|
+
// body is the parsed JSON object, or {} if event.body is falsy
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Returns the `event.body` as-is if it's already an object (e.g. from serverless-offline).
|
|
168
|
+
|
|
169
|
+
### `parseEvent(event, callback?)`
|
|
170
|
+
|
|
171
|
+
Like `parseBody` but for parsing an entire event string. Optionally calls `callback(error)` on failure instead of throwing.
|
|
172
|
+
|
|
173
|
+
```js
|
|
174
|
+
const parsed = parseEvent(eventString);
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### `getCurrentUser(event)`
|
|
178
|
+
|
|
179
|
+
Extract user info from Cognito JWT authorizer claims.
|
|
180
|
+
|
|
181
|
+
```js
|
|
182
|
+
const user = getCurrentUser(event);
|
|
183
|
+
// => { username: 'john@example.com', id: 42 }
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
| Return field | Source claim | Default |
|
|
187
|
+
|-------------|-------------|---------|
|
|
188
|
+
| `id` | `custom:UID` (JSON-parsed) | `0` |
|
|
189
|
+
| `username` | `email` | `'localtestuser@gexample.com'` |
|
|
190
|
+
|
|
191
|
+
### `getCurrentUserNameFromCognitoEvent(event)`
|
|
192
|
+
|
|
193
|
+
Extract email from a **Cognito User Pool trigger event** (different structure than API Gateway authorizer events).
|
|
194
|
+
|
|
195
|
+
```js
|
|
196
|
+
// Inside a Cognito Pre-Sign-Up or Post-Confirmation trigger
|
|
197
|
+
const email = getCurrentUserNameFromCognitoEvent(event);
|
|
198
|
+
// Reads from event.request.userAttributes['cognito:email_alias'] or ['email']
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
## Authentication & Access Control
|
|
204
|
+
|
|
205
|
+
All auth functions read JWT claims from `event.requestContext.authorizer.claims` (or `event.requestContext.authorizer` for custom authorizers). See [JWT Claims Reference](#jwt-claims-reference) for the full claim structure.
|
|
206
|
+
|
|
207
|
+
**Local bypass:** When `BUILD_ENV=local`, most auth checks are relaxed to ease development.
|
|
208
|
+
|
|
209
|
+
### `accessRightsUtils(event, options?)`
|
|
210
|
+
|
|
211
|
+
Get the list of business IDs the current user is authorized to access. Compares requested IDs (from query string or body) against allowed IDs (from JWT claims).
|
|
212
|
+
|
|
213
|
+
```js
|
|
214
|
+
const businessIds = accessRightsUtils(event, { useCognitoBid: true });
|
|
215
|
+
// => ['1', '5', '12']
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
| Param | Type | Description |
|
|
219
|
+
|-------|------|-------------|
|
|
220
|
+
| `event` | object | Lambda event |
|
|
221
|
+
| `options.useCognitoBid` | boolean | If true, prefer Cognito businessId; skip local fallback of BID '1' |
|
|
222
|
+
|
|
223
|
+
**Returns:** `string[]` - business IDs user can access.
|
|
224
|
+
|
|
225
|
+
**Behavior by user type:**
|
|
226
|
+
- **Super user:** Gets all requested IDs; if none requested, gets all allowed IDs
|
|
227
|
+
- **System user:** Gets all requested IDs (no filtering)
|
|
228
|
+
- **Regular user:** Gets the intersection of requested and allowed IDs
|
|
229
|
+
|
|
230
|
+
### `checkWriteAccess(event, options?)`
|
|
231
|
+
|
|
232
|
+
Verify the user has write (`W`) or admin (`A`) role for the businessId in the request body. Throws `errorList.unauthorized` (401) if denied.
|
|
233
|
+
|
|
234
|
+
```js
|
|
235
|
+
const businessId = checkWriteAccess(event);
|
|
236
|
+
// Returns the authorized businessId string
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
**Bypassed for:** super users, system users, `BUILD_ENV=local`.
|
|
240
|
+
|
|
241
|
+
### `checkModule(moduleName, event)`
|
|
242
|
+
|
|
243
|
+
Verify the user has access to a named module. Throws `errorList.unauthorized` (401) if denied.
|
|
244
|
+
|
|
245
|
+
```js
|
|
246
|
+
checkModule('customer', event); // throws if no access
|
|
247
|
+
checkModule('inventory', event); // throws if no access
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
Reads module permissions from `custom:MOD` (falls back to `custom:AR`). **Bypassed for:** super users, system users, `BUILD_ENV=local`.
|
|
251
|
+
|
|
252
|
+
### `checkIsSuper(event)`
|
|
253
|
+
|
|
254
|
+
Throws `errorList.unauthorized` (401) if the user is not a super or system user. **Bypassed for** `BUILD_ENV=local`.
|
|
255
|
+
|
|
256
|
+
```js
|
|
257
|
+
checkIsSuper(event); // throws if not super/system
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
### `isSuperUser(event)` / `isSystemUser(event)`
|
|
261
|
+
|
|
262
|
+
Boolean checks. No throwing, no bypass.
|
|
263
|
+
|
|
264
|
+
```js
|
|
265
|
+
if (isSuperUser(event)) { /* full access */ }
|
|
266
|
+
if (isSystemUser(event)) { /* machine-to-machine */ }
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
### `isPartnerUser(event)`
|
|
270
|
+
|
|
271
|
+
Returns the partner ID from `custom:PID`, or `false`.
|
|
272
|
+
|
|
273
|
+
### `getBelongsToPartnerId(event)`
|
|
274
|
+
|
|
275
|
+
Returns the partner ID from `custom:BPID` (user's business belongs to a partner), or `false`.
|
|
276
|
+
|
|
277
|
+
### `getEffectivePartnerId(event)`
|
|
278
|
+
|
|
279
|
+
Returns `isPartnerUser(event) || getBelongsToPartnerId(event)` - the partner ID from either claim, or `false`.
|
|
280
|
+
|
|
281
|
+
### `enrichEventWithPartnerAccess(event, partnerBusinessIds, role?)`
|
|
282
|
+
|
|
283
|
+
Mutate the event's `custom:AR` claim in-memory to add partner business IDs. Call this **before** `accessRightsUtils()` or `getBusinessesInfo()` so partner admins get access to their partner's merchants.
|
|
284
|
+
|
|
285
|
+
```js
|
|
286
|
+
// After looking up partner's business IDs from your DB:
|
|
287
|
+
enrichEventWithPartnerAccess(event, ['101', '102', '103'], 'R');
|
|
288
|
+
// Now accessRightsUtils(event) will include 101, 102, 103
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
| Param | Type | Default | Description |
|
|
292
|
+
|-------|------|---------|-------------|
|
|
293
|
+
| `event` | object | | Lambda event (mutated in-place) |
|
|
294
|
+
| `partnerBusinessIds` | string[] | | Business IDs to add |
|
|
295
|
+
| `role` | string | `'R'` | Access role: `'R'` (read), `'W'` (write), `'A'` (admin) |
|
|
296
|
+
|
|
297
|
+
### `userDefaultBid(event)`
|
|
298
|
+
|
|
299
|
+
Get the user's default business ID from `custom:DBI`.
|
|
300
|
+
|
|
301
|
+
```js
|
|
302
|
+
const defaultBid = userDefaultBid(event);
|
|
303
|
+
// => '5' (or '1' as fallback)
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
### `getBusinessesInfo(event, useCognitoBid?)`
|
|
307
|
+
|
|
308
|
+
Get the raw business ID to role mapping from `custom:AR`.
|
|
309
|
+
|
|
310
|
+
```js
|
|
311
|
+
const businesses = getBusinessesInfo(event);
|
|
312
|
+
// => { '1': 'A', '5': 'W', '12': 'R' }
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
When `BUILD_ENV=local`, automatically injects `{ '1': 'A' }` and any businessId from the request body.
|
|
316
|
+
|
|
317
|
+
### Low-level Claim Helpers
|
|
318
|
+
|
|
319
|
+
These extract raw claim values without business logic:
|
|
320
|
+
|
|
321
|
+
- **`getAccessRightsInfo(event)`** - Returns `custom:AR.businessIds` as an object (e.g. `{ '1': 'A', '5': 'R' }`)
|
|
322
|
+
- **`getDefaultBusinessIDInfo(event)`** - Returns `custom:DBI.defaultBid` as a string (default `'1'`)
|
|
323
|
+
- **`getModuleInfo(event)`** - Returns `custom:MOD.module` (or `custom:AR.module`) as an object
|
|
324
|
+
|
|
325
|
+
### `getCompanySettings(event)`
|
|
326
|
+
|
|
327
|
+
Get company-level cached settings from `custom:SET`.
|
|
328
|
+
|
|
329
|
+
```js
|
|
330
|
+
const settings = getCompanySettings(event);
|
|
331
|
+
// => { auditEnabled: true, ... }
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
Returns `{}` on parse error.
|
|
335
|
+
|
|
336
|
+
---
|
|
337
|
+
|
|
338
|
+
## JWT Claims Reference
|
|
339
|
+
|
|
340
|
+
The library expects these custom Cognito attributes on `event.requestContext.authorizer.claims` (or `event.requestContext.authorizer` for custom authorizers):
|
|
341
|
+
|
|
342
|
+
| Claim | Type | Example | Used by |
|
|
343
|
+
|-------|------|---------|---------|
|
|
344
|
+
| `custom:UID` | JSON number | `"42"` | `getCurrentUser` |
|
|
345
|
+
| `email` | string | `"john@example.com"` | `getCurrentUser` |
|
|
346
|
+
| `custom:SYSTEM` | JSON boolean | `"true"` | `isSystemUser` |
|
|
347
|
+
| `custom:SUPER` | JSON boolean | `"true"` | `isSuperUser` |
|
|
348
|
+
| `custom:AR` | JSON object | `{"businessIds":{"1":"A","5":"R"}}` | `accessRightsUtils`, `checkWriteAccess`, `getBusinessesInfo` |
|
|
349
|
+
| `custom:DBI` | JSON object | `{"defaultBid":"5"}` | `userDefaultBid` |
|
|
350
|
+
| `custom:MOD` | JSON object | `{"module":{"customer":true}}` | `checkModule` |
|
|
351
|
+
| `custom:PID` | string | `"PARTNER_123"` | `isPartnerUser` |
|
|
352
|
+
| `custom:BPID` | string | `"PARTNER_123"` | `getBelongsToPartnerId` |
|
|
353
|
+
| `custom:SET` | JSON object | `{"auditEnabled":true}` | `getCompanySettings` |
|
|
354
|
+
|
|
355
|
+
**Role values** in `custom:AR.businessIds`: `A` (admin), `W` (write), `R` (read).
|
|
356
|
+
|
|
357
|
+
---
|
|
358
|
+
|
|
359
|
+
## Database Query Helpers
|
|
360
|
+
|
|
361
|
+
Utilities for translating API query string parameters into Sequelize queries. These handle filtering, sorting, pagination, and relation includes automatically.
|
|
362
|
+
|
|
363
|
+
### `defaultFilters(schema, subSchemas?)`
|
|
364
|
+
|
|
365
|
+
Create a filter configuration object from a Sequelize model schema. This maps each column's data type to the appropriate Sequelize operator.
|
|
366
|
+
|
|
367
|
+
```js
|
|
368
|
+
import DB from 'sequelize';
|
|
369
|
+
|
|
370
|
+
const orderSchema = {
|
|
371
|
+
orderNumber: { type: new DB.STRING() },
|
|
372
|
+
status: { type: new DB.STRING() },
|
|
373
|
+
total: { type: new DB.DECIMAL() },
|
|
374
|
+
active: { type: new DB.BOOLEAN() },
|
|
375
|
+
metadata: { type: DB.JSONB },
|
|
376
|
+
createdAt: { type: new DB.DATE() }
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
const orderFilter = defaultFilters(orderSchema, {
|
|
380
|
+
customer: customerSchema // enable customer.* filtering
|
|
381
|
+
});
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
**Type-to-operator mapping:**
|
|
385
|
+
|
|
386
|
+
| Sequelize type | Operator | Behavior |
|
|
387
|
+
|---------------|----------|----------|
|
|
388
|
+
| STRING, CHAR, TEXT | `Op.iLike` | Case-insensitive `LOWER(col) LIKE '%value%'` |
|
|
389
|
+
| INTEGER, DECIMAL, BIGINT | `Op.or` | Matches `+value` or `-value` |
|
|
390
|
+
| BOOLEAN | `Op.eq` | Exact match with string-to-boolean coercion |
|
|
391
|
+
| JSONB | (special) | Case-insensitive search via `jsonb_extract_path_text` |
|
|
392
|
+
| DATE | `Op.between` | Range filter |
|
|
393
|
+
| DATEONLY | `Op.eq` | Exact match |
|
|
394
|
+
|
|
395
|
+
**Auto-added fields:** `id`, `createdAt`, `updatedAt` are always included.
|
|
396
|
+
|
|
397
|
+
### `createFilters(event, objectFilters)`
|
|
398
|
+
|
|
399
|
+
Convert query string parameters into a Sequelize WHERE clause. Automatically applies:
|
|
400
|
+
- **Business ID scoping** via `accessRightsUtils(event)`
|
|
401
|
+
- **Active record filtering** (defaults `active: true` if the schema has an `active` column)
|
|
402
|
+
- **Date range filtering** when both `startDate` and `endDate` are provided
|
|
403
|
+
- **Full-text search** when `searchString` is provided
|
|
404
|
+
|
|
405
|
+
```js
|
|
406
|
+
// URL: /orders?status=Shipped&searchString=john&startDate=2024-01-01&endDate=2024-12-31&sort=-createdAt
|
|
407
|
+
const where = createFilters(event, orderFilter);
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
See [Query String DSL](#query-string-dsl) for all supported operators.
|
|
411
|
+
|
|
412
|
+
### `createSort(event, defaultFilter)`
|
|
413
|
+
|
|
414
|
+
Convert the `sort` query parameter into a Sequelize ORDER BY array.
|
|
415
|
+
|
|
416
|
+
```js
|
|
417
|
+
// URL: /orders?sort=-createdAt,status
|
|
418
|
+
const order = createSort(event, orderFilter);
|
|
419
|
+
// => [['createdAt', 'DESC'], ['status', 'ASC']]
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
- Prefix with `-` for DESC, no prefix for ASC
|
|
423
|
+
- Comma-separated for multiple fields
|
|
424
|
+
- **Default:** `-updatedAt` (DESC)
|
|
425
|
+
- Only allows sorting on fields defined in `defaultFilter` (prevents injection)
|
|
426
|
+
|
|
427
|
+
### `createIncludes(event, objectFilters)`
|
|
428
|
+
|
|
429
|
+
Build a Sequelize includes array by detecting which relations are referenced in filter or sort parameters (via dot-notation).
|
|
430
|
+
|
|
431
|
+
```js
|
|
432
|
+
// URL: /orders?customer.email=john@test.com&sort=-customer.name
|
|
433
|
+
const includes = createIncludes(event, orderFilter);
|
|
434
|
+
// => ['customer']
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
### `findAll(model, options)`
|
|
438
|
+
|
|
439
|
+
Paginated wrapper around `Model.findAll()`. Fetches `limit + 1` records to detect `hasMore` without a COUNT query.
|
|
440
|
+
|
|
441
|
+
```js
|
|
442
|
+
const result = await findAll(Order, {
|
|
443
|
+
where,
|
|
444
|
+
order,
|
|
445
|
+
limit: 20,
|
|
446
|
+
offset: 0,
|
|
447
|
+
includes: [{ model: Customer, as: 'customer' }]
|
|
448
|
+
});
|
|
449
|
+
// => { offset: 0, limit: 20, rows: [...], hasMore: true }
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
| Param | Type | Default | Description |
|
|
453
|
+
|-------|------|---------|-------------|
|
|
454
|
+
| `model` | Sequelize.Model | | Sequelize model class |
|
|
455
|
+
| `options.where` | object | | WHERE clause (from `createFilters`) |
|
|
456
|
+
| `options.order` | array | | ORDER BY (from `createSort`) |
|
|
457
|
+
| `options.includes` | array | `{ all: true, nested: true }` | Relations to include |
|
|
458
|
+
| `options.limit` | number | `0` (no limit) | Page size |
|
|
459
|
+
| `options.offset` | number | `0` | Page offset |
|
|
460
|
+
|
|
461
|
+
**Returns:** `{ offset, limit, rows, hasMore }`
|
|
462
|
+
|
|
463
|
+
---
|
|
464
|
+
|
|
465
|
+
## Query String DSL
|
|
466
|
+
|
|
467
|
+
Supported query string parameters for `createFilters` and `createSort`:
|
|
468
|
+
|
|
469
|
+
| Query | Behavior |
|
|
470
|
+
|-------|----------|
|
|
471
|
+
| `?field=value` | Match by type: iLike for strings, eq for booleans, or for numbers |
|
|
472
|
+
| `?searchString=john` | OR search across all filterable string/numeric fields |
|
|
473
|
+
| `?startDate=2024-01-01&endDate=2024-12-31` | BETWEEN on `createdAt` |
|
|
474
|
+
| `?sort=-field` | Sort DESC (prefix `-`) |
|
|
475
|
+
| `?sort=field1,-field2` | Multi-field sort, comma-separated |
|
|
476
|
+
| `?token.cardHolderName=gregory` | JSONB dot-notation search (case-insensitive) |
|
|
477
|
+
| `?token={"cardType":"visa"}` | JSONB object search (all key-value pairs, case-insensitive) |
|
|
478
|
+
| `?customer.email=john` | Relation field filter (auto-includes the relation) |
|
|
479
|
+
|
|
480
|
+
---
|
|
481
|
+
|
|
482
|
+
## Event Manager (S3 Pipeline)
|
|
483
|
+
|
|
484
|
+
A set of utilities for building S3 file processing pipelines. The typical flow is:
|
|
485
|
+
|
|
486
|
+
1. **Cron job** triggers `watchBucket` -> calls `publishEvents` to scan S3 and publish file batches to SNS
|
|
487
|
+
2. **SNS trigger** invokes Lambda -> `watchBucket` routes to `handleEvents` which processes files in parallel
|
|
488
|
+
3. **Direct S3 trigger** invokes Lambda -> `watchBucket` routes to `handleDirectS3WriteEvent`
|
|
489
|
+
|
|
490
|
+
### `watchBucket(params)`
|
|
491
|
+
|
|
492
|
+
Entry point for the S3 pipeline. Routes incoming events to the appropriate handler based on event source.
|
|
493
|
+
|
|
494
|
+
```js
|
|
495
|
+
import { watchBucket } from 'piper-utils';
|
|
496
|
+
|
|
497
|
+
export async function handler(event) {
|
|
498
|
+
return watchBucket({
|
|
499
|
+
event,
|
|
500
|
+
dynamoConfigTable: 'Config-Dev',
|
|
501
|
+
dynamoConfigKey: 'importConfig',
|
|
502
|
+
s3Bucket: 'my-data-bucket',
|
|
503
|
+
snsTopic: 'my-import-topic',
|
|
504
|
+
transformer: async (parsedJson) => {
|
|
505
|
+
// Process each file's parsed JSON content
|
|
506
|
+
await saveToDatabase(parsedJson);
|
|
507
|
+
},
|
|
508
|
+
errorHandlerPerFile: (err, filePath) => {
|
|
509
|
+
console.error(`Failed: ${filePath}`, err);
|
|
510
|
+
return false; // return true to suppress error, false to fail
|
|
511
|
+
},
|
|
512
|
+
shouldSkipFailedFolders: false,
|
|
513
|
+
userImportTypes: { orders: 'orders', customers: 'customers' }
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
| Param | Type | Required | Description |
|
|
519
|
+
|-------|------|----------|-------------|
|
|
520
|
+
| `event` | object | Yes | Lambda event (SNS, S3, or CloudWatch cron) |
|
|
521
|
+
| `dynamoConfigTable` | string | Yes | DynamoDB table name for pipeline config |
|
|
522
|
+
| `dynamoConfigKey` | string | Yes | Key in DynamoDB table (holds `snsChunkSize` and `snsMaxMessages`) |
|
|
523
|
+
| `s3Bucket` | string | Yes | S3 bucket name to watch |
|
|
524
|
+
| `snsTopic` | string | Yes | SNS topic name for publishing file batches |
|
|
525
|
+
| `transformer` | function | Yes | `async (parsedJson) => result` - processes each file's content |
|
|
526
|
+
| `errorHandlerPerFile` | function | No | `(error, filePath) => boolean` - return `true` to suppress, `false` to fail batch |
|
|
527
|
+
| `shouldSkipFailedFolders` | boolean | No | If `true`, skip retry folders and move directly to `error/` |
|
|
528
|
+
| `userImportTypes` | object | No | Map of import type subdirectories |
|
|
529
|
+
|
|
530
|
+
**Routing logic:**
|
|
531
|
+
- `EventSource === 'aws:sns'` -> `handleEvents()`
|
|
532
|
+
- `EventSource === 'aws:s3'` -> `handleDirectS3WriteEvent()` (skips files in `failed-once/`, `failed-twice/`, `error/`)
|
|
533
|
+
- `EventSource === 'aws:s3'` with key prefix `DIRECT` -> `handleDirectS3WriteEvent()`
|
|
534
|
+
- Otherwise (cron job, no Records) -> `publishEvents()`
|
|
535
|
+
|
|
536
|
+
### `handleFile(path, s3Bucket, transformer, options?)`
|
|
537
|
+
|
|
538
|
+
Process a single file from S3. Downloads the file, parses it as JSON, passes it to the transformer, then deletes the original. On error, moves the file through the retry folder strategy.
|
|
539
|
+
|
|
540
|
+
```js
|
|
541
|
+
import { handleFile } from 'piper-utils';
|
|
542
|
+
|
|
543
|
+
const result = await handleFile(
|
|
544
|
+
'orders/order-123.json',
|
|
545
|
+
'my-data-bucket',
|
|
546
|
+
async (parsedJson) => {
|
|
547
|
+
// parsedJson is the JSON.parse'd file content
|
|
548
|
+
await Order.create(parsedJson);
|
|
549
|
+
return { processed: true };
|
|
550
|
+
},
|
|
551
|
+
{ shouldSkipFailedFolders: false }
|
|
552
|
+
);
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
| Param | Type | Description |
|
|
556
|
+
|-------|------|-------------|
|
|
557
|
+
| `path` | string | S3 object key |
|
|
558
|
+
| `s3Bucket` | string | S3 bucket name |
|
|
559
|
+
| `transformer` | function | `async (parsedJson) => result` |
|
|
560
|
+
| `options.shouldSkipFailedFolders` | boolean | Skip retry folders, move straight to `error/` |
|
|
561
|
+
| `options.userImportTypes` | object | Import type subdirectories |
|
|
562
|
+
|
|
563
|
+
**Returns:** The transformer's return value, or `undefined` if the file was empty/missing.
|
|
564
|
+
|
|
565
|
+
**Behavior:**
|
|
566
|
+
- Returns silently if file not found (`NoSuchKey`)
|
|
567
|
+
- Returns silently (and deletes file) if body is empty, `{}`, or `[]`
|
|
568
|
+
- On success: deletes the original file
|
|
569
|
+
- On error: moves file through retry folders, then re-throws
|
|
570
|
+
|
|
571
|
+
### `handleEvents(event, transformer, errorHandlerPerFile?, shouldSkipFailedFolders?, userImportTypes?)`
|
|
572
|
+
|
|
573
|
+
Process a batch of files from SNS-relayed S3 events. Processes files in parallel using `Bluebird.Promise.map`.
|
|
574
|
+
|
|
575
|
+
```js
|
|
576
|
+
// event.Records[].Sns.Message = JSON.stringify({ bucket: '...', files: ['file1.json', 'file2.json'] })
|
|
577
|
+
await handleEvents(event, transformer, errorHandlerPerFile, false, userImportTypes);
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
| Param | Type | Description |
|
|
581
|
+
|-------|------|-------------|
|
|
582
|
+
| `event` | object | SNS event with `Records[].Sns.Message` containing `{ bucket, files }` |
|
|
583
|
+
| `transformer` | function | `async (parsedJson) => result` |
|
|
584
|
+
| `errorHandlerPerFile` | function | `(error, filePath) => boolean` - return `true` to suppress |
|
|
585
|
+
| `shouldSkipFailedFolders` | boolean | Default `false` |
|
|
586
|
+
| `userImportTypes` | object | Import subdirectories |
|
|
587
|
+
|
|
588
|
+
Throws `'ERROR: HANDLE-FILE'` if any file fails and `errorHandlerPerFile` returns `false` (or is not provided).
|
|
589
|
+
|
|
590
|
+
### `publishEvents(configTable, tableKey, bucket, snsTopic, userImportTypes?)`
|
|
591
|
+
|
|
592
|
+
Scan an S3 bucket and publish file batches to SNS. Typically called by a cron job via `watchBucket`.
|
|
593
|
+
|
|
594
|
+
```js
|
|
595
|
+
await publishEvents('Config-Dev', 'importConfig', 'my-data-bucket', 'my-import-topic');
|
|
596
|
+
```
|
|
597
|
+
|
|
598
|
+
| Param | Type | Description |
|
|
599
|
+
|-------|------|-------------|
|
|
600
|
+
| `configTable` | string | DynamoDB table with pipeline config |
|
|
601
|
+
| `tableKey` | string | Config key (item must have `snsChunkSize` and `snsMaxMessages`) |
|
|
602
|
+
| `bucket` | string | S3 bucket to scan |
|
|
603
|
+
| `snsTopic` | string | SNS topic name |
|
|
604
|
+
| `userImportTypes` | object | Import subdirectories (optional) |
|
|
605
|
+
|
|
606
|
+
**Config lookup:** Reads `Item.snsChunkSize` (default: 2) and `Item.snsMaxMessages` (default: 10) from DynamoDB.
|
|
607
|
+
|
|
608
|
+
**File filtering:** Skips files in `error/`, `failed-once/`, `failed-twice/`. Supports `delayUntil/` folder (files processed only after the time encoded in the path, format `YYYYMMDDHHmm`).
|
|
609
|
+
|
|
610
|
+
**Publishing:** Lists all eligible files, chunks them by `snsChunkSize`, publishes up to `snsMaxMessages` SNS messages. Each message body: `{ bucket, files: [...keys] }`.
|
|
611
|
+
|
|
612
|
+
### Retry / Error Folder Strategy
|
|
613
|
+
|
|
614
|
+
When `handleFile` encounters an error processing a file, it moves the file through progressive retry folders before giving up:
|
|
615
|
+
|
|
616
|
+
```
|
|
617
|
+
file.json --(error)--> failed-once/file.json
|
|
618
|
+
--(error)--> failed-twice/file.json
|
|
619
|
+
--(error)--> error/file.json (terminal)
|
|
620
|
+
```
|
|
621
|
+
|
|
622
|
+
If `shouldSkipFailedFolders: true`, files go directly to `error/` on first failure.
|
|
623
|
+
|
|
624
|
+
With `userImportTypes`, the same pattern applies within each import type's subfolder:
|
|
625
|
+
```
|
|
626
|
+
orders/file.json --> orders/failed-once/file.json --> orders/failed-twice/file.json --> orders/error/file.json
|
|
627
|
+
```
|
|
628
|
+
|
|
629
|
+
Files in `error/` are never reprocessed. A nesting guard prevents double-nesting (e.g. `failed-once/failed-once/file.json`).
|
|
630
|
+
|
|
631
|
+
---
|
|
632
|
+
|
|
633
|
+
## Database Migrations
|
|
634
|
+
|
|
635
|
+
### `runMigrations(databaseName, sequelizeInstance, initializeModels, pathToMigrationFolder?)`
|
|
636
|
+
|
|
637
|
+
Execute pending database migrations using [Umzug](https://github.com/sequelize/umzug). On failure, rolls back pending migrations before throwing.
|
|
638
|
+
|
|
639
|
+
```js
|
|
640
|
+
import { runMigrations } from 'piper-utils';
|
|
641
|
+
|
|
642
|
+
await runMigrations(
|
|
643
|
+
'my-database',
|
|
644
|
+
sequelizeInstance,
|
|
645
|
+
async () => { await sequelizeInstance.sync(); },
|
|
646
|
+
`${__dirname}/migrations/*.js` // optional, defaults to ${PWD}/migrations/*.js
|
|
647
|
+
);
|
|
648
|
+
```
|
|
649
|
+
|
|
650
|
+
| Param | Type | Default | Description |
|
|
651
|
+
|-------|------|---------|-------------|
|
|
652
|
+
| `databaseName` | string | | Database name (for logging) |
|
|
653
|
+
| `sequelizeInstance` | Sequelize | | Initialized Sequelize instance |
|
|
654
|
+
| `initializeModels` | function | | Async function to define/sync models (called after migrations) |
|
|
655
|
+
| `pathToMigrationFolder` | string | `${PWD}/migrations/*.js` | Glob pattern for migration files |
|
|
656
|
+
|
|
657
|
+
---
|
|
658
|
+
|
|
659
|
+
## Built-in Error Codes
|
|
660
|
+
|
|
661
|
+
The library includes a pre-defined `errorList` used internally by auth checks and `parseBody`. These are the error objects that get thrown/returned:
|
|
662
|
+
|
|
663
|
+
| Key | errorCode | statusCode | Message |
|
|
664
|
+
|-----|-----------|------------|---------|
|
|
665
|
+
| `unauthorized` | 4111 | 401 | UNAUTHORIZED |
|
|
666
|
+
| `notFound` | 4004 | 404 | ITEM NOT FOUND |
|
|
667
|
+
| `invalidJson` | 4005 | 400 | INVALID JSON |
|
|
668
|
+
| `invalidRequest` | 4016 | 400 | INVALID REQUEST DATA |
|
|
669
|
+
| `invalidID` | 4014 | 400 | ID is invalid |
|
|
670
|
+
| `invalidFilter` | 4026 | 400 | INVALID FILTER |
|
|
671
|
+
| `invalidStartDate` | 4020 | 400 | INVALID START DATE |
|
|
672
|
+
| `invalidEndDate` | 4021 | 400 | INVALID END DATE |
|
|
673
|
+
| `invalidDateFormat` | 4019 | 400 | INVALID DATE FORMAT |
|
|
674
|
+
| `invalidAPIKey` | 4017 | 400 | INVALID REQUEST - API MAY KEY INVALID |
|
|
675
|
+
| `invalidUserNameUpdate` | 4027 | 400 | UNABLE TO UPDATE USERNAME |
|
|
676
|
+
| `imageSizeLimit` | 4028 | 400 | IMAGE SIZE LIMIT 100KB EXCEEDED |
|
|
677
|
+
| `emailRequired` | 4004 | 404 | NO EMAIL PROVIDED, CHECK CUSTOMER EMAIL |
|
|
678
|
+
| `mobilePhoneRequired` | 4004 | 404 | NO MOBILE PHONE PROVIDED, CHECK CUSTOMER CONTACTS |
|
|
679
|
+
| `invalidCadenceType` | 5001 | 500 | INVALID CADENCE TYPE |
|
|
680
|
+
|
|
681
|
+
Consuming services typically define their own `errorList` that extends or mirrors this pattern:
|
|
682
|
+
|
|
683
|
+
```js
|
|
684
|
+
const errorList = {
|
|
685
|
+
paymentFailed: { statusCode: 400, errorCode: '4030', message: 'PAYMENT FAILED' }
|
|
686
|
+
};
|
|
687
|
+
|
|
688
|
+
// Throw it — failure() will format it correctly
|
|
689
|
+
throw errorList.paymentFailed;
|
|
690
|
+
```
|
|
691
|
+
|
|
692
|
+
---
|
|
693
|
+
|
|
694
|
+
## Examples
|
|
695
|
+
|
|
696
|
+
### Complete Lambda Handler (Read)
|
|
697
|
+
|
|
698
|
+
```js
|
|
699
|
+
import {
|
|
700
|
+
accessRightsUtils, checkModule,
|
|
701
|
+
createFilters, createSort, findAll,
|
|
702
|
+
success, failure
|
|
703
|
+
} from 'piper-utils';
|
|
704
|
+
|
|
705
|
+
export async function getOrders(event) {
|
|
706
|
+
try {
|
|
707
|
+
checkModule('orders', event);
|
|
708
|
+
|
|
709
|
+
const where = createFilters(event, orderFilter);
|
|
710
|
+
const order = createSort(event, orderFilter);
|
|
711
|
+
const query = event.queryStringParameters || {};
|
|
712
|
+
|
|
713
|
+
const result = await findAll(Order, {
|
|
714
|
+
where,
|
|
715
|
+
order,
|
|
716
|
+
limit: parseInt(query.limit || '20'),
|
|
717
|
+
offset: parseInt(query.offset || '0')
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
return success(result);
|
|
721
|
+
} catch (err) {
|
|
722
|
+
return failure(err);
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
```
|
|
726
|
+
|
|
727
|
+
> Note: `createFilters` automatically scopes the query to the user's authorized business IDs via `accessRightsUtils(event)`. You do not need to add `where.businessId` manually.
|
|
728
|
+
|
|
729
|
+
### Complete Lambda Handler (Write)
|
|
730
|
+
|
|
731
|
+
```js
|
|
732
|
+
import {
|
|
733
|
+
parseBody, getCurrentUser, checkWriteAccess,
|
|
734
|
+
success, failure
|
|
735
|
+
} from 'piper-utils';
|
|
736
|
+
|
|
737
|
+
export async function updateOrder(event) {
|
|
738
|
+
try {
|
|
739
|
+
const user = getCurrentUser(event);
|
|
740
|
+
// => { username: 'john@example.com', id: 42 }
|
|
741
|
+
|
|
742
|
+
const businessId = checkWriteAccess(event);
|
|
743
|
+
// Throws 401 if user lacks write/admin role for the businessId in body
|
|
744
|
+
|
|
745
|
+
const body = parseBody(event);
|
|
746
|
+
|
|
747
|
+
const order = await Order.findOne({
|
|
748
|
+
where: { id: event.pathParameters.id, businessId }
|
|
749
|
+
});
|
|
750
|
+
|
|
751
|
+
if (!order) throw { statusCode: 404, errorCode: '4004', message: 'Order not found' };
|
|
752
|
+
|
|
753
|
+
order.set({ ...body, updatedBy: user.id });
|
|
754
|
+
await order.save();
|
|
755
|
+
|
|
756
|
+
return success(order);
|
|
757
|
+
} catch (e) {
|
|
758
|
+
return failure(e);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
```
|
|
762
|
+
|
|
763
|
+
### S3 File Processing Pipeline
|
|
764
|
+
|
|
765
|
+
```js
|
|
766
|
+
import { watchBucket, success, failure } from 'piper-utils';
|
|
767
|
+
|
|
768
|
+
export async function importHandler(event) {
|
|
769
|
+
try {
|
|
770
|
+
return await watchBucket({
|
|
771
|
+
event,
|
|
772
|
+
dynamoConfigTable: 'Config-Dev',
|
|
773
|
+
dynamoConfigKey: 'orderImportConfig',
|
|
774
|
+
s3Bucket: 'order-imports-dev',
|
|
775
|
+
snsTopic: 'order-import-topic-dev',
|
|
776
|
+
transformer: async (parsedJson) => {
|
|
777
|
+
// Each file contains a JSON order object
|
|
778
|
+
await Order.create(parsedJson);
|
|
779
|
+
},
|
|
780
|
+
errorHandlerPerFile: (err, filePath) => {
|
|
781
|
+
console.error(`Import failed for ${filePath}:`, err);
|
|
782
|
+
return false; // don't suppress — let retry folders handle it
|
|
783
|
+
}
|
|
784
|
+
});
|
|
785
|
+
} catch (e) {
|
|
786
|
+
return failure(e);
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
```
|
|
790
|
+
|
|
791
|
+
### Partner Access Enrichment
|
|
792
|
+
|
|
793
|
+
```js
|
|
794
|
+
import {
|
|
795
|
+
getEffectivePartnerId, enrichEventWithPartnerAccess,
|
|
796
|
+
accessRightsUtils, success, failure
|
|
797
|
+
} from 'piper-utils';
|
|
798
|
+
|
|
799
|
+
export async function getPartnerOrders(event) {
|
|
800
|
+
try {
|
|
801
|
+
const partnerId = getEffectivePartnerId(event);
|
|
802
|
+
|
|
803
|
+
if (partnerId) {
|
|
804
|
+
// Look up which businesses belong to this partner
|
|
805
|
+
const partnerBusinessIds = await getPartnerBusinessIds(partnerId);
|
|
806
|
+
// Inject them into the event so accessRightsUtils includes them
|
|
807
|
+
enrichEventWithPartnerAccess(event, partnerBusinessIds, 'R');
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
const businessIds = accessRightsUtils(event);
|
|
811
|
+
// Now includes both user's own businesses AND partner businesses
|
|
812
|
+
|
|
813
|
+
// ... query with businessIds
|
|
814
|
+
return success(results);
|
|
815
|
+
} catch (e) {
|
|
816
|
+
return failure(e);
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
```
|
|
820
|
+
|
|
821
|
+
---
|
|
822
|
+
|
|
823
|
+
## Peer Dependencies
|
|
824
|
+
|
|
825
|
+
These must be installed in the consuming project:
|
|
826
|
+
|
|
827
|
+
| Package | Version |
|
|
828
|
+
|---------|---------|
|
|
829
|
+
| `bluebird` | >= 3.7.0 |
|
|
830
|
+
| `dayjs` | ^1.11.13 |
|
|
831
|
+
| `lodash` | >= 4.17.15 |
|
|
832
|
+
| `sequelize` | >= 6.6.2 |
|
|
833
|
+
| `umzug` | >= 3.2.1 |
|
|
834
|
+
|
|
835
|
+
## Testing
|
|
836
|
+
|
|
837
|
+
```bash
|
|
838
|
+
npm test # unit tests (BUILD_ENV=test)
|
|
839
|
+
npm run itest # integration tests (BUILD_ENV=development)
|
|
840
|
+
```
|
|
841
|
+
|
|
842
|
+
Tests use Jasmine 5 with NYC coverage. Coverage thresholds: branches >= 70%, functions >= 70%, statements >= 85%, lines >= 80%.
|
|
843
|
+
|
|
844
|
+
## License
|
|
845
|
+
|
|
846
|
+
Private - Copyright (c) Piper
|