@bitblit/ratchet-common 6.0.146-alpha → 6.0.148-alpha

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.
Files changed (140) hide show
  1. package/package.json +2 -1
  2. package/src/2d/line-2d.ts +6 -0
  3. package/src/2d/matrix-factory.ts +94 -0
  4. package/src/2d/plane-2d-type.ts +6 -0
  5. package/src/2d/plane-2d.ts +7 -0
  6. package/src/2d/point-2d.ts +4 -0
  7. package/src/2d/poly-line-2d.ts +5 -0
  8. package/src/2d/ratchet-2d.spec.ts +205 -0
  9. package/src/2d/ratchet-2d.ts +350 -0
  10. package/src/2d/transformation-matrix.ts +19 -0
  11. package/src/build/build-information.ts +8 -0
  12. package/src/build/ratchet-common-info.ts +19 -0
  13. package/src/histogram/histogram-entry.ts +4 -0
  14. package/src/histogram/histogram.spec.ts +25 -0
  15. package/src/histogram/histogram.ts +61 -0
  16. package/src/jwt/common-jwt-token.ts +17 -0
  17. package/src/jwt/expired-jwt-handling.ts +5 -0
  18. package/src/jwt/jwt-decode-only-ratchet.ts +26 -0
  19. package/src/jwt/jwt-payload-expiration-ratchet.ts +45 -0
  20. package/src/jwt/jwt-token-base.ts +14 -0
  21. package/src/lang/array-ratchet.spec.ts +79 -0
  22. package/src/lang/array-ratchet.ts +141 -0
  23. package/src/lang/base64-ratchet.spec.ts +48 -0
  24. package/src/lang/base64-ratchet.ts +247 -0
  25. package/src/lang/boolean-ratchet.spec.ts +95 -0
  26. package/src/lang/boolean-ratchet.ts +52 -0
  27. package/src/lang/composite-last-success-provider.spec.ts +31 -0
  28. package/src/lang/composite-last-success-provider.ts +30 -0
  29. package/src/lang/currency-ratchet.ts +29 -0
  30. package/src/lang/date-ratchet.spec.ts +27 -0
  31. package/src/lang/date-ratchet.ts +42 -0
  32. package/src/lang/duration-ratchet.spec.ts +47 -0
  33. package/src/lang/duration-ratchet.ts +77 -0
  34. package/src/lang/enum-ratchet.spec.ts +45 -0
  35. package/src/lang/enum-ratchet.ts +41 -0
  36. package/src/lang/error-handling-approach.ts +6 -0
  37. package/src/lang/error-ratchet.spec.ts +25 -0
  38. package/src/lang/error-ratchet.ts +70 -0
  39. package/src/lang/esm-ratchet.ts +81 -0
  40. package/src/lang/expiring-object.spec.ts +56 -0
  41. package/src/lang/expiring-object.ts +84 -0
  42. package/src/lang/geolocation-ratchet.spec.ts +177 -0
  43. package/src/lang/geolocation-ratchet.ts +341 -0
  44. package/src/lang/global-ratchet.spec.ts +17 -0
  45. package/src/lang/global-ratchet.ts +105 -0
  46. package/src/lang/key-value.ts +8 -0
  47. package/src/lang/last-success-provider.ts +4 -0
  48. package/src/lang/map-ratchet.spec.ts +113 -0
  49. package/src/lang/map-ratchet.ts +220 -0
  50. package/src/lang/no.spec.ts +9 -0
  51. package/src/lang/no.ts +7 -0
  52. package/src/lang/number-ratchet.spec.ts +154 -0
  53. package/src/lang/number-ratchet.ts +253 -0
  54. package/src/lang/parsed-url.ts +10 -0
  55. package/src/lang/promise-ratchet.spec.ts +104 -0
  56. package/src/lang/promise-ratchet.ts +196 -0
  57. package/src/lang/range.ts +4 -0
  58. package/src/lang/require-ratchet.spec.ts +85 -0
  59. package/src/lang/require-ratchet.ts +68 -0
  60. package/src/lang/simple-arg-ratchet.spec.ts +13 -0
  61. package/src/lang/simple-arg-ratchet.ts +47 -0
  62. package/src/lang/simple-encryption-ratchet.ts +88 -0
  63. package/src/lang/sort-ratchet.spec.ts +58 -0
  64. package/src/lang/sort-ratchet.ts +50 -0
  65. package/src/lang/stop-watch.spec.ts +53 -0
  66. package/src/lang/stop-watch.ts +202 -0
  67. package/src/lang/string-ratchet.spec.ts +226 -0
  68. package/src/lang/string-ratchet.ts +676 -0
  69. package/src/lang/time-zone-ratchet.spec.ts +51 -0
  70. package/src/lang/time-zone-ratchet.ts +148 -0
  71. package/src/lang/timeout-token.spec.ts +12 -0
  72. package/src/lang/timeout-token.ts +21 -0
  73. package/src/lang/uint-8-array-ratchet.spec.ts +22 -0
  74. package/src/lang/uint-8-array-ratchet.ts +48 -0
  75. package/src/lang/web-stream-ratchet.spec.ts +12 -0
  76. package/src/lang/web-stream-ratchet.ts +96 -0
  77. package/src/logger/classic-single-line-log-message-formatter.ts +19 -0
  78. package/src/logger/log-message-builder.ts +60 -0
  79. package/src/logger/log-message-format-type.ts +11 -0
  80. package/src/logger/log-message-formatter.ts +6 -0
  81. package/src/logger/log-message-processor.ts +6 -0
  82. package/src/logger/log-message.ts +9 -0
  83. package/src/logger/log-snapshot.ts +6 -0
  84. package/src/logger/logger-instance.ts +269 -0
  85. package/src/logger/logger-level-name.ts +11 -0
  86. package/src/logger/logger-meta.ts +7 -0
  87. package/src/logger/logger-options.ts +14 -0
  88. package/src/logger/logger-output-function.ts +10 -0
  89. package/src/logger/logger-ring-buffer.ts +89 -0
  90. package/src/logger/logger-util.spec.ts +11 -0
  91. package/src/logger/logger-util.ts +68 -0
  92. package/src/logger/logger.spec.ts +177 -0
  93. package/src/logger/logger.ts +213 -0
  94. package/src/logger/none-log-message-formatter.ts +10 -0
  95. package/src/logger/single-line-no-level-log-message-formatter.ts +18 -0
  96. package/src/logger/structured-json-log-message-formatter.ts +25 -0
  97. package/src/mail/archive-email-result.ts +8 -0
  98. package/src/mail/email-attachment.ts +23 -0
  99. package/src/mail/mail-sending-provider.ts +21 -0
  100. package/src/mail/mailer-config.ts +30 -0
  101. package/src/mail/mailer-like.ts +38 -0
  102. package/src/mail/mailer-util.ts +65 -0
  103. package/src/mail/mailer.spec.ts +120 -0
  104. package/src/mail/mailer.ts +214 -0
  105. package/src/mail/ready-to-send-email.ts +67 -0
  106. package/src/mail/resolved-ready-to-send-email.ts +17 -0
  107. package/src/mail/send-email-result.ts +16 -0
  108. package/src/mail/test-mail-sending-provider.ts +35 -0
  109. package/src/network/browser-local-ip-provider.spec.ts +23 -0
  110. package/src/network/browser-local-ip-provider.ts +26 -0
  111. package/src/network/fixed-local-ip-provider.ts +9 -0
  112. package/src/network/local-ip-provider.ts +4 -0
  113. package/src/network/network-ratchet.spec.ts +17 -0
  114. package/src/network/network-ratchet.ts +209 -0
  115. package/src/network/remote-file-tracker/backup-result.ts +6 -0
  116. package/src/network/remote-file-tracker/file-transfer-result-type.ts +5 -0
  117. package/src/network/remote-file-tracker/file-transfer-result.ts +9 -0
  118. package/src/network/remote-file-tracker/remote-file-tracker-options.ts +6 -0
  119. package/src/network/remote-file-tracker/remote-file-tracker-push-options.ts +4 -0
  120. package/src/network/remote-file-tracker/remote-file-tracker.ts +117 -0
  121. package/src/network/remote-file-tracker/remote-file-tracking-provider.ts +19 -0
  122. package/src/network/remote-file-tracker/remote-status-data-and-content.ts +6 -0
  123. package/src/network/remote-file-tracker/remote-status-data.ts +7 -0
  124. package/src/network/restful-api-http-error.spec.ts +13 -0
  125. package/src/network/restful-api-http-error.ts +173 -0
  126. package/src/template/ratchet-template-renderer.ts +8 -0
  127. package/src/third-party/google/google-recaptcha-ratchet.spec.ts +27 -0
  128. package/src/third-party/google/google-recaptcha-ratchet.ts +36 -0
  129. package/src/third-party/twilio/twilio-ratchet.ts +92 -0
  130. package/src/third-party/twilio/twilio-verify-ratchet.ts +83 -0
  131. package/src/transform/built-in-transforms.ts +214 -0
  132. package/src/transform/transform-ratchet.spec.ts +134 -0
  133. package/src/transform/transform-ratchet.ts +88 -0
  134. package/src/transform/transform-rule.ts +7 -0
  135. package/src/tx/transaction-configuration.ts +8 -0
  136. package/src/tx/transaction-final-state.ts +7 -0
  137. package/src/tx/transaction-ratchet.spec.ts +150 -0
  138. package/src/tx/transaction-ratchet.ts +98 -0
  139. package/src/tx/transaction-result.ts +10 -0
  140. package/src/tx/transaction-step.ts +5 -0
@@ -0,0 +1,214 @@
1
+ /*
2
+ Some useful transforms for the transform ratchet
3
+ */
4
+
5
+ import { TransformRule } from './transform-rule.js';
6
+ import { Logger } from '../logger/logger.js';
7
+ import { NumberRatchet } from '../lang/number-ratchet.js';
8
+ import { DateTime } from 'luxon';
9
+
10
+ export class BuiltInTransforms {
11
+ public static keysOnly(rule: TransformRule): TransformRule {
12
+ return {
13
+ transform(value: any, isKey: boolean, context: any): any {
14
+ return isKey ? rule.transform(value, isKey, context) : value;
15
+ },
16
+ } as TransformRule;
17
+ }
18
+
19
+ public static valuesOnly(rule: TransformRule): TransformRule {
20
+ return {
21
+ transform(value: any, isKey: boolean, context: any): any {
22
+ return !isKey ? rule.transform(value, isKey, context) : value;
23
+ },
24
+ } as TransformRule;
25
+ }
26
+
27
+ public static stringReplaceTransform(input: string, output: string): TransformRule {
28
+ return {
29
+ transform(value: any, _isKey: boolean, _context: any): any {
30
+ return value == input ? output : value;
31
+ },
32
+ } as TransformRule;
33
+ }
34
+
35
+ public static stripStringTransform(input: string): TransformRule {
36
+ return {
37
+ transform(value: any, _isKey: boolean, _context: any): any {
38
+ return value == input ? null : value;
39
+ },
40
+ } as TransformRule;
41
+ }
42
+
43
+ public static retainAll(input: string[]): TransformRule {
44
+ return {
45
+ transform(value: any, _isKey: boolean, _context: any): any {
46
+ return input.indexOf(value) == -1 ? null : value;
47
+ },
48
+ } as TransformRule;
49
+ }
50
+
51
+ public static removeAll(input: string[]): TransformRule {
52
+ return {
53
+ transform(value: any, _isKey: boolean, _context: any): any {
54
+ return input.indexOf(value) > -1 ? null : value;
55
+ },
56
+ } as TransformRule;
57
+ }
58
+
59
+ public static snakeToCamelCase(): TransformRule {
60
+ return {
61
+ transform(value: any, _isKey: boolean, _context: any): any {
62
+ let rval = value;
63
+ if (typeof value == 'string') {
64
+ // Taken, mainly, from https://stackoverflow.com/questions/4969605/javascript-regexp-to-camelcase-a-hyphened-css-property
65
+ rval = value.replace(/_([a-z0-9])/gi, function (s, group1) {
66
+ return group1.toUpperCase();
67
+ });
68
+ }
69
+ return rval;
70
+ },
71
+ } as TransformRule;
72
+ }
73
+
74
+ public static stringToNumber(): TransformRule {
75
+ return {
76
+ transform(value: any, _isKey: boolean, _context: any): any {
77
+ let rval = value;
78
+ if (typeof value == 'string') {
79
+ const num: number = NumberRatchet.safeNumber(value);
80
+ if (num !== null) {
81
+ rval = num;
82
+ }
83
+ }
84
+ return rval;
85
+ },
86
+ } as TransformRule;
87
+ }
88
+
89
+ public static camelToSnakeCase(): TransformRule {
90
+ return {
91
+ transform(value: any, _isKey: boolean, _context: any): any {
92
+ let rval = value;
93
+ if (typeof value == 'string') {
94
+ // https://stackoverflow.com/questions/30521224/javascript-convert-pascalcase-to-underscore-case
95
+ rval = value
96
+ .replace(/\.?([A-Z]+)/g, function (x, y) {
97
+ return '_' + y.toLowerCase();
98
+ })
99
+ .replace(/^_/, '');
100
+ }
101
+ return rval;
102
+ },
103
+ } as TransformRule;
104
+ }
105
+
106
+ public static concatenateToNewField(newFieldName: string, oldFieldNamesInOrder: string[], abortIfFieldMissing = true): TransformRule {
107
+ return {
108
+ transform(value: any, _isKey: boolean, _context: any): any {
109
+ if (typeof value == 'object') {
110
+ let rval = '';
111
+ oldFieldNamesInOrder.forEach((n) => {
112
+ if (rval != null) {
113
+ const temp = value[n];
114
+ if (temp == null && abortIfFieldMissing) {
115
+ rval = null;
116
+ } else {
117
+ rval = temp == null ? rval : rval + String(temp);
118
+ }
119
+ }
120
+ });
121
+ if (rval != null) {
122
+ value[newFieldName] = rval;
123
+ // Dynamic-safe(r) since we are outside the loop
124
+ // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
125
+ oldFieldNamesInOrder.forEach((n) => delete value[n]);
126
+ }
127
+ }
128
+ return value;
129
+ },
130
+ } as TransformRule;
131
+ }
132
+
133
+ public static numberToBool(fieldNames: string[]): TransformRule {
134
+ return {
135
+ transform(value: any, _isKey: boolean, _context: any): any {
136
+ if (typeof value == 'object') {
137
+ fieldNames.forEach((n) => {
138
+ const oldVal = value[n];
139
+ if (typeof oldVal == 'number') {
140
+ const newVal = 0 != oldVal;
141
+ value[n] = newVal;
142
+ }
143
+ });
144
+ }
145
+ return value;
146
+ },
147
+ } as TransformRule;
148
+ }
149
+
150
+ public static boolToNumber(fieldNames: string[]): TransformRule {
151
+ return {
152
+ transform(value: any, _isKey: boolean, _context: any): any {
153
+ if (typeof value == 'object') {
154
+ fieldNames.forEach((n) => {
155
+ const oldVal = value[n];
156
+ if (typeof oldVal == 'boolean') {
157
+ const newVal = oldVal ? 1 : 0;
158
+ value[n] = newVal;
159
+ }
160
+ });
161
+ }
162
+ return value;
163
+ },
164
+ } as TransformRule;
165
+ }
166
+
167
+ public static makeDuplicateField(oldName: string, newName: string): TransformRule {
168
+ return {
169
+ transform(value: any, _isKey: boolean, _context: any): any {
170
+ if (typeof value == 'object') {
171
+ const oldVal = value[oldName];
172
+ if (oldVal != null) {
173
+ value[newName] = oldVal;
174
+ }
175
+ }
176
+ return value;
177
+ },
178
+ } as TransformRule;
179
+ }
180
+
181
+ public static addField(name: string, valueToAdd: string): TransformRule {
182
+ return {
183
+ transform(value: any, _isKey: boolean, _context: any): any {
184
+ if (typeof value == 'object') {
185
+ value[name] = valueToAdd;
186
+ }
187
+ return value;
188
+ },
189
+ } as TransformRule;
190
+ }
191
+
192
+ // Moment formats, https://momentjs.com/docs/#/parsing/
193
+ public static reformatDateFields(fieldNames: string[], oldFormat: string, newFormat: string): TransformRule {
194
+ return {
195
+ transform(value: any, _isKey: boolean, _context: any): any {
196
+ if (typeof value == 'object') {
197
+ fieldNames.forEach((key) => {
198
+ const oldValue = value[key];
199
+ if (oldValue != null) {
200
+ try {
201
+ const parsed: DateTime = DateTime.fromFormat(oldValue, oldFormat);
202
+ const newValue: string = parsed.toFormat(newFormat);
203
+ value[key] = newValue;
204
+ } catch (err) {
205
+ Logger.warn('Failed to reparse date %s in format %s : %s', oldValue, oldFormat, err);
206
+ }
207
+ }
208
+ });
209
+ }
210
+ return value;
211
+ },
212
+ } as TransformRule;
213
+ }
214
+ }
@@ -0,0 +1,134 @@
1
+ import { TransformRatchet } from './transform-ratchet.js';
2
+ import { BuiltInTransforms } from './built-in-transforms.js';
3
+ import { describe, expect, test } from 'vitest';
4
+
5
+ describe('#formatBytes', function () {
6
+ const srcData = {
7
+ key1: 'value1',
8
+ key2: 'value2',
9
+ dateKey1: '1995-02-01',
10
+ intKey1: 0,
11
+ intKey2: 1,
12
+ boolKey1: true,
13
+ boolKey2: false,
14
+ camelSnake: 'this is a camel case',
15
+ subKey: {
16
+ key1: 'subValue1',
17
+ key2: 'subValue2',
18
+ convertToNumberInner: '42',
19
+ },
20
+ convertToNumber: '1',
21
+ convertToNumberAlso: '20',
22
+ };
23
+
24
+ const srcUnderData = {
25
+ new_key_1: 'value1',
26
+ sub_key: {
27
+ new_key_2: 'value2',
28
+ },
29
+ };
30
+
31
+ test('should convert camel to snakecase', function () {
32
+ const result = TransformRatchet.transform(srcData, [BuiltInTransforms.keysOnly(BuiltInTransforms.camelToSnakeCase())]);
33
+ expect(result['dateKey1']).toBeUndefined();
34
+ expect(result['intKey1']).toBeUndefined();
35
+ expect(result['intKey2']).toBeUndefined();
36
+ expect(result['boolKey1']).toBeUndefined();
37
+ expect(result['boolKey2']).toBeUndefined();
38
+ expect(result['subKey']).toBeUndefined();
39
+ expect(result['date_key1']).toBeTruthy();
40
+ expect(result['int_key1']).toEqual(0);
41
+ expect(result['int_key2']).toBeTruthy();
42
+ expect(result['bool_key1']).toBeTruthy();
43
+ expect(result['bool_key1']).toBeTruthy();
44
+ expect(result['sub_key']).toBeTruthy();
45
+ });
46
+
47
+ test('should duplicate key1 into key3', function () {
48
+ const result = TransformRatchet.transform(srcData, [BuiltInTransforms.makeDuplicateField('key1', 'key3')]);
49
+ expect(result.key1).toEqual('value1');
50
+ expect(result.key3).toEqual('value1');
51
+ });
52
+
53
+ test('should create a new field named key3', function () {
54
+ const result = TransformRatchet.transform(srcData, [BuiltInTransforms.addField('key3', 'value3')]);
55
+ expect(result.key1).toEqual('value1');
56
+ expect(result.key3).toEqual('value3');
57
+ });
58
+
59
+ test('should reformat the date in dateField1 to MM-dd-yyyy', function () {
60
+ const result = TransformRatchet.transform(srcData, [BuiltInTransforms.reformatDateFields(['dateKey1'], 'yyyy-MM-dd', 'MM/dd/yyyy')]);
61
+ expect(result.dateKey1).toEqual('02/01/1995');
62
+ });
63
+
64
+ test('should convert numbers to booleans', function () {
65
+ const result = TransformRatchet.transform(srcData, [BuiltInTransforms.numberToBool(['intKey1', 'intKey2'])]);
66
+ expect(result.key1).toEqual('value1');
67
+ expect(result.intKey1).toEqual(false);
68
+ expect(result.intKey2).toEqual(true);
69
+ });
70
+
71
+ test('should convert booleans to number', function () {
72
+ const result = TransformRatchet.transform(srcData, [BuiltInTransforms.boolToNumber(['boolKey1', 'boolKey2'])]);
73
+ expect(result.key1).toEqual('value1');
74
+ expect(result.boolKey1).toEqual(1);
75
+ expect(result.boolKey2).toEqual(0);
76
+ });
77
+
78
+ test('should concatenate key1 and key2 into key3', function () {
79
+ const result = TransformRatchet.transform(srcData, [BuiltInTransforms.concatenateToNewField('key3', ['key1', 'key2'])]);
80
+ expect(result.key1).toBeUndefined();
81
+ expect(result.key2).toBeUndefined();
82
+ expect(result.key3).toEqual('value1value2');
83
+ expect(result.subKey.key1).toBeUndefined();
84
+ expect(result.subKey.key2).toBeUndefined();
85
+ expect(result.subKey.key3).toEqual('subValue1subValue2');
86
+ });
87
+
88
+ test('should strip a key correctly', function () {
89
+ const result = TransformRatchet.transform(srcData, [BuiltInTransforms.stripStringTransform('key1')]);
90
+ expect(result.key1).toBeUndefined();
91
+ });
92
+
93
+ test('should rename a key correctly', function () {
94
+ const result = TransformRatchet.transform(srcData, [BuiltInTransforms.stringReplaceTransform('key1', 'newKey1')]);
95
+ expect(result.key1).toBeUndefined();
96
+ expect(result.newKey1).toEqual('value1');
97
+ expect(result.subKey.key1).toBeUndefined();
98
+ expect(result.subKey.newKey1).toEqual('subValue1');
99
+ });
100
+
101
+ test('should retain only key1 and key2', function () {
102
+ const result = TransformRatchet.transform(srcData, [BuiltInTransforms.keysOnly(BuiltInTransforms.retainAll(['key1', 'key2']))]);
103
+ expect(result.key1).toBeTruthy();
104
+ expect(result.key2).toBeTruthy();
105
+ expect(result.intKey1).toBeUndefined();
106
+ expect(result.boolKey1).toBeUndefined();
107
+ expect(result.subKey).toBeUndefined();
108
+ });
109
+
110
+ test('should remove only key1 and key2', function () {
111
+ const result = TransformRatchet.transform(srcData, [BuiltInTransforms.keysOnly(BuiltInTransforms.removeAll(['key1', 'key2']))]);
112
+ expect(result.key1).toBeUndefined();
113
+ expect(result.key2).toBeUndefined();
114
+ expect(result.intKey1).toEqual(0);
115
+ expect(result.boolKey1).toBeTruthy();
116
+ expect(result.subKey).toBeTruthy();
117
+ });
118
+
119
+ test('should convert snake to camelcase', function () {
120
+ const result = TransformRatchet.transform(srcUnderData, [BuiltInTransforms.keysOnly(BuiltInTransforms.snakeToCamelCase())]);
121
+ expect(result['new_key_1']).toBeUndefined();
122
+ expect(result['sub_key']).toBeUndefined();
123
+ expect(result.newKey1).toBeTruthy();
124
+ expect(result.subKey).toBeTruthy();
125
+ expect(result.subKey.newKey2).toBeTruthy();
126
+ });
127
+
128
+ test('should convert strings to numbers', function () {
129
+ const result = TransformRatchet.transform(srcData, [BuiltInTransforms.valuesOnly(BuiltInTransforms.stringToNumber())]);
130
+ expect(result['convertToNumber']).toEqual(1);
131
+ expect(result['convertToNumberAlso']).toEqual(20);
132
+ expect(result['subKey']['convertToNumberInner']).toEqual(42);
133
+ });
134
+ });
@@ -0,0 +1,88 @@
1
+ /*
2
+ Functions for working with maps (dictionaries/objects in javascript)
3
+
4
+ For each entry:
5
+ 1) Apply to key
6
+ 2) If the key is now null, skip it, else:
7
+ 2a) If the value is an array or map, recursively descend it
8
+ 2b) Apply to value
9
+ 3) If the value is now null, skip it. Else, put into new result
10
+
11
+ */
12
+
13
+ import { TransformRule } from './transform-rule.js';
14
+ import { Logger } from '../logger/logger.js';
15
+
16
+ export class TransformRatchet {
17
+ public static transform(toTransform: any, rules: TransformRule[] = []): any {
18
+ return TransformRatchet.transformGeneric(toTransform, rules, false, null);
19
+ }
20
+
21
+ private static transformGeneric(toTransform: any, rules: TransformRule[] = [], isKey: boolean, context: any): any {
22
+ let rval: any = null;
23
+ const type: string = typeof toTransform;
24
+ switch (type) {
25
+ case 'undefined':
26
+ case 'symbol':
27
+ case 'function':
28
+ rval = toTransform;
29
+ break;
30
+ case 'number':
31
+ case 'string':
32
+ case 'boolean':
33
+ rval = TransformRatchet.applyTransformToPrimitive(toTransform, rules, isKey, context);
34
+ break;
35
+ case 'object':
36
+ rval = TransformRatchet.applyTransformToObject(toTransform, rules, isKey, context);
37
+ break;
38
+ default:
39
+ throw new Error('Unrecognized type ' + type);
40
+ }
41
+ return rval;
42
+ }
43
+
44
+ private static applyTransformToObject(toTransform: any, rules: TransformRule[] = [], isKey: boolean, context: any = null) {
45
+ Logger.silly('Tranform: %j, %s, %j', toTransform, isKey, context);
46
+ let rval: any = null;
47
+ if (toTransform != null) {
48
+ if (Array.isArray(toTransform)) {
49
+ rval = [];
50
+ toTransform.forEach((val) => {
51
+ const newVal = TransformRatchet.transformGeneric(val, rules, isKey, toTransform);
52
+ if (newVal != null) {
53
+ rval.push(newVal);
54
+ }
55
+ });
56
+ } else {
57
+ // Its a complex object
58
+ rval = {};
59
+ Object.keys(toTransform).forEach((k) => {
60
+ // First, transform the key
61
+ const oldValue = toTransform[k];
62
+ const newKey = TransformRatchet.applyTransformToPrimitive(k, rules, true, toTransform);
63
+ if (newKey != null) {
64
+ // Recursively descend first
65
+ let newValue = TransformRatchet.transformGeneric(oldValue, rules, false, toTransform);
66
+ // Then apply to the object as a whole
67
+ newValue = TransformRatchet.applyTransformToPrimitive(newValue, rules, false, toTransform);
68
+
69
+ if (newValue != null) {
70
+ rval[newKey] = newValue;
71
+ }
72
+ }
73
+ });
74
+ // Finally, apply to the object itself
75
+ rval = TransformRatchet.applyTransformToPrimitive(rval, rules, false, toTransform);
76
+ }
77
+ }
78
+ return rval;
79
+ }
80
+
81
+ private static applyTransformToPrimitive(toTransform: any, rules: TransformRule[] = [], isKey: boolean, context: any) {
82
+ let rval: any = toTransform;
83
+ rules.forEach((r) => {
84
+ rval = rval == null ? null : r.transform(rval, isKey, context);
85
+ });
86
+ return rval;
87
+ }
88
+ }
@@ -0,0 +1,7 @@
1
+ /*
2
+ Functions as input for the transformer
3
+ */
4
+
5
+ export interface TransformRule {
6
+ transform(value: any, isKey: boolean, context: any): any;
7
+ }
@@ -0,0 +1,8 @@
1
+ import { TransactionResult } from './transaction-result.js';
2
+ import { LoggerLevelName } from '../logger/logger-level-name.js';
3
+
4
+ export interface TransactionConfiguration<T> {
5
+ stepLogLevel?: LoggerLevelName;
6
+ executeAfterRollback?(result: TransactionResult<T>): Promise<void>;
7
+ executeAfterRollbackFailure?(result: TransactionResult<T>): Promise<void>;
8
+ }
@@ -0,0 +1,7 @@
1
+ // NOTE: This is a psuedo-enum to fix some issues with Typescript enums. See: https://exploringjs.com/tackling-ts/ch_enum-alternatives.html for details
2
+ export const TransactionFinalState = {
3
+ Success: 'Success',
4
+ RolledBack: 'RolledBack',
5
+ RollbackFailed: 'RollbackFailed',
6
+ };
7
+ export type TransactionFinalState = (typeof TransactionFinalState)[keyof typeof TransactionFinalState];
@@ -0,0 +1,150 @@
1
+ import { TransactionStep } from './transaction-step.js';
2
+ import { TransactionRatchet } from './transaction-ratchet.js';
3
+ import { TransactionResult } from './transaction-result.js';
4
+ import { TransactionFinalState } from './transaction-final-state.js';
5
+ import { LoggerLevelName } from '../logger/logger-level-name.js';
6
+ import { describe, expect, test } from 'vitest';
7
+
8
+ interface TestTransactionContext {
9
+ runningTotal: number;
10
+ failStep?: number;
11
+ failRollbackStep?: number;
12
+ postTxTracker?: number;
13
+ }
14
+
15
+ class TestTransactionStep implements TransactionStep<TestTransactionContext> {
16
+ async execute(context: TestTransactionContext, stepNumber?: number): Promise<void> {
17
+ context.runningTotal += stepNumber;
18
+ if (stepNumber === context?.failStep) {
19
+ throw new Error('Forced error at step ' + stepNumber);
20
+ }
21
+ }
22
+
23
+ async rollback(context: TestTransactionContext, stepNumber?: number): Promise<void> {
24
+ context.runningTotal -= stepNumber;
25
+ if (stepNumber === context?.failRollbackStep) {
26
+ throw new Error('Forced rollback error at step ' + stepNumber);
27
+ }
28
+ }
29
+ }
30
+
31
+ describe('#TransactionRatchet.execute', function () {
32
+ test('should return the right value when it executes successfully', async () => {
33
+ const steps: TestTransactionStep[] = [new TestTransactionStep(), new TestTransactionStep(), new TestTransactionStep()];
34
+ const ctx: TestTransactionContext = { runningTotal: 0 };
35
+ const result: TransactionResult<TestTransactionContext> = await TransactionRatchet.execute(steps, ctx);
36
+ expect(result).not.toBeNull();
37
+ expect(result.error).toBeUndefined();
38
+ expect(result.finalState).toEqual(TransactionFinalState.Success);
39
+ expect(result.finalContext.runningTotal).toEqual(3);
40
+ });
41
+
42
+ test('should return the right value when it rolls back successfully fail step 2', async () => {
43
+ const steps: TestTransactionStep[] = [new TestTransactionStep(), new TestTransactionStep(), new TestTransactionStep()];
44
+ const ctx: TestTransactionContext = { runningTotal: 0, failStep: 2 };
45
+ const result: TransactionResult<TestTransactionContext> = await TransactionRatchet.execute(steps, ctx);
46
+ expect(result).not.toBeUndefined();
47
+ expect(result.finalState).toEqual(TransactionFinalState.RolledBack);
48
+ expect(result.errorStep).toEqual(2);
49
+ expect(result.error).not.toBeUndefined();
50
+ expect(result.finalContext.runningTotal).toEqual(0);
51
+ });
52
+
53
+ test('should return the right value when it rolls back successfully fail step 1', async () => {
54
+ const steps: TestTransactionStep[] = [new TestTransactionStep(), new TestTransactionStep(), new TestTransactionStep()];
55
+ const ctx: TestTransactionContext = { runningTotal: 0, failStep: 2 };
56
+ const result: TransactionResult<TestTransactionContext> = await TransactionRatchet.execute(steps, ctx);
57
+ expect(result).not.toBeUndefined();
58
+ expect(result.finalState).toEqual(TransactionFinalState.RolledBack);
59
+ expect(result.errorStep).toEqual(2);
60
+ expect(result.error).not.toBeUndefined();
61
+ expect(result.finalContext.runningTotal).toEqual(0);
62
+ });
63
+
64
+ test('should return the right value the rollback fails', async () => {
65
+ const steps: TestTransactionStep[] = [new TestTransactionStep(), new TestTransactionStep(), new TestTransactionStep()];
66
+ const ctx: TestTransactionContext = { runningTotal: 0, failStep: 2, failRollbackStep: 1 };
67
+ const result: TransactionResult<TestTransactionContext> = await TransactionRatchet.execute(steps, ctx);
68
+ expect(result).not.toBeUndefined();
69
+ expect(result.finalState).toEqual(TransactionFinalState.RollbackFailed);
70
+ expect(result.finalContext.runningTotal).toEqual(0);
71
+ expect(result.errorStep).toEqual(2);
72
+ expect(result.rollbackErrorStep).toEqual(1);
73
+ expect(result.error).not.toBeUndefined();
74
+ expect(result.rollbackError).not.toBeUndefined();
75
+ });
76
+
77
+ test('should run the post-error handler when a failure happens', async () => {
78
+ const steps: TestTransactionStep[] = [new TestTransactionStep(), new TestTransactionStep(), new TestTransactionStep()];
79
+ const ctx: TestTransactionContext = { runningTotal: 0, failStep: 2 };
80
+ const result: TransactionResult<TestTransactionContext> = await TransactionRatchet.execute(steps, ctx, {
81
+ executeAfterRollback: async (res) => {
82
+ res.finalContext.postTxTracker = 1;
83
+ },
84
+ });
85
+ expect(result).not.toBeUndefined();
86
+ expect(result.finalState).toEqual(TransactionFinalState.RolledBack);
87
+ expect(result.errorStep).toEqual(2);
88
+ expect(result.error).not.toBeUndefined();
89
+ expect(result.finalContext.postTxTracker).toEqual(1);
90
+ });
91
+
92
+ test('should run both post-error handler when a failure happens in both error and rollback', async () => {
93
+ const steps: TestTransactionStep[] = [new TestTransactionStep(), new TestTransactionStep(), new TestTransactionStep()];
94
+ const ctx: TestTransactionContext = { runningTotal: 0, failStep: 2, failRollbackStep: 1 };
95
+ const result: TransactionResult<TestTransactionContext> = await TransactionRatchet.execute(steps, ctx, {
96
+ executeAfterRollback: async (res) => {
97
+ res.finalContext.postTxTracker = 1;
98
+ },
99
+ executeAfterRollbackFailure: async (res) => {
100
+ res.finalContext.postTxTracker += 1;
101
+ },
102
+ });
103
+ expect(result).not.toBeUndefined();
104
+ expect(result.finalState).toEqual(TransactionFinalState.RollbackFailed);
105
+ expect(result.errorStep).toEqual(2);
106
+ expect(result.error).not.toBeUndefined();
107
+ expect(result.finalContext.postTxTracker).toEqual(2);
108
+ });
109
+
110
+ test('should still return if a post-tx executor fails', async () => {
111
+ const steps: TestTransactionStep[] = [new TestTransactionStep(), new TestTransactionStep(), new TestTransactionStep()];
112
+ const ctx: TestTransactionContext = { runningTotal: 0, failStep: 2, failRollbackStep: 1 };
113
+ const result: TransactionResult<TestTransactionContext> = await TransactionRatchet.execute(steps, ctx, {
114
+ executeAfterRollback: async (res) => {
115
+ res.finalContext.postTxTracker = 1;
116
+ },
117
+ executeAfterRollbackFailure: async (res) => {
118
+ res.finalContext.postTxTracker += 1;
119
+ throw new Error('Forced tracker failure');
120
+ },
121
+ });
122
+ expect(result).not.toBeUndefined();
123
+ expect(result.finalState).toEqual(TransactionFinalState.RollbackFailed);
124
+ expect(result.errorStep).toEqual(2);
125
+ expect(result.error).not.toBeUndefined();
126
+ expect(result.finalContext.postTxTracker).toEqual(2);
127
+ });
128
+
129
+ test('should work with transaction configuration that only provides read log level access', async () => {
130
+ const steps: TestTransactionStep[] = [new TestTransactionStep(), new TestTransactionStep(), new TestTransactionStep()];
131
+ const ctx: TestTransactionContext = { runningTotal: 0, failStep: 2, failRollbackStep: 1 };
132
+ const result: TransactionResult<TestTransactionContext> = await TransactionRatchet.execute(steps, ctx, {
133
+ get stepLogLevel(): LoggerLevelName {
134
+ return LoggerLevelName.info;
135
+ },
136
+ executeAfterRollback: async (res) => {
137
+ res.finalContext.postTxTracker = 1;
138
+ },
139
+ executeAfterRollbackFailure: async (res) => {
140
+ res.finalContext.postTxTracker += 1;
141
+ throw new Error('Forced tracker failure');
142
+ },
143
+ });
144
+ expect(result).not.toBeUndefined();
145
+ expect(result.finalState).toEqual(TransactionFinalState.RollbackFailed);
146
+ expect(result.errorStep).toEqual(2);
147
+ expect(result.error).not.toBeUndefined();
148
+ expect(result.finalContext.postTxTracker).toEqual(2);
149
+ });
150
+ });