@controlium/utils 0.0.0
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/LICENSE +661 -0
- package/README.md +10 -0
- package/dist/cjs/detokeniser/detokeniser.js +1135 -0
- package/dist/cjs/index.js +14 -0
- package/dist/cjs/jsonUtils/jsonUtils.js +460 -0
- package/dist/cjs/logger/logger.js +863 -0
- package/dist/cjs/logger/logger.spec.js +875 -0
- package/dist/cjs/logger/types.js +2 -0
- package/dist/cjs/stringUtils/stringUtils.js +294 -0
- package/dist/cjs/utils/utils.js +1050 -0
- package/dist/esm/detokeniser/detokeniser.js +1131 -0
- package/dist/esm/index.js +6 -0
- package/dist/esm/jsonUtils/jsonUtils.js +420 -0
- package/dist/esm/logger/logger.js +859 -0
- package/dist/esm/logger/logger.spec.js +873 -0
- package/dist/esm/logger/types.js +1 -0
- package/dist/esm/stringUtils/stringUtils.js +290 -0
- package/dist/esm/utils/utils.js +1043 -0
- package/dist/types/detokeniser/detokeniser.d.ts +402 -0
- package/dist/types/index.d.ts +18 -0
- package/dist/types/jsonUtils/jsonUtils.d.ts +196 -0
- package/dist/types/logger/logger.d.ts +388 -0
- package/dist/types/logger/logger.spec.d.ts +1 -0
- package/dist/types/logger/types.d.ts +235 -0
- package/dist/types/stringUtils/stringUtils.d.ts +129 -0
- package/dist/types/utils/utils.d.ts +450 -0
- package/package.json +110 -0
|
@@ -0,0 +1,1131 @@
|
|
|
1
|
+
import { addDays, addHours, addMinutes, addMonths, addYears, format, secondsToHours, secondsToMinutes } from "date-fns";
|
|
2
|
+
import { formatInTimeZone, getTimezoneOffset } from "date-fns-tz";
|
|
3
|
+
import { JsonUtils, Log, LogLevels, StringUtils, Utils } from "../index";
|
|
4
|
+
// ─── PublicHolidays stub ─────────────────────────────────────────────────────
|
|
5
|
+
// TODO: Replace with a real import when a PublicHolidays module is provided by the consumer.
|
|
6
|
+
// The region string is an opaque identifier passed through by the caller (e.g. "us-east", "eu-london").
|
|
7
|
+
// eslint-disable-next-line @typescript-eslint/no-namespace
|
|
8
|
+
var PublicHolidays;
|
|
9
|
+
(function (PublicHolidays) {
|
|
10
|
+
function getIANAZone(_region) { throw new Error("PublicHolidays not yet implemented"); }
|
|
11
|
+
PublicHolidays.getIANAZone = getIANAZone;
|
|
12
|
+
async function isDatePublicHoliday(_date, _region) { throw new Error("PublicHolidays not yet implemented"); }
|
|
13
|
+
PublicHolidays.isDatePublicHoliday = isDatePublicHoliday;
|
|
14
|
+
async function getFirstPublicHolidayBetweenDates(_start, _end, _region) { throw new Error("PublicHolidays not yet implemented"); }
|
|
15
|
+
PublicHolidays.getFirstPublicHolidayBetweenDates = getFirstPublicHolidayBetweenDates;
|
|
16
|
+
})(PublicHolidays || (PublicHolidays = {}));
|
|
17
|
+
// ─── MockUtils stub ───────────────────────────────────────────────────────────
|
|
18
|
+
// TODO: Replace with real import when MockUtils is brought into the package
|
|
19
|
+
const MockUtils = {
|
|
20
|
+
interceptedRequests: undefined,
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Processes strings containing tokens in the format `[[tokenType|expression|format]]`, replacing each
|
|
24
|
+
* token with its resolved value. Tokens can be nested — the innermost is always resolved first.
|
|
25
|
+
*
|
|
26
|
+
* ## Token syntax
|
|
27
|
+
* ```
|
|
28
|
+
* [[tokenType|expression|format]]
|
|
29
|
+
* ```
|
|
30
|
+
* - **tokenType** — identifies the built-in handler or is matched against registered callbacks
|
|
31
|
+
* - **expression** — what to compute (type-specific)
|
|
32
|
+
* - **format** — how to format the result (type-specific, often optional)
|
|
33
|
+
*
|
|
34
|
+
* The delimiter `|` and the `[[` / `]]` endstops are configurable via {@link Detokeniser.delimiter} and
|
|
35
|
+
* {@link Detokeniser.tokenStartEndChars}, but the defaults cover the vast majority of use cases.
|
|
36
|
+
*
|
|
37
|
+
* ---
|
|
38
|
+
* ## Built-in token types
|
|
39
|
+
*
|
|
40
|
+
* ### `random` — random data
|
|
41
|
+
* | Expression | Format | Example | Sample output |
|
|
42
|
+
* |---|---|---|---|
|
|
43
|
+
* | `digits` | count | `[[random\|digits\|6]]` | `482910` |
|
|
44
|
+
* | `letters` | count | `[[random\|letters\|4]]` | `xKpQ` |
|
|
45
|
+
* | `lowercaseletters` | count | `[[random\|lowercaseletters\|4]]` | `xkpq` |
|
|
46
|
+
* | `uppercaseletters` | count | `[[random\|uppercaseletters\|4]]` | `XKPQ` |
|
|
47
|
+
* | `alphanumerics` | count | `[[random\|alphanumerics\|8]]` | `a3Kx92Zp` |
|
|
48
|
+
* | `from(<chars>)` | count | `[[random\|from(aeiou)\|3]]` | `ioa` |
|
|
49
|
+
* | `float(min,max)` | decimal places | `[[random\|float(1.5,3.7)\|2]]` | `2.83` |
|
|
50
|
+
* | `date(fromEpoch,toEpoch)` | date format | `[[random\|date(0,1700000000000)\|yyyy-MM-dd]]` | `1994-07-12` |
|
|
51
|
+
*
|
|
52
|
+
* ### `date` / `date(<state>)` — date and time
|
|
53
|
+
* Format is a [date-fns format string](https://date-fns.org/docs/format), or the special values
|
|
54
|
+
* `epoch` (milliseconds since 1970-01-01) or `second-epoch`.
|
|
55
|
+
*
|
|
56
|
+
* | Expression | Example | Sample output |
|
|
57
|
+
* |---|---|---|
|
|
58
|
+
* | `today` / `now` | `[[date\|today\|dd-MM-yyyy]]` | `01-04-2026` |
|
|
59
|
+
* | `yesterday` | `[[date\|yesterday\|dd-MM-yyyy]]` | `31-03-2026` |
|
|
60
|
+
* | `tomorrow` | `[[date\|tomorrow\|dd-MM-yyyy]]` | `02-04-2026` |
|
|
61
|
+
* | `addYears(n)` | `[[date\|addYears(-1)\|yyyy]]` | `2025` |
|
|
62
|
+
* | `addMonths(n)` | `[[date\|addMonths(3)\|MMM yyyy]]` | `Jul 2026` |
|
|
63
|
+
* | `addDays(n)` | `[[date\|addDays(5)\|dd-MM-yyyy]]` | `06-04-2026` |
|
|
64
|
+
* | `addHours(n)` | `[[date\|addHours(2)\|HH:mm]]` | `14:30` |
|
|
65
|
+
* | `addMinutes(n)` | `[[date\|addMinutes(30)\|HH:mm]]` | `13:00` |
|
|
66
|
+
* | `random(fromEpoch,toEpoch)` | date format | `[[date\|random(0,1700000000000)\|yyyy-MM-dd]]` | random date |
|
|
67
|
+
* | `followingDay(epoch,dayName)` | date format | `[[date\|followingDay(1711929600000,wednesday)\|dd-MM-yyyy]]` | next Wednesday |
|
|
68
|
+
* | `yyyy-MM-dd` _(fixed date)_ | date format | `[[date\|2026-06-15\|EEEE]]` | `Monday` |
|
|
69
|
+
* | `timezoneOffset` _(with region)_ | _(none)_ | `[[date(us-east)\|timezoneOffset]]` | `-0500` |
|
|
70
|
+
*
|
|
71
|
+
* A region qualifier routes date formatting through the region's IANA timezone (resolved by the
|
|
72
|
+
* consumer-provided `PublicHolidays` module):
|
|
73
|
+
* ```
|
|
74
|
+
* [[date(eu-london)|today|HH:mm]] ← formatted in London time
|
|
75
|
+
* [[date(us-east)|addDays(1)|dd-MM-yyyy]]
|
|
76
|
+
* ```
|
|
77
|
+
*
|
|
78
|
+
* The following expressions are **async-only** — use {@link Detokeniser.doAsync} with a region qualifier:
|
|
79
|
+
* - `addWorkingDays(n)` — adds business days, skipping weekends and public holidays
|
|
80
|
+
* - `followingWorkingDay(epoch,dayName)` — as `followingDay` but skips weekends and public holidays
|
|
81
|
+
* - `nextPublicHoliday(fromEpoch,maxDays)` — next public holiday within a search window
|
|
82
|
+
*
|
|
83
|
+
* ```typescript
|
|
84
|
+
* await Detokeniser.doAsync('[[date(us-east)|addWorkingDays(5)|dd-MM-yyyy]]');
|
|
85
|
+
* await Detokeniser.doAsync('[[date(eu-london)|nextPublicHoliday([[date|today|epoch]],90)|dd-MM-yyyy]]');
|
|
86
|
+
* ```
|
|
87
|
+
*
|
|
88
|
+
* ### `setting` — retrieve a configured value
|
|
89
|
+
* Parameters are given as JSON key/value pairs after the delimiter. Resolution order:
|
|
90
|
+
* process env → npm config → context parameters → default value.
|
|
91
|
+
* ```
|
|
92
|
+
* [[setting|processEnvName: "MY_VAR"]]
|
|
93
|
+
* [[setting|processEnvName: "MY_VAR", defaultValue: "fallback"]]
|
|
94
|
+
* [[setting|npmPackageConfigName: "myconfig"]]
|
|
95
|
+
* [[setting|profileParameterName: "myParam", defaultValue: "none"]]
|
|
96
|
+
* ```
|
|
97
|
+
*
|
|
98
|
+
* ### `base64` — encode or decode
|
|
99
|
+
* ```
|
|
100
|
+
* [[base64|encode|Hello World]] → SGVsbG8gV29ybGQ=
|
|
101
|
+
* [[base64|decode|SGVsbG8gV29ybGQ=]] → Hello World
|
|
102
|
+
* ```
|
|
103
|
+
*
|
|
104
|
+
* ### `jwt` — generate a signed JWT
|
|
105
|
+
* ```
|
|
106
|
+
* [[jwt|{"sub":"1234","name":"Test"}|MySecret]]
|
|
107
|
+
* [[jwt|{"sub":"1234"}|MySecret|{"algorithm":"HS256"}]]
|
|
108
|
+
* ```
|
|
109
|
+
*
|
|
110
|
+
* ### `mockintercepts` — harvest a value from intercepted mock requests
|
|
111
|
+
* ```
|
|
112
|
+
* [[mockintercepts|$.requests[0].body.userId]]
|
|
113
|
+
* ```
|
|
114
|
+
*
|
|
115
|
+
* ---
|
|
116
|
+
* ## Nesting
|
|
117
|
+
* Tokens are resolved innermost-first, so nested tokens compose naturally:
|
|
118
|
+
* ```typescript
|
|
119
|
+
* // Date N random days from now, where N is itself a random digit
|
|
120
|
+
* Detokeniser.do('[[date|addDays([[random|digits|1]])|dd-MM-yyyy]]');
|
|
121
|
+
*
|
|
122
|
+
* // Next NSW public holiday from today (async — needs doAsync)
|
|
123
|
+
* await Detokeniser.doAsync('[[date(eu-london)|nextPublicHoliday([[date|today|epoch]],90)|dd-MM-yyyy]]');
|
|
124
|
+
* ```
|
|
125
|
+
*
|
|
126
|
+
* ---
|
|
127
|
+
* ## Escaping
|
|
128
|
+
* The escape character is `/` (configurable via `EscapeChar`). Escaping applies both inside and
|
|
129
|
+
* outside tokens. A double escape `//` produces a literal `/`.
|
|
130
|
+
*
|
|
131
|
+
* | Input | Output | Notes |
|
|
132
|
+
* |---|---|---|
|
|
133
|
+
* | `/[[` | `[[` | Literal `[[` — not treated as token start |
|
|
134
|
+
* | `/]]` | `]]` | Literal `]]` — not treated as token end |
|
|
135
|
+
* | `//` | `/` | Literal escape char |
|
|
136
|
+
* | `[[random\|from(xyz/[[)\|3]]` | 3 chars from `xyz[[` | `/[[` inside `from()` = `/[` (→`[`) + `[` = two `[` (higher weight) |
|
|
137
|
+
* | `[[random\|from(abc/))\|2]]` | 2 chars from `abc)` | `/)` inside `from()` = literal `)` |
|
|
138
|
+
*
|
|
139
|
+
* ---
|
|
140
|
+
* ## Extending with callbacks
|
|
141
|
+
* Custom token types are registered via {@link Detokeniser.addCallbackSync} (for sync processing) or
|
|
142
|
+
* {@link Detokeniser.addCallbackAsync} (for async processing). Callbacks are tried in registration
|
|
143
|
+
* order; return `undefined` to pass to the next callback. If all callbacks return `undefined` and no
|
|
144
|
+
* built-in handler matched, an error is thrown.
|
|
145
|
+
*
|
|
146
|
+
* @see {@link Detokeniser.addCallbackSync}
|
|
147
|
+
* @see {@link Detokeniser.addCallbackAsync}
|
|
148
|
+
* @see {@link Detokeniser.do}
|
|
149
|
+
* @see {@link Detokeniser.doAsync}
|
|
150
|
+
*/
|
|
151
|
+
export class Detokeniser {
|
|
152
|
+
/**
|
|
153
|
+
* Gets the current single-character delimiter used to separate token parts.
|
|
154
|
+
* Default: `|`
|
|
155
|
+
*/
|
|
156
|
+
static get delimiter() {
|
|
157
|
+
return this._delimiter;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Sets the single-character delimiter used to separate token parts.
|
|
161
|
+
* The new value applies to all subsequent {@link Detokeniser.do} / {@link Detokeniser.doAsync} calls.
|
|
162
|
+
* @param newDelimiter - Exactly one character
|
|
163
|
+
* @throws If `newDelimiter` is not exactly one character
|
|
164
|
+
* @example
|
|
165
|
+
* Detokeniser.delimiter = ':';
|
|
166
|
+
* Detokeniser.do('[[random:digits:6]]'); // → e.g. '482910'
|
|
167
|
+
* Detokeniser.reset(); // restore default '|'
|
|
168
|
+
*/
|
|
169
|
+
static set delimiter(newDelimiter) {
|
|
170
|
+
if (newDelimiter.length !== 1) {
|
|
171
|
+
const errTxt = `Invalid delimiter [${newDelimiter}]. Must be exactly 1 character!`;
|
|
172
|
+
Log.writeLine(LogLevels.Error, errTxt);
|
|
173
|
+
throw new Error(errTxt);
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
this._delimiter = newDelimiter;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Sets the start and end token sequences.
|
|
181
|
+
* @param startEndChars - An even-length string whose first half is the start sequence and second half the end sequence.
|
|
182
|
+
* @example Detokeniser.tokenStartEndChars = "[[]]"; // start = "[[", end = "]]"
|
|
183
|
+
* @remarks Start and end sequences must differ. Minimum total length is 2 (one char each).
|
|
184
|
+
*/
|
|
185
|
+
static set tokenStartEndChars(startEndChars) {
|
|
186
|
+
if (startEndChars.length < 2 || startEndChars.length % 2 !== 0) {
|
|
187
|
+
const errTxt = `Invalid start/end chars [${startEndChars}]. Must be an even number of characters (minimum 2) — first half as start sequence, second half as end sequence!`;
|
|
188
|
+
Log.writeLine(LogLevels.Error, errTxt);
|
|
189
|
+
throw new Error(errTxt);
|
|
190
|
+
}
|
|
191
|
+
const half = startEndChars.length / 2;
|
|
192
|
+
const start = startEndChars.substring(0, half);
|
|
193
|
+
const end = startEndChars.substring(half);
|
|
194
|
+
if (start === end) {
|
|
195
|
+
const errTxt = `Invalid start/end chars — start sequence [${start}] must differ from end sequence [${end}]!`;
|
|
196
|
+
Log.writeLine(LogLevels.Error, errTxt);
|
|
197
|
+
throw new Error(errTxt);
|
|
198
|
+
}
|
|
199
|
+
this._startTokenChar = start;
|
|
200
|
+
this._endTokenChar = end;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Gets the current token start/end sequences concatenated (e.g. `"[[]]"`).
|
|
204
|
+
*/
|
|
205
|
+
static get tokenStartEndChars() {
|
|
206
|
+
return this._startTokenChar + this._endTokenChar;
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Resets the Detokeniser to factory defaults:
|
|
210
|
+
* - Token endstops restored to `[[` / `]]`
|
|
211
|
+
* - Delimiter restored to `|`
|
|
212
|
+
* - Escape char restored to `/`
|
|
213
|
+
* - All registered sync and async callbacks cleared
|
|
214
|
+
*
|
|
215
|
+
* Call this in test teardown to guarantee a clean state between scenarios.
|
|
216
|
+
* @example
|
|
217
|
+
* afterEach(() => Detokeniser.reset());
|
|
218
|
+
*/
|
|
219
|
+
static reset() {
|
|
220
|
+
this._endTokenChar = "]]";
|
|
221
|
+
this._startTokenChar = "[[";
|
|
222
|
+
this.EscapeChar = "/";
|
|
223
|
+
this._delimiter = "|";
|
|
224
|
+
if (this._syncCallbacks) {
|
|
225
|
+
this._syncCallbacks = [];
|
|
226
|
+
}
|
|
227
|
+
if (this._asyncCallbacks) {
|
|
228
|
+
this._asyncCallbacks = [];
|
|
229
|
+
}
|
|
230
|
+
this._asyncCallbacks = undefined;
|
|
231
|
+
this._syncCallbacks = undefined;
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Registers a synchronous custom token handler.
|
|
235
|
+
*
|
|
236
|
+
* When {@link Detokeniser.do} encounters a token not handled by the built-in set, it invokes each
|
|
237
|
+
* registered sync callback in registration order. The first to return a non-`undefined` string wins.
|
|
238
|
+
* Return `undefined` to pass to the next callback. If all callbacks return `undefined`, an error is thrown.
|
|
239
|
+
*
|
|
240
|
+
* @param callback - `(delimiter: string, token: string) => string | undefined`
|
|
241
|
+
* - `delimiter` — current delimiter character (default `|`)
|
|
242
|
+
* - `token` — full token body without `[[` / `]]`, e.g. `"mytype|arg1|arg2"`
|
|
243
|
+
*
|
|
244
|
+
* @example
|
|
245
|
+
* // Handle [[env|VAR_NAME]] tokens
|
|
246
|
+
* Detokeniser.addCallbackSync((delimiter, token) => {
|
|
247
|
+
* const [type, name] = token.split(delimiter);
|
|
248
|
+
* if (type.toLowerCase() !== 'env') return undefined;
|
|
249
|
+
* return process.env[name] ?? '';
|
|
250
|
+
* });
|
|
251
|
+
* Detokeniser.do('Path: [[env|HOME]]'); // → 'Path: /home/user'
|
|
252
|
+
*
|
|
253
|
+
* @example
|
|
254
|
+
* // Multiple callbacks — each handles one type, passes on the rest
|
|
255
|
+
* Detokeniser.addCallbackSync((delimiter, token) => {
|
|
256
|
+
* const [type, value] = token.split(delimiter);
|
|
257
|
+
* if (type === 'upper') return value.toUpperCase();
|
|
258
|
+
* return undefined;
|
|
259
|
+
* });
|
|
260
|
+
* Detokeniser.addCallbackSync((delimiter, token) => {
|
|
261
|
+
* const [type, value] = token.split(delimiter);
|
|
262
|
+
* if (type === 'lower') return value.toLowerCase();
|
|
263
|
+
* return undefined;
|
|
264
|
+
* });
|
|
265
|
+
* Detokeniser.do('[[upper|hello]] [[lower|WORLD]]'); // → 'HELLO world'
|
|
266
|
+
*
|
|
267
|
+
* @see {@link Detokeniser.resetSyncCallbacks} to remove all sync callbacks
|
|
268
|
+
* @see {@link Detokeniser.addCallbackAsync} for async token handlers
|
|
269
|
+
*/
|
|
270
|
+
static addCallbackSync(callback) {
|
|
271
|
+
if (!this._syncCallbacks) {
|
|
272
|
+
this._syncCallbacks = new Array();
|
|
273
|
+
}
|
|
274
|
+
this._syncCallbacks.push(callback);
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Registers an asynchronous custom token handler.
|
|
278
|
+
*
|
|
279
|
+
* Works identically to {@link Detokeniser.addCallbackSync} but is invoked by {@link Detokeniser.doAsync}.
|
|
280
|
+
* Async callbacks are tried in registration order; the first to return a non-`undefined` value wins.
|
|
281
|
+
* Return `undefined` to pass to the next callback.
|
|
282
|
+
*
|
|
283
|
+
* @param asyncCallback - `(delimiter: string, token: string) => Promise<string | undefined>`
|
|
284
|
+
* - `delimiter` — current delimiter character (default `|`)
|
|
285
|
+
* - `token` — full token body without `[[` / `]]`, e.g. `"mytype|arg1|arg2"`
|
|
286
|
+
*
|
|
287
|
+
* @example
|
|
288
|
+
* // Handle [[db|table|column|whereClause]] tokens
|
|
289
|
+
* Detokeniser.addCallbackAsync(async (delimiter, token) => {
|
|
290
|
+
* const [type, table, column, where] = token.split(delimiter);
|
|
291
|
+
* if (type.toLowerCase() !== 'db') return undefined;
|
|
292
|
+
* const row = await db.query(`SELECT ${column} FROM ${table} WHERE ${where} LIMIT 1`);
|
|
293
|
+
* return String(row[column]);
|
|
294
|
+
* });
|
|
295
|
+
* const result = await Detokeniser.doAsync('ID: [[db|users|id|active=1]]');
|
|
296
|
+
*
|
|
297
|
+
* @example
|
|
298
|
+
* // Combine with nested tokens — inner tokens resolve before the callback is called
|
|
299
|
+
* Detokeniser.addCallbackAsync(async (delimiter, token) => {
|
|
300
|
+
* const [type, key] = token.split(delimiter);
|
|
301
|
+
* if (type !== 'cache') return undefined;
|
|
302
|
+
* return await redis.get(key);
|
|
303
|
+
* });
|
|
304
|
+
* // [[random|digits|8]] resolves first, then [[cache|...]] receives the result
|
|
305
|
+
* await Detokeniser.doAsync('Val: [[cache|prefix-[[random|digits|8]]]]');
|
|
306
|
+
*
|
|
307
|
+
* @see {@link Detokeniser.resetAsyncCallbacks} to remove all async callbacks
|
|
308
|
+
* @see {@link Detokeniser.addCallbackSync} for synchronous token handlers
|
|
309
|
+
*/
|
|
310
|
+
static addCallbackAsync(asyncCallback) {
|
|
311
|
+
if (!this._asyncCallbacks) {
|
|
312
|
+
this._asyncCallbacks = new Array();
|
|
313
|
+
}
|
|
314
|
+
this._asyncCallbacks?.push(asyncCallback);
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Removes all registered sync callbacks. Built-in token handlers are unaffected.
|
|
318
|
+
* Use between tests or scenarios to ensure callback isolation.
|
|
319
|
+
* @see {@link Detokeniser.reset} to also clear async callbacks and restore all defaults
|
|
320
|
+
*/
|
|
321
|
+
static resetSyncCallbacks() {
|
|
322
|
+
if (this._syncCallbacks) {
|
|
323
|
+
this._syncCallbacks = [];
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Removes all registered async callbacks. Built-in token handlers are unaffected.
|
|
328
|
+
* Use between tests or scenarios to ensure callback isolation.
|
|
329
|
+
* @see {@link Detokeniser.reset} to also clear sync callbacks and restore all defaults
|
|
330
|
+
*/
|
|
331
|
+
static resetAsyncCallbacks() {
|
|
332
|
+
if (this._asyncCallbacks) {
|
|
333
|
+
this._asyncCallbacks = [];
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Synchronously resolves all tokens in the given string and returns the result.
|
|
338
|
+
*
|
|
339
|
+
* Tokens are resolved innermost-first. Registered sync callbacks are invoked for token types not
|
|
340
|
+
* handled by the built-in set. Use {@link Detokeniser.doAsync} if you need async callbacks or any
|
|
341
|
+
* of the async-only date expressions (`addWorkingDays`, `followingWorkingDay`, `nextPublicHoliday`).
|
|
342
|
+
*
|
|
343
|
+
* @param tokenisedString - String potentially containing `[[...]]` tokens
|
|
344
|
+
* @param options - Optional processing options
|
|
345
|
+
* @returns The input string with all tokens replaced by their resolved values
|
|
346
|
+
* @throws If any token is malformed, unsupported, or a callback throws
|
|
347
|
+
*
|
|
348
|
+
* @example
|
|
349
|
+
* Detokeniser.do('Ref-[[random|digits|6]]'); // → e.g. 'Ref-482910'
|
|
350
|
+
* Detokeniser.do('Expires [[date|addDays(30)|dd/MM/yyyy]]'); // → e.g. 'Expires 01/05/2026'
|
|
351
|
+
* Detokeniser.do('[[random|uppercaseletters|3]]-[[random|digits|4]]'); // → e.g. 'XKP-7391'
|
|
352
|
+
*
|
|
353
|
+
* @example
|
|
354
|
+
* // Nested tokens — innermost resolved first
|
|
355
|
+
* Detokeniser.do('[[date|addDays([[random|digits|1]])|dd-MM-yyyy]]');
|
|
356
|
+
*
|
|
357
|
+
* @example
|
|
358
|
+
* // Context parameters for [[setting|...]] tokens
|
|
359
|
+
* Detokeniser.do('Hello [[setting|profileParameterName: "username"]]', {
|
|
360
|
+
* contextParameters: { username: 'Alice' }
|
|
361
|
+
* }); // → 'Hello Alice'
|
|
362
|
+
*
|
|
363
|
+
* @example
|
|
364
|
+
* // Escaping — produce literal [[ / ]] in output
|
|
365
|
+
* Detokeniser.do('Press /[[Enter/]] to continue'); // → 'Press [[Enter]] to continue'
|
|
366
|
+
*/
|
|
367
|
+
static do(tokenisedString, options = {}) {
|
|
368
|
+
let deEscape = true;
|
|
369
|
+
try {
|
|
370
|
+
const runningString = this.doPreamble(tokenisedString);
|
|
371
|
+
// Loop until last token find found no tokens
|
|
372
|
+
while (runningString.currentToken.hasToken) {
|
|
373
|
+
// Process the last found token, prepend it to the text after the last found token then find any token in the resulting string
|
|
374
|
+
runningString.currentToken = new InnermostToken(this.doToken(runningString.currentToken.childToken.substring(this._startTokenChar.length), options) + runningString.currentToken.postamble, this._startTokenChar, this._endTokenChar, this.EscapeChar);
|
|
375
|
+
// Concatinate the last found tokens preable (or full text if none found) to the built string and recursivley call self to ensure full token resolution
|
|
376
|
+
runningString.outputString = this.do(runningString.outputString + runningString.currentToken.preamble);
|
|
377
|
+
deEscape = false;
|
|
378
|
+
}
|
|
379
|
+
//
|
|
380
|
+
// Okay, so there is a bug here. But it shall remain unfixed as it requires time.
|
|
381
|
+
//
|
|
382
|
+
// The bug is that is the resolved token string contains a sequence of characters that makes doDeEscapesIfRequired think it sees an escaped special char
|
|
383
|
+
// the it will de-escape it!! However, probablity of incidence is currently low. When/If it does happen (impact could be high!) it can then be fixed.
|
|
384
|
+
//
|
|
385
|
+
// IE.
|
|
386
|
+
// const encoded = Buffer.from('this/>escaped').toString('base64');
|
|
387
|
+
// const decoded = Detokeniser.do(`<base64;decode;${encoded}>`);
|
|
388
|
+
//
|
|
389
|
+
// decoded would now be "this>escaped". Should be "this/>escaped".
|
|
390
|
+
//
|
|
391
|
+
runningString.outputString = this.doDeEscapesIfRequired(tokenisedString, deEscape, runningString.outputString);
|
|
392
|
+
return runningString.outputString;
|
|
393
|
+
}
|
|
394
|
+
catch (err) {
|
|
395
|
+
const errText = `Error processing [${tokenisedString}]: ${typeof err === "string" ? err : err instanceof Error ? err.message : "<unknown>"}`;
|
|
396
|
+
Log.writeLine(LogLevels.Error, errText);
|
|
397
|
+
throw Error(errText);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* Asynchronously resolves all tokens in the given string and returns a Promise of the result.
|
|
402
|
+
*
|
|
403
|
+
* Functionally equivalent to {@link Detokeniser.do} but additionally supports:
|
|
404
|
+
* - Async callbacks registered via {@link Detokeniser.addCallbackAsync}
|
|
405
|
+
* - Async-only date expressions: `addWorkingDays`, `followingWorkingDay`, `nextPublicHoliday`
|
|
406
|
+
*
|
|
407
|
+
* Note: sync callbacks registered via {@link Detokeniser.addCallbackSync} are **not** invoked
|
|
408
|
+
* during async processing — re-register them with {@link Detokeniser.addCallbackAsync} if needed.
|
|
409
|
+
*
|
|
410
|
+
* @param tokenisedString - String potentially containing `[[...]]` tokens
|
|
411
|
+
* @returns Promise resolving to the input string with all tokens replaced
|
|
412
|
+
* @throws If any token is malformed, unsupported, or a callback throws
|
|
413
|
+
*
|
|
414
|
+
* @example
|
|
415
|
+
* // Async-only date expressions require doAsync and a region qualifier
|
|
416
|
+
* await Detokeniser.doAsync('[[date(us-east)|addWorkingDays(5)|dd-MM-yyyy]]');
|
|
417
|
+
* await Detokeniser.doAsync('[[date(eu-london)|nextPublicHoliday([[date|today|epoch]],90)|dd-MM-yyyy]]');
|
|
418
|
+
*
|
|
419
|
+
* @example
|
|
420
|
+
* // Async callback for database-driven tokens
|
|
421
|
+
* Detokeniser.addCallbackAsync(async (delim, token) => {
|
|
422
|
+
* const [type, key] = token.split(delim);
|
|
423
|
+
* if (type !== 'db') return undefined;
|
|
424
|
+
* return await fetchFromDatabase(key);
|
|
425
|
+
* });
|
|
426
|
+
* await Detokeniser.doAsync('User: [[db|users.name.first]]');
|
|
427
|
+
*/
|
|
428
|
+
static async doAsync(tokenisedString) {
|
|
429
|
+
let deEscape = true;
|
|
430
|
+
try {
|
|
431
|
+
const runningString = this.doPreamble(tokenisedString);
|
|
432
|
+
// Loop until last token find found no tokens
|
|
433
|
+
while (runningString.currentToken.hasToken) {
|
|
434
|
+
// Process the last found token, prepend it to the text after the last found token then find any token in the resulting string
|
|
435
|
+
runningString.currentToken = new InnermostToken((await this.asyncDoToken(runningString.currentToken.childToken.substring(this._startTokenChar.length))) +
|
|
436
|
+
runningString.currentToken.postamble, this._startTokenChar, this._endTokenChar, this.EscapeChar);
|
|
437
|
+
const logToken = runningString.currentToken.hasToken ? runningString.currentToken.childToken : '<No Token>';
|
|
438
|
+
Log.writeLine(LogLevels.FrameworkDebug, `Preamble:[${runningString.currentToken.preamble}] Token:[${logToken}] Postamble:[${runningString.currentToken.postamble}]`);
|
|
439
|
+
// Concatinate the last found tokens preable (or full text if none found) to the built string and recursivley call self to ensure full token resolution
|
|
440
|
+
runningString.outputString = await this.doAsync(runningString.outputString + runningString.currentToken.preamble);
|
|
441
|
+
deEscape = false;
|
|
442
|
+
}
|
|
443
|
+
runningString.outputString = this.doDeEscapesIfRequired(tokenisedString, deEscape, runningString.outputString);
|
|
444
|
+
return runningString.outputString;
|
|
445
|
+
}
|
|
446
|
+
catch (err) {
|
|
447
|
+
const errText = `Error processing [${tokenisedString}]: ${typeof err === "string" ? err : err instanceof Error ? err.message : "<unknown>"}`;
|
|
448
|
+
Log.writeLine(LogLevels.Error, errText);
|
|
449
|
+
throw Error(errText);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
static doDeEscapesIfRequired(tokenisedString, deEscape, stringToProcess) {
|
|
453
|
+
// After all tokens are resolved, remove escaping from any chars that were escaped to prevent token recognition.
|
|
454
|
+
// Uses a 3-pass approach to correctly handle escaped escape chars (e.g. //) adjacent to other escapes (e.g. /[):
|
|
455
|
+
// Pass 1: replace // with \x00 placeholder
|
|
456
|
+
// Pass 2: de-escape each char that makes up the start/end token sequences
|
|
457
|
+
// Pass 3: restore \x00 back to the escape char
|
|
458
|
+
let processedString = stringToProcess;
|
|
459
|
+
if (deEscape && !StringUtils.isBlank(processedString)) {
|
|
460
|
+
const doubleEscapes = this.EscapeChar + this.EscapeChar;
|
|
461
|
+
processedString = StringUtils.replaceAll(processedString, doubleEscapes, "\x00");
|
|
462
|
+
const tokenChars = new Set([...this._startTokenChar, ...this._endTokenChar]);
|
|
463
|
+
for (const char of tokenChars) {
|
|
464
|
+
processedString = StringUtils.replaceAll(processedString, this.EscapeChar + char, char);
|
|
465
|
+
}
|
|
466
|
+
processedString = StringUtils.replaceAll(processedString, "\x00", this.EscapeChar);
|
|
467
|
+
}
|
|
468
|
+
if (tokenisedString !== processedString) {
|
|
469
|
+
Log.writeLine(LogLevels.FrameworkDebug, `Processed [${tokenisedString}]\nto [${processedString}]`);
|
|
470
|
+
}
|
|
471
|
+
return processedString;
|
|
472
|
+
}
|
|
473
|
+
static doPreamble(tokenisedString) {
|
|
474
|
+
// Drill down to the left-most deepest (if nested) token
|
|
475
|
+
const token = new InnermostToken(tokenisedString, this._startTokenChar, this._endTokenChar, this.EscapeChar);
|
|
476
|
+
// Get all text to the left of any token found in the string passed in. If no token was found Preamble will contain all text from passed string
|
|
477
|
+
const outputString = token.preamble;
|
|
478
|
+
return { currentToken: token, outputString };
|
|
479
|
+
}
|
|
480
|
+
static doTokenMain(token, options = {}) {
|
|
481
|
+
const doTokenReturn = {};
|
|
482
|
+
doTokenReturn.processedToken = undefined;
|
|
483
|
+
if (!token || token === "") {
|
|
484
|
+
throw new Error("Empty token! Token must be populated.");
|
|
485
|
+
}
|
|
486
|
+
const [tokenName, postAmble] = StringUtils.splitRemaining(token, this._delimiter, 2);
|
|
487
|
+
const loweredTokenName = tokenName.toLowerCase().trim();
|
|
488
|
+
const postAmbleDeEscaped = this.doDeEscapesIfRequired(postAmble, true, postAmble);
|
|
489
|
+
if (typeof doTokenReturn.processedToken === "undefined") {
|
|
490
|
+
switch (loweredTokenName) {
|
|
491
|
+
case "random":
|
|
492
|
+
if (StringUtils.isBlank(postAmbleDeEscaped))
|
|
493
|
+
throw new Error(`Random token [${token}] needs at least 2 parts (IE. {{random;type[;<length>]}} etc.)`);
|
|
494
|
+
doTokenReturn.processedToken = this.doRandomToken(postAmbleDeEscaped);
|
|
495
|
+
break;
|
|
496
|
+
case "setting":
|
|
497
|
+
{
|
|
498
|
+
if (StringUtils.isBlank(postAmbleDeEscaped))
|
|
499
|
+
throw new Error(`Setting token [${token}] needs at least 2 parts (IE. {{setting;processEnvName: "TEST_LOG_TO_CONSOLE")`);
|
|
500
|
+
doTokenReturn.processedToken = this.doSettingToken(postAmbleDeEscaped, options);
|
|
501
|
+
break;
|
|
502
|
+
}
|
|
503
|
+
case "mockintercepts":
|
|
504
|
+
if (StringUtils.isBlank(postAmbleDeEscaped))
|
|
505
|
+
throw new Error(`Request token [${token}] needs 2 parts {{mockintercepts;JSONPath}}`);
|
|
506
|
+
doTokenReturn.processedToken = this.doMockRequests(postAmbleDeEscaped);
|
|
507
|
+
break;
|
|
508
|
+
case "jwt":
|
|
509
|
+
if (StringUtils.isBlank(postAmbleDeEscaped))
|
|
510
|
+
throw new Error(`JWT token [${token}] needs at least 2 parts (IE. {{jwt;payload[;signature[;options]]}} etc.)`);
|
|
511
|
+
doTokenReturn.processedToken = this.doJWTToken(postAmbleDeEscaped);
|
|
512
|
+
break;
|
|
513
|
+
case "base64":
|
|
514
|
+
// aha, we actuall need three parts <base64;encode|decode;<string>>
|
|
515
|
+
if (!StringUtils.isBlank(postAmbleDeEscaped)) {
|
|
516
|
+
const [direction, value] = StringUtils.splitRemaining(postAmbleDeEscaped, this._delimiter, 2);
|
|
517
|
+
if (direction == "encode" || direction == "decode") {
|
|
518
|
+
doTokenReturn.processedToken = this.doBase64(value, direction);
|
|
519
|
+
break;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
throw new Error(`BASE64 token [${token}] needs 3 parts (IE. {{base64;encode|decode;value}})`);
|
|
523
|
+
default:
|
|
524
|
+
if (loweredTokenName.startsWith("date")) {
|
|
525
|
+
doTokenReturn.tokenBodyIfDateToken = postAmbleDeEscaped;
|
|
526
|
+
if (loweredTokenName[4] === "(" && loweredTokenName.endsWith(")")) {
|
|
527
|
+
if (StringUtils.isBlank(postAmbleDeEscaped)) {
|
|
528
|
+
throw new Error(`Date token [${token}] needs 2 or 3 parts {{date(<state>);<offset>;<format>}} or {{date;timezone}}`);
|
|
529
|
+
}
|
|
530
|
+
else {
|
|
531
|
+
const state = StringUtils.trimChar(StringUtils.splitRemaining(tokenName, "(", 2)[1], ")");
|
|
532
|
+
const body = postAmbleDeEscaped;
|
|
533
|
+
Log.writeLine(LogLevels.FrameworkDebug, `Calling doDateToken. State = [${state}]. Body = [${body}]`);
|
|
534
|
+
doTokenReturn.state = state;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
return doTokenReturn;
|
|
541
|
+
}
|
|
542
|
+
static doToken(token, options = {}) {
|
|
543
|
+
const doTokenReturn = this.doTokenMain(token, options);
|
|
544
|
+
let processedToken;
|
|
545
|
+
if (Utils.isNullOrUndefined(doTokenReturn.processedToken) && doTokenReturn.tokenBodyIfDateToken) {
|
|
546
|
+
processedToken = this.doDateToken(doTokenReturn.state, doTokenReturn.tokenBodyIfDateToken);
|
|
547
|
+
}
|
|
548
|
+
else {
|
|
549
|
+
processedToken = doTokenReturn.processedToken;
|
|
550
|
+
}
|
|
551
|
+
//
|
|
552
|
+
// If token still not been processed itterate through the callbacks, breaking as soon as it is processed
|
|
553
|
+
//
|
|
554
|
+
try {
|
|
555
|
+
if (Utils.isNullOrUndefined(processedToken)) {
|
|
556
|
+
if (typeof this._syncCallbacks != "undefined") {
|
|
557
|
+
this._syncCallbacks.every((callback) => {
|
|
558
|
+
processedToken = callback(this._delimiter, token);
|
|
559
|
+
return Utils.isNullOrUndefined(processedToken);
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
catch (e) {
|
|
565
|
+
throw new Error(`Error processing callback for [${token}]: ${e?.message ?? "<Unknown Error>"}}`);
|
|
566
|
+
}
|
|
567
|
+
if (Utils.isNullOrUndefined(processedToken)) {
|
|
568
|
+
throw new Error(`Unsupported token [${StringUtils.splitRemaining(token, this._delimiter, 2)[0]}]`);
|
|
569
|
+
}
|
|
570
|
+
else {
|
|
571
|
+
return processedToken;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
static async asyncDoToken(token, options = {}) {
|
|
575
|
+
const doTokenReturn = this.doTokenMain(token, options);
|
|
576
|
+
let processedToken;
|
|
577
|
+
if (Utils.isNullOrUndefined(doTokenReturn.processedToken) && doTokenReturn.tokenBodyIfDateToken) {
|
|
578
|
+
processedToken = await this.asyncDoDateToken(doTokenReturn.state, doTokenReturn.tokenBodyIfDateToken);
|
|
579
|
+
}
|
|
580
|
+
else {
|
|
581
|
+
processedToken = doTokenReturn.processedToken;
|
|
582
|
+
}
|
|
583
|
+
//
|
|
584
|
+
// If token still not been processed itterate through the callbacks, breaking as soon as it is processed. Dirty
|
|
585
|
+
// ucky loop. But it works.... If you know a sexier way of doing it please change!!
|
|
586
|
+
//
|
|
587
|
+
try {
|
|
588
|
+
if (Utils.isNullOrUndefined(processedToken)) {
|
|
589
|
+
if (typeof this._asyncCallbacks != "undefined") {
|
|
590
|
+
for (let i = 0; i < this._asyncCallbacks.length; i++) {
|
|
591
|
+
processedToken = await this._asyncCallbacks[i](this._delimiter, token);
|
|
592
|
+
if (!Utils.isNullOrUndefined(processedToken))
|
|
593
|
+
break;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
catch (e) {
|
|
599
|
+
Log.writeLine(LogLevels.Error, `Error processing async callback (ignoring and setting response to undefined):${e?.message ?? "<Unknown Error>"}}`);
|
|
600
|
+
processedToken = undefined;
|
|
601
|
+
}
|
|
602
|
+
if (Utils.isNullOrUndefined(processedToken)) {
|
|
603
|
+
throw new Error(`Unsupported token [${StringUtils.splitRemaining(token, this._delimiter, 2)[0]}]`);
|
|
604
|
+
}
|
|
605
|
+
else {
|
|
606
|
+
return processedToken;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
static doSettingToken(tokenBody, options = {}) {
|
|
610
|
+
const bodyJSON = `{${tokenBody}}`;
|
|
611
|
+
if (!JsonUtils.isJson(bodyJSON, true)) {
|
|
612
|
+
const errText = `Token 'Setting'.\nExpected: parameters in name: value format (IE. ${this._startTokenChar}setting;processEnvName: "MY_SETTING"${this._endTokenChar}\nToken was: ${this._startTokenChar}setting;${tokenBody}${this._endTokenChar}\n\nValid Setting parameters include;\n processEnvName - Name of process env variable\n npmPackageConfigName - Name of NPM config var\n profileParameterName - Name of Cucumber profile parameter\n defaultValue - default value if cannot be found`;
|
|
613
|
+
Log.writeLine(LogLevels.Error, errText);
|
|
614
|
+
throw new Error(errText);
|
|
615
|
+
}
|
|
616
|
+
const result = Utils.getSetting(LogLevels.TestInformation, "From Detokeniser", JsonUtils.parse(bodyJSON, true), options.contextParameters);
|
|
617
|
+
if (Utils.isNullOrUndefined(result)) {
|
|
618
|
+
return result;
|
|
619
|
+
}
|
|
620
|
+
else {
|
|
621
|
+
return typeof result === 'object' ? JSON.stringify(result) : String(result);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
/**
|
|
625
|
+
* Base64-encodes or decodes a string.
|
|
626
|
+
*
|
|
627
|
+
* This is the underlying handler for `[[base64|encode|...]]` and `[[base64|decode|...]]` tokens
|
|
628
|
+
* but is also exposed for direct use.
|
|
629
|
+
*
|
|
630
|
+
* @param original - The string to encode or decode
|
|
631
|
+
* @param direction - `"encode"` to base64-encode; `"decode"` to base64-decode
|
|
632
|
+
* @returns The encoded or decoded string
|
|
633
|
+
* @throws If the conversion fails (e.g. invalid base64 input for decode)
|
|
634
|
+
*
|
|
635
|
+
* @example
|
|
636
|
+
* Detokeniser.doBase64('Hello World', 'encode'); // → 'SGVsbG8gV29ybGQ='
|
|
637
|
+
* Detokeniser.doBase64('SGVsbG8gV29ybGQ=', 'decode'); // → 'Hello World'
|
|
638
|
+
*/
|
|
639
|
+
static doBase64(original, direction) {
|
|
640
|
+
try {
|
|
641
|
+
return direction == "encode" ? Buffer.from(original).toString("base64") : Buffer.from(original, "base64").toString();
|
|
642
|
+
}
|
|
643
|
+
catch (err) {
|
|
644
|
+
const errText = `Converting:\n[${original}]\n ${direction == "encode" ? "to" : "from"} base64:\n {${err.message}}`;
|
|
645
|
+
Log.writeLine(LogLevels.Error, errText);
|
|
646
|
+
throw new Error(errText);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
//
|
|
650
|
+
// So, this has been created quickly with little/no checking/testing. Lots and lots of error handling needs adding or testers will get
|
|
651
|
+
// errors/fails they have no idea how to fix!!!!
|
|
652
|
+
//
|
|
653
|
+
static doJWTToken(tokenBody) {
|
|
654
|
+
Log.writeLine(LogLevels.FrameworkDebug, `JWT token [${tokenBody}]`);
|
|
655
|
+
const typeAndLengthOrFormat = StringUtils.splitRemaining(tokenBody, this._delimiter, 3);
|
|
656
|
+
if (typeAndLengthOrFormat.length == 3) {
|
|
657
|
+
return Utils.createJWT(typeAndLengthOrFormat[0], typeAndLengthOrFormat[1], typeAndLengthOrFormat[2]);
|
|
658
|
+
}
|
|
659
|
+
else if (typeAndLengthOrFormat.length == 2) {
|
|
660
|
+
return Utils.createJWT(typeAndLengthOrFormat[0], typeAndLengthOrFormat[1]);
|
|
661
|
+
}
|
|
662
|
+
else {
|
|
663
|
+
return Utils.createJWT(typeAndLengthOrFormat[0], "DummySignature");
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
static doMockRequests(jsonPath) {
|
|
667
|
+
//
|
|
668
|
+
// NOTE. This is MockUtils only at the moment. When playwright is brought in to the library this MUST be made generic so user can harvest from Mock intercepts,
|
|
669
|
+
// whether MSW OR Playwright.
|
|
670
|
+
//
|
|
671
|
+
if (!MockUtils.interceptedRequests || MockUtils.interceptedRequests.length <= 0) {
|
|
672
|
+
const errMessage = "No Mock Intercepted requests to harvest from!?";
|
|
673
|
+
Log.writeLine(LogLevels.Error, errMessage);
|
|
674
|
+
throw new Error(errMessage);
|
|
675
|
+
}
|
|
676
|
+
const jsonProperties = JsonUtils.getPropertiesMatchingPath(MockUtils.interceptedRequests, jsonPath);
|
|
677
|
+
if (jsonProperties.length != 1) {
|
|
678
|
+
throw new Error(`Expected path (${jsonPath}) to match exactly one JSON node. However, path matched ${jsonProperties.length} nodes!`);
|
|
679
|
+
}
|
|
680
|
+
else {
|
|
681
|
+
return jsonProperties[0].value;
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
/**
|
|
685
|
+
* Extracts the content inside the first set of parentheses in `input`, respecting the escape char.
|
|
686
|
+
* An escaped `)` (i.e. `/)`) is treated as a literal `)` and does not end the content.
|
|
687
|
+
* @example parseParenContent("from(abc/))") → "abc)"
|
|
688
|
+
* @example parseParenContent("from(xyz/[[)") → "xyz[[" (with default escape char `/`)
|
|
689
|
+
*/
|
|
690
|
+
static parseParenContent(input) {
|
|
691
|
+
const openParen = input.indexOf("(");
|
|
692
|
+
if (openParen === -1)
|
|
693
|
+
return "";
|
|
694
|
+
let result = "";
|
|
695
|
+
let i = openParen + 1;
|
|
696
|
+
while (i < input.length) {
|
|
697
|
+
if (input[i] === this.EscapeChar && i + 1 < input.length) {
|
|
698
|
+
result += input[i + 1];
|
|
699
|
+
i += 2;
|
|
700
|
+
}
|
|
701
|
+
else if (input[i] === ")") {
|
|
702
|
+
break;
|
|
703
|
+
}
|
|
704
|
+
else {
|
|
705
|
+
result += input[i];
|
|
706
|
+
i++;
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
return result;
|
|
710
|
+
}
|
|
711
|
+
static doRandomToken(tokenBody) {
|
|
712
|
+
const typeAndLengthOrFormat = StringUtils.splitRemaining(tokenBody, this._delimiter, 2);
|
|
713
|
+
let result = "";
|
|
714
|
+
let select = "";
|
|
715
|
+
const verb = typeAndLengthOrFormat[0].toLowerCase().trim();
|
|
716
|
+
if (verb.startsWith("date(")) {
|
|
717
|
+
const randomDate = this.doRandomDate(verb.substring(verb.indexOf("(") + 1, verb.indexOf(")")));
|
|
718
|
+
result = typeAndLengthOrFormat[1].toLowerCase() === "epoch" ? "" + randomDate : format(randomDate, typeAndLengthOrFormat[1]);
|
|
719
|
+
}
|
|
720
|
+
else if (verb.startsWith("float(")) {
|
|
721
|
+
// ToDo: Current format is only for number of decimals. However in future could be precision,decimals if needed (IE. {random,float(4000,8000),3,4} precision 3 decimals 4...), defaulting
|
|
722
|
+
// to decimal places if only a single number given (then change would be non-breaking)....
|
|
723
|
+
if (isNaN(parseInt(typeAndLengthOrFormat[1])))
|
|
724
|
+
throw `Invalid Float format. Expect {{random.float(min;max),<number of decimals>}}. Format was: [${typeAndLengthOrFormat[1]}]`;
|
|
725
|
+
const numberOfDecimalPlaces = parseInt(typeAndLengthOrFormat[1]);
|
|
726
|
+
const powerDPs = Math.pow(10, numberOfDecimalPlaces);
|
|
727
|
+
result = (Math.trunc(this.doRandomFloat(verb.substring(verb.indexOf("(") + 1, verb.indexOf(")"))) * powerDPs) / powerDPs).toFixed(numberOfDecimalPlaces);
|
|
728
|
+
}
|
|
729
|
+
else {
|
|
730
|
+
if (verb.startsWith("from(")) {
|
|
731
|
+
select = this.parseParenContent(typeAndLengthOrFormat[0]);
|
|
732
|
+
}
|
|
733
|
+
else {
|
|
734
|
+
switch (verb) {
|
|
735
|
+
case "letters":
|
|
736
|
+
select = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
|
737
|
+
break;
|
|
738
|
+
case "lowercaseletters":
|
|
739
|
+
select = "abcdefghijklmnopqrstuvwxyz";
|
|
740
|
+
break;
|
|
741
|
+
case "uppercaseletters":
|
|
742
|
+
select = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
|
743
|
+
break;
|
|
744
|
+
case "digits":
|
|
745
|
+
select = "0123456789";
|
|
746
|
+
break;
|
|
747
|
+
case "alphanumerics":
|
|
748
|
+
select = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567890";
|
|
749
|
+
break;
|
|
750
|
+
default:
|
|
751
|
+
throw `Unrecognised random Type [${typeAndLengthOrFormat[0]}] - Expect letters, lowercaseletters, uppercaseletters digits or alphanumerics`;
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
if (isNaN(parseInt(typeAndLengthOrFormat[1])) || parseInt(typeAndLengthOrFormat[1]) < 0)
|
|
755
|
+
throw `Invalid length part in Random token {{random;<type>;<length>}}. Length was: [${typeAndLengthOrFormat[1]}]`;
|
|
756
|
+
for (let count = 0; count < parseInt(typeAndLengthOrFormat[1]); count++) {
|
|
757
|
+
result += select[Utils.getRandomInt(0, select.length - 1)];
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
return result;
|
|
761
|
+
}
|
|
762
|
+
static doDateTokenPreamble(date, state, tokenBody) {
|
|
763
|
+
const offsetAndFormat = StringUtils.splitRemaining(tokenBody, this._delimiter, 2);
|
|
764
|
+
const stateIANAZone = state ? PublicHolidays.getIANAZone(state) : undefined;
|
|
765
|
+
Log.writeLine(LogLevels.FrameworkInformation, `Time now is [${date}] (Epoch) and we are in [${stateIANAZone ?? "<No state defined>"}]`);
|
|
766
|
+
if (offsetAndFormat.length != 2 && !(offsetAndFormat.length == 1 && offsetAndFormat[0] == "timezoneoffset"))
|
|
767
|
+
throw "Date token does not have a format parameter; example: {date;today;dd-MM-yyyy}";
|
|
768
|
+
return {
|
|
769
|
+
offsetAndFormat: offsetAndFormat,
|
|
770
|
+
verb: (offsetAndFormat[0].includes("(") && offsetAndFormat[0].endsWith(")") ? offsetAndFormat[0].split("(")[0] : offsetAndFormat[0]).toLowerCase().trim(),
|
|
771
|
+
params: offsetAndFormat[0].includes("(") && offsetAndFormat[0].endsWith(")")
|
|
772
|
+
? ((x) => {
|
|
773
|
+
return x.substring(0, x.length - 1);
|
|
774
|
+
})(offsetAndFormat[0].split("(")[1])
|
|
775
|
+
: undefined,
|
|
776
|
+
errParseDateOffset: "Invalid Active Date offset. Expect AddYears(n) AddMonths(n) or AddDays(n)",
|
|
777
|
+
errInvalidEpoch: "Invalid Epoch offset. Expect number of milliseconds since 1/1/1970",
|
|
778
|
+
errRandomParams: `Invalid Random params ([${offsetAndFormat[0]}]). Expect Random(<start date>,<end date>). Example: {date;random(1708990784000,1701360784000);yyy-MM-dd}`,
|
|
779
|
+
errFollowingDayParams: `FollowingDay requires epoch and day name. IE. FollowingDay(123456,wednesday) Got ${offsetAndFormat[0]}`,
|
|
780
|
+
errNextPublicHoldayParams: `Invalid Next Public Holiday Params. Expect nextPublicHoliday(searchFromDateEpoch,maxDaysInFuture). Got ${offsetAndFormat[0]}`,
|
|
781
|
+
errAddWorkingDaysParams: `Invalid Add Working Days Params. Expect addWorkingDays(numberOfDaysToAdd,[state/s]). Got Got ${offsetAndFormat[0]}`,
|
|
782
|
+
errInvalidDateVerb: `Invalid date verb [${offsetAndFormat[0]}]. Need: Random, Today, Now, Yesterday, Tomorrow, AddYears(n) etc... EG {date;AddDays(5);yyyy-MM-dd}`,
|
|
783
|
+
stateIANAZone: stateIANAZone,
|
|
784
|
+
};
|
|
785
|
+
}
|
|
786
|
+
static doDateTokenPostamble(offsetAndFormat, date, stateIANAZone) {
|
|
787
|
+
if (offsetAndFormat[1].toLowerCase() === "epoch") {
|
|
788
|
+
return date.toString();
|
|
789
|
+
}
|
|
790
|
+
if (offsetAndFormat[1].toLowerCase() === "second-epoch") {
|
|
791
|
+
return Math.floor(date / 1000).toString();
|
|
792
|
+
}
|
|
793
|
+
try {
|
|
794
|
+
//date += await getEpochOffset(state);
|
|
795
|
+
const processedDate = stateIANAZone ? formatInTimeZone(date, stateIANAZone, offsetAndFormat[1]) : format(date, offsetAndFormat[1]);
|
|
796
|
+
Log.writeLine(LogLevels.FrameworkDebug, `Formatted [${offsetAndFormat[0]}] date [${date}] (Epoch) with [${offsetAndFormat[1]}] to: [${processedDate}]`);
|
|
797
|
+
return processedDate;
|
|
798
|
+
}
|
|
799
|
+
catch (err) {
|
|
800
|
+
const errorText = `Error formatting date [${date}] with format string[${offsetAndFormat[1]}]: ${err?.message ?? "<Unknown error>"}}`;
|
|
801
|
+
throw new Error(errorText + "/r" + (err?.stack ?? ""));
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
static doDateTokenNonAsyncVerbs(date, dateTokenPrep) {
|
|
805
|
+
let returnDate = date;
|
|
806
|
+
switch (dateTokenPrep.verb) {
|
|
807
|
+
case "random": {
|
|
808
|
+
if (Utils.isNullOrUndefined(dateTokenPrep.params)) {
|
|
809
|
+
throw new Error(dateTokenPrep.errRandomParams);
|
|
810
|
+
}
|
|
811
|
+
else {
|
|
812
|
+
returnDate = this.doRandomDate(dateTokenPrep.params);
|
|
813
|
+
}
|
|
814
|
+
break;
|
|
815
|
+
}
|
|
816
|
+
case "today":
|
|
817
|
+
case "now":
|
|
818
|
+
break;
|
|
819
|
+
case "yesterday":
|
|
820
|
+
returnDate = addDays(date, -1).getTime();
|
|
821
|
+
break;
|
|
822
|
+
case "tomorrow":
|
|
823
|
+
returnDate = addDays(date, 1).getTime();
|
|
824
|
+
break;
|
|
825
|
+
case "addyears":
|
|
826
|
+
returnDate = addYears(date, this.getParsedDateOffset(dateTokenPrep.params, dateTokenPrep.errParseDateOffset)).getTime();
|
|
827
|
+
break;
|
|
828
|
+
case "addmonths":
|
|
829
|
+
returnDate = addMonths(date, this.getParsedDateOffset(dateTokenPrep.params, dateTokenPrep.errParseDateOffset)).getTime();
|
|
830
|
+
break;
|
|
831
|
+
case "adddays":
|
|
832
|
+
returnDate = addDays(date, this.getParsedDateOffset(dateTokenPrep.params, dateTokenPrep.errParseDateOffset)).getTime();
|
|
833
|
+
break;
|
|
834
|
+
case "addhours":
|
|
835
|
+
returnDate = addHours(date, this.getParsedDateOffset(dateTokenPrep.params, dateTokenPrep.errParseDateOffset)).getTime();
|
|
836
|
+
break;
|
|
837
|
+
case "addMinutes":
|
|
838
|
+
returnDate = addMinutes(date, this.getParsedDateOffset(dateTokenPrep.params, dateTokenPrep.errParseDateOffset)).getTime();
|
|
839
|
+
break;
|
|
840
|
+
case "followingday": {
|
|
841
|
+
if (Utils.isNullOrUndefined(dateTokenPrep.params)) {
|
|
842
|
+
throw new Error(dateTokenPrep.errFollowingDayParams);
|
|
843
|
+
}
|
|
844
|
+
else {
|
|
845
|
+
const currentAndDay = dateTokenPrep.params.split(",");
|
|
846
|
+
if (currentAndDay.length != 2)
|
|
847
|
+
throw new Error(dateTokenPrep.errFollowingDayParams);
|
|
848
|
+
returnDate = this.getFollowingDay(this.getParsedDateOffset(currentAndDay[0], dateTokenPrep.errInvalidEpoch), currentAndDay[1].toLowerCase(), dateTokenPrep.errFollowingDayParams).getTime();
|
|
849
|
+
}
|
|
850
|
+
break;
|
|
851
|
+
}
|
|
852
|
+
default:
|
|
853
|
+
if (/^\d{4}-\d{1,2}-\d{1,2}$/.test(dateTokenPrep.verb)) {
|
|
854
|
+
// we have a fixed date. User must be wanted to just format a date....
|
|
855
|
+
const date = dateTokenPrep.verb.match(/^(\d{4})-(\d{1,2})-(\d{1,2})$/);
|
|
856
|
+
const year = Number(date[1]) ?? 0;
|
|
857
|
+
const month = (Number(date[2]) ?? 0) - 1;
|
|
858
|
+
const day = Number(date[3]) ?? 0;
|
|
859
|
+
Log.writeLine(LogLevels.FrameworkInformation, `Date got fixed date:- >>${year}<<>>${month}<<>>${day}<<`);
|
|
860
|
+
const fullEpoch = new Date(Date.UTC(year, month, day, 0, 0, 0));
|
|
861
|
+
Log.writeLine(LogLevels.FrameworkInformation, `... which is:- >>${fullEpoch}<<`);
|
|
862
|
+
returnDate = fullEpoch.getTime();
|
|
863
|
+
}
|
|
864
|
+
else {
|
|
865
|
+
throw dateTokenPrep.errInvalidDateVerb;
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
return returnDate;
|
|
869
|
+
}
|
|
870
|
+
static doDateToken(state, tokenBody) {
|
|
871
|
+
let date = new Date().getTime();
|
|
872
|
+
const dateTokenPrep = this.doDateTokenPreamble(date, state, tokenBody);
|
|
873
|
+
switch (dateTokenPrep.verb) {
|
|
874
|
+
case "timezoneoffset": {
|
|
875
|
+
return this.getOffset(state, date);
|
|
876
|
+
}
|
|
877
|
+
case "addworkingdays":
|
|
878
|
+
case "followingworkingday":
|
|
879
|
+
case "nextpublicholiday": {
|
|
880
|
+
const errorString = `${dateTokenPrep.verb} uses asynchoronous calls. Use Detokenise.asyncDo`;
|
|
881
|
+
Log.writeLine(LogLevels.Error, errorString);
|
|
882
|
+
throw `Detokeniser: ${errorString}`;
|
|
883
|
+
}
|
|
884
|
+
default:
|
|
885
|
+
date = this.doDateTokenNonAsyncVerbs(date, dateTokenPrep);
|
|
886
|
+
}
|
|
887
|
+
return this.doDateTokenPostamble(dateTokenPrep.offsetAndFormat, date, dateTokenPrep.stateIANAZone);
|
|
888
|
+
}
|
|
889
|
+
static async asyncDoDateToken(state, tokenBody) {
|
|
890
|
+
let date = new Date().getTime();
|
|
891
|
+
Log.writeLine(LogLevels.FrameworkDebug, `asyncDoDateToken - Calling preAmble: Dates [${date}], State [${state}], Token Body [${tokenBody}]`);
|
|
892
|
+
const dateTokenPrep = this.doDateTokenPreamble(date, state, tokenBody);
|
|
893
|
+
Log.writeLine(LogLevels.FrameworkDebug, `asyncDoDateToken - Back from doDateTokenPreamble. Verb ${dateTokenPrep.verb}`);
|
|
894
|
+
switch (dateTokenPrep.verb) {
|
|
895
|
+
case "timezoneoffset": {
|
|
896
|
+
return this.getOffset(state, date);
|
|
897
|
+
}
|
|
898
|
+
case "addworkingdays": {
|
|
899
|
+
// Adds days to current date not counting public holidays or weekends
|
|
900
|
+
try {
|
|
901
|
+
if (Utils.isNullOrUndefined(dateTokenPrep.params)) {
|
|
902
|
+
throw new Error(dateTokenPrep.errAddWorkingDaysParams);
|
|
903
|
+
}
|
|
904
|
+
const paramArray = dateTokenPrep.params.split(",");
|
|
905
|
+
if (paramArray.length < 1) {
|
|
906
|
+
throw new Error(dateTokenPrep.errAddWorkingDaysParams);
|
|
907
|
+
}
|
|
908
|
+
const numberOfDays = Number(paramArray[0]);
|
|
909
|
+
const ascending = !(numberOfDays < 0);
|
|
910
|
+
// Get the date not counting weekends
|
|
911
|
+
const startDate = date;
|
|
912
|
+
let endDate = startDate;
|
|
913
|
+
let daysRemaining = numberOfDays;
|
|
914
|
+
while (daysRemaining != 0) {
|
|
915
|
+
endDate = addDays(endDate, ascending ? +1 : -1).getTime();
|
|
916
|
+
while ((await PublicHolidays.isDatePublicHoliday(endDate, state)) ||
|
|
917
|
+
Number(dateTokenPrep.stateIANAZone ? formatInTimeZone(endDate, dateTokenPrep.stateIANAZone, "e") : format(endDate, "e")) == 1 ||
|
|
918
|
+
Number(dateTokenPrep.stateIANAZone ? formatInTimeZone(endDate, dateTokenPrep.stateIANAZone, "e") : format(endDate, "e")) == 7) {
|
|
919
|
+
endDate = addDays(endDate, ascending ? +1 : -1).getTime();
|
|
920
|
+
}
|
|
921
|
+
daysRemaining += ascending ? -1 : 1;
|
|
922
|
+
}
|
|
923
|
+
date = endDate;
|
|
924
|
+
}
|
|
925
|
+
catch (err) {
|
|
926
|
+
Log.writeLine(LogLevels.Error, `Error processing addworkingdays: ${err?.message ?? "<Unknown error>"}`);
|
|
927
|
+
}
|
|
928
|
+
break;
|
|
929
|
+
}
|
|
930
|
+
case "followingworkingday": {
|
|
931
|
+
if (Utils.isNullOrUndefined(dateTokenPrep.params)) {
|
|
932
|
+
throw new Error(dateTokenPrep.errFollowingDayParams);
|
|
933
|
+
}
|
|
934
|
+
else {
|
|
935
|
+
// So, first get the followingday...
|
|
936
|
+
const body = `followingDay(${dateTokenPrep.params});epoch`;
|
|
937
|
+
Log.writeLine(LogLevels.FrameworkDebug, `asyncDoDateToken - Calling doDateToken for followingDay. State = [${state}]. Body = [${body}]`);
|
|
938
|
+
let workingDate = Number(this.doDateToken(state, body));
|
|
939
|
+
Log.writeLine(LogLevels.FrameworkDebug, `got from doDateToken followingDay. workingDate = ${workingDate}`);
|
|
940
|
+
// Then, check it is not a weekend or public holiday anywhere. Move forward if it is...
|
|
941
|
+
while (Number(dateTokenPrep.stateIANAZone ? formatInTimeZone(workingDate, dateTokenPrep.stateIANAZone, "e") : format(workingDate, "e")) == 7 ||
|
|
942
|
+
Number(dateTokenPrep.stateIANAZone ? formatInTimeZone(workingDate, dateTokenPrep.stateIANAZone, "e") : format(workingDate, "e")) == 1 ||
|
|
943
|
+
(await PublicHolidays.isDatePublicHoliday(workingDate, state))) {
|
|
944
|
+
workingDate = addDays(workingDate, 1).getTime();
|
|
945
|
+
}
|
|
946
|
+
date = workingDate;
|
|
947
|
+
}
|
|
948
|
+
break;
|
|
949
|
+
}
|
|
950
|
+
case "nextpublicholiday": {
|
|
951
|
+
if (Utils.isNullOrUndefined(dateTokenPrep.params)) {
|
|
952
|
+
throw new Error(dateTokenPrep.errNextPublicHoldayParams);
|
|
953
|
+
}
|
|
954
|
+
else {
|
|
955
|
+
const paramArray = dateTokenPrep.params.split(",");
|
|
956
|
+
if (paramArray.length != 2) {
|
|
957
|
+
throw new Error(dateTokenPrep.errNextPublicHoldayParams);
|
|
958
|
+
}
|
|
959
|
+
else {
|
|
960
|
+
date = (await this.getNextPublicHoliday(Number(paramArray[0]), state, Number(paramArray[1]))).getTime();
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
break;
|
|
964
|
+
}
|
|
965
|
+
default:
|
|
966
|
+
date = this.doDateTokenNonAsyncVerbs(date, dateTokenPrep);
|
|
967
|
+
}
|
|
968
|
+
return this.doDateTokenPostamble(dateTokenPrep.offsetAndFormat, date, dateTokenPrep.stateIANAZone);
|
|
969
|
+
}
|
|
970
|
+
static async getNextPublicHoliday(dateEpoch, state, maxDaysAway) {
|
|
971
|
+
// So, to do this we get all public holidays upto maxDaysAway; first holiday is day we want! easy!!!
|
|
972
|
+
if (state == undefined) {
|
|
973
|
+
const errMsg = "Cannot get next public holiday, State not defined (Use {Date(<state>);......})";
|
|
974
|
+
Log.writeLine(LogLevels.Error, errMsg);
|
|
975
|
+
throw new Error(errMsg);
|
|
976
|
+
}
|
|
977
|
+
const startDate = new Date(dateEpoch).getTime();
|
|
978
|
+
const endDate = addDays(startDate, maxDaysAway).getTime();
|
|
979
|
+
const stateIANAZone = PublicHolidays.getIANAZone(state);
|
|
980
|
+
const nextPublicHoliday = await PublicHolidays.getFirstPublicHolidayBetweenDates(startDate, endDate, state);
|
|
981
|
+
if (Utils.isNullOrUndefined(nextPublicHoliday)) {
|
|
982
|
+
throw new Error(`No public holiday found between ${formatInTimeZone(startDate, stateIANAZone, "YYYY-MM-dd")} and ${formatInTimeZone(startDate, stateIANAZone, "YYYY-MM-dd")} for State: ${state} (${stateIANAZone})`);
|
|
983
|
+
}
|
|
984
|
+
Log.writeLine(LogLevels.FrameworkInformation, `Found Public holiday: ${formatInTimeZone(nextPublicHoliday.date, stateIANAZone, "yyyy-MM-dd")} State: ${state} (${stateIANAZone})`);
|
|
985
|
+
return nextPublicHoliday.date;
|
|
986
|
+
}
|
|
987
|
+
static getFollowingDay(currentDateEpoch, requiredDayOfTheWeek, errorString) {
|
|
988
|
+
const dayName = requiredDayOfTheWeek.toLowerCase();
|
|
989
|
+
const requiredDayOfWeek = ["monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"].findIndex((item) => {
|
|
990
|
+
if (item == dayName)
|
|
991
|
+
return true;
|
|
992
|
+
return false;
|
|
993
|
+
});
|
|
994
|
+
if (requiredDayOfWeek == -1)
|
|
995
|
+
throw new Error(errorString);
|
|
996
|
+
const actualDayOfWeek = new Date(currentDateEpoch).getUTCDay() - 1;
|
|
997
|
+
if (actualDayOfWeek < requiredDayOfWeek) {
|
|
998
|
+
return addDays(currentDateEpoch, requiredDayOfWeek - actualDayOfWeek);
|
|
999
|
+
}
|
|
1000
|
+
else {
|
|
1001
|
+
return addDays(currentDateEpoch, requiredDayOfWeek + 7 - actualDayOfWeek);
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
static getParsedDateOffset(numString, errorMessage) {
|
|
1005
|
+
if (Utils.isNullOrUndefined(numString)) {
|
|
1006
|
+
throw `${errorMessage} Got [No Params!]`;
|
|
1007
|
+
}
|
|
1008
|
+
else {
|
|
1009
|
+
const offsetValue = parseInt(numString?.trim());
|
|
1010
|
+
if (isNaN(offsetValue))
|
|
1011
|
+
throw `${errorMessage} Got [${numString?.trim()}]`;
|
|
1012
|
+
return offsetValue;
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
static doRandomDate(maxAndMinDates) {
|
|
1016
|
+
const maxAndMin = maxAndMinDates.split(",");
|
|
1017
|
+
if (maxAndMin.length != 2 || Number.isNaN(+maxAndMin[0]) || Number.isNaN(+maxAndMin[1]))
|
|
1018
|
+
throw new Error(`Invalid Maximum and Minimum dates. Expect {random;date(fromEpoch,toEpoch);<format>}. Max/min was: [${maxAndMinDates}]`);
|
|
1019
|
+
const minDate = Number(maxAndMin[0]);
|
|
1020
|
+
const maxDate = Number(maxAndMin[1]);
|
|
1021
|
+
if (minDate > maxDate)
|
|
1022
|
+
throw new Error(`Minimum date greater than maximum!! Max/min was: [${maxAndMinDates}]`);
|
|
1023
|
+
return minDate + Math.abs(Utils.getRandomInt(0, this.numberOfDays(minDate, maxDate) - 1)) * 1000 * 60 * 60 * 24;
|
|
1024
|
+
}
|
|
1025
|
+
static getOffset(state, dateOfOffset) {
|
|
1026
|
+
if (state == undefined) {
|
|
1027
|
+
const errMsg = "Unable to get timezone offset as no state provided. Expect {Date(<state>);TimezoneOffset}";
|
|
1028
|
+
Log.writeLine(LogLevels.Error, errMsg);
|
|
1029
|
+
throw new Error(errMsg);
|
|
1030
|
+
}
|
|
1031
|
+
const offsetMilliseconds = getTimezoneOffset(PublicHolidays.getIANAZone(state), dateOfOffset);
|
|
1032
|
+
const offsetSeconds = Math.floor(offsetMilliseconds / 1000);
|
|
1033
|
+
const offsetHours = secondsToHours(offsetSeconds);
|
|
1034
|
+
const offsetMinutes = secondsToMinutes(offsetSeconds - offsetHours * 3600);
|
|
1035
|
+
const offset = (offsetSeconds < 0 ? "-" : "+") + Utils.pad(offsetHours, 2) + Utils.pad(offsetMinutes, 2);
|
|
1036
|
+
Log.writeLine(LogLevels.FrameworkInformation, `Got current offset (${offset}) from UTC for [${state}]`);
|
|
1037
|
+
return offset;
|
|
1038
|
+
}
|
|
1039
|
+
static numberOfDays(minDate, maxDate) {
|
|
1040
|
+
Log.writeLine(LogLevels.FrameworkDebug, `Min date [${minDate}], Max date[${maxDate}]`);
|
|
1041
|
+
const MS_PER_DAY = 1000 * 60 * 60 * 24;
|
|
1042
|
+
const numberOfDays = Math.floor(Math.abs((minDate - maxDate) / MS_PER_DAY)) + 1;
|
|
1043
|
+
Log.writeLine(LogLevels.FrameworkDebug, `Min date [${minDate}], Max date[${maxDate}]. Number of days [${numberOfDays}]`);
|
|
1044
|
+
return numberOfDays;
|
|
1045
|
+
}
|
|
1046
|
+
static doRandomFloat(limits) {
|
|
1047
|
+
const minimumAndMaximum = limits.split(",");
|
|
1048
|
+
if (minimumAndMaximum.length != 2)
|
|
1049
|
+
throw new Error(`Invalid Maximum and Minimum floats. Expect {{random.float(min;max),<format>}}. Max/min was: [${limits}]`);
|
|
1050
|
+
const min = parseFloat(minimumAndMaximum[0]);
|
|
1051
|
+
const max = parseFloat(minimumAndMaximum[1]);
|
|
1052
|
+
if (isNaN(min))
|
|
1053
|
+
throw new Error(`Invalid Minimum float. Expect {{random.float(min;max),<format>}}. Max/min was: [${limits}]`);
|
|
1054
|
+
if (isNaN(max))
|
|
1055
|
+
throw new Error(`Invalid Maximum float. Expect {{random.float(min;max),<format>}}. Max/min was: [${limits}]`);
|
|
1056
|
+
return Utils.getRandomFloat(min, max);
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
Detokeniser._endTokenChar = "]]";
|
|
1060
|
+
Detokeniser._startTokenChar = "[[";
|
|
1061
|
+
Detokeniser.EscapeChar = "/";
|
|
1062
|
+
Detokeniser._delimiter = "|";
|
|
1063
|
+
Detokeniser._asyncCallbacks = undefined;
|
|
1064
|
+
Detokeniser._syncCallbacks = undefined;
|
|
1065
|
+
class InnermostToken {
|
|
1066
|
+
constructor(inputString, StartTokenChar, EndTokenChar, EscapeChar) {
|
|
1067
|
+
let startIndex = -1;
|
|
1068
|
+
let endIndex = -1;
|
|
1069
|
+
this.escapeChar = EscapeChar;
|
|
1070
|
+
// Find the first (leftmost) non-escaped end token sequence. Because we search left-to-right this naturally
|
|
1071
|
+
// gives us the innermost token when tokens are nested.
|
|
1072
|
+
for (let index = 0; index <= inputString.length - EndTokenChar.length; index++) {
|
|
1073
|
+
if (inputString.startsWith(EndTokenChar, index) && !this.isEscaped(inputString, index)) {
|
|
1074
|
+
endIndex = index;
|
|
1075
|
+
break;
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
// Now scan right-to-left from just before the end sequence to find the matching non-escaped start sequence.
|
|
1079
|
+
if (endIndex !== -1) {
|
|
1080
|
+
for (let index = endIndex - 1; index >= 0; index--) {
|
|
1081
|
+
if (inputString.startsWith(StartTokenChar, index) && !this.isEscaped(inputString, index)) {
|
|
1082
|
+
startIndex = index;
|
|
1083
|
+
break;
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
if (startIndex !== -1 && endIndex !== -1) {
|
|
1088
|
+
this.tokenPreamble = inputString.substring(0, startIndex);
|
|
1089
|
+
this.tokenPostamble = inputString.substring(endIndex + EndTokenChar.length);
|
|
1090
|
+
this._childToken = inputString.substring(startIndex, endIndex); // includes StartTokenChar, excludes EndTokenChar
|
|
1091
|
+
}
|
|
1092
|
+
else {
|
|
1093
|
+
this.tokenPreamble = inputString;
|
|
1094
|
+
this.tokenPostamble = "";
|
|
1095
|
+
this._childToken = "";
|
|
1096
|
+
}
|
|
1097
|
+
this.foundToken = startIndex !== -1 && endIndex !== -1;
|
|
1098
|
+
}
|
|
1099
|
+
get preamble() {
|
|
1100
|
+
return this.tokenPreamble;
|
|
1101
|
+
}
|
|
1102
|
+
get postamble() {
|
|
1103
|
+
return this.tokenPostamble;
|
|
1104
|
+
}
|
|
1105
|
+
get childToken() {
|
|
1106
|
+
return this._childToken;
|
|
1107
|
+
}
|
|
1108
|
+
get hasToken() {
|
|
1109
|
+
return this.foundToken;
|
|
1110
|
+
}
|
|
1111
|
+
isEscaped(fullString, positionToTest) {
|
|
1112
|
+
let index;
|
|
1113
|
+
let escapeCharCount = 0;
|
|
1114
|
+
let returnIsEscaped = false;
|
|
1115
|
+
if (positionToTest > 0) {
|
|
1116
|
+
index = positionToTest;
|
|
1117
|
+
while (!(--index < 0)) {
|
|
1118
|
+
if (fullString[index] == this.escapeChar) {
|
|
1119
|
+
escapeCharCount++;
|
|
1120
|
+
}
|
|
1121
|
+
else {
|
|
1122
|
+
break;
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
if (escapeCharCount % 2 == 1) {
|
|
1126
|
+
returnIsEscaped = true;
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
return returnIsEscaped;
|
|
1130
|
+
}
|
|
1131
|
+
}
|