@controlium/utils 1.0.2-alpha.2 → 1.0.2-alpha.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,1015 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.Detokeniser = void 0;
4
- const date_fns_1 = require("date-fns");
5
- const date_fns_tz_1 = require("date-fns-tz");
6
- const index_1 = require("../index");
7
- // ─── PublicHolidays stub ─────────────────────────────────────────────────────
8
- // TODO: Replace with a real import when a PublicHolidays module is provided by the consumer.
9
- // The region string is an opaque identifier passed through by the caller (e.g. "us-east", "eu-london").
10
- // eslint-disable-next-line @typescript-eslint/no-namespace
11
- var PublicHolidays;
12
- (function (PublicHolidays) {
13
- function getIANAZone(_region) { throw new Error("PublicHolidays not yet implemented"); }
14
- PublicHolidays.getIANAZone = getIANAZone;
15
- async function isDatePublicHoliday(_date, _region) { throw new Error("PublicHolidays not yet implemented"); }
16
- PublicHolidays.isDatePublicHoliday = isDatePublicHoliday;
17
- async function getFirstPublicHolidayBetweenDates(_start, _end, _region) { throw new Error("PublicHolidays not yet implemented"); }
18
- PublicHolidays.getFirstPublicHolidayBetweenDates = getFirstPublicHolidayBetweenDates;
19
- })(PublicHolidays || (PublicHolidays = {}));
20
- // ─── MockUtils stub ───────────────────────────────────────────────────────────
21
- // TODO: Replace with real import when MockUtils is brought into the package
22
- const MockUtils = {
23
- interceptedRequests: undefined,
24
- };
25
- /**
26
- * Processes strings containing tokens in the format `[[tokenType|expression|format]]`, replacing each
27
- * token with its resolved value. Tokens can be nested — the innermost is always resolved first.
28
- *
29
- * ## Token syntax
30
- * ```
31
- * [[tokenType|expression|format]]
32
- * ```
33
- * - **tokenType** — identifies the built-in handler or is matched against registered callbacks
34
- * - **expression** — what to compute (type-specific)
35
- * - **format** — how to format the result (type-specific, often optional)
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.addCallback}. Both sync and async handlers
142
- * share the same registration method and callback list. Callbacks are tried in registration order;
143
- * return `undefined` to pass to the next callback. If all callbacks return `undefined` and no built-in
144
- * handler matched, an error is thrown.
145
- *
146
- * Async callbacks are silently skipped when {@link Detokeniser.do} (sync) is used — use
147
- * {@link Detokeniser.doAsync} if your callback returns a Promise.
148
- *
149
- * @see {@link Detokeniser.addCallback}
150
- * @see {@link Detokeniser.do}
151
- * @see {@link Detokeniser.doAsync}
152
- */
153
- class Detokeniser {
154
- /**
155
- * Resets the Detokeniser to factory defaults:
156
- * - Escape char restored to `/`
157
- * - All registered sync and async callbacks cleared
158
- *
159
- * Call this in test teardown to guarantee a clean state between scenarios.
160
- * @example
161
- * afterEach(() => Detokeniser.reset());
162
- */
163
- static reset() {
164
- this.EscapeChar = "/";
165
- this._callbacks = undefined;
166
- }
167
- /**
168
- * Registers a custom token handler. Both sync and async handlers are registered via this method
169
- * and share the same callback list.
170
- *
171
- * Callbacks are tried in registration order. The first to return a non-`undefined` value wins.
172
- * Return `undefined` to pass to the next callback. If all callbacks return `undefined` and no
173
- * built-in handler matched, an error is thrown.
174
- *
175
- * Async callbacks (those returning a `Promise`) are silently skipped when {@link Detokeniser.do}
176
- * is used — register an async callback only if you intend to call {@link Detokeniser.doAsync}.
177
- *
178
- * @param callback - `(token: string) => string | undefined | Promise<string | undefined>`
179
- * - `token` — full token body without `[[` / `]]`, e.g. `"mytype|arg1|arg2"` (delimiter is always `|`)
180
- *
181
- * @example
182
- * // Sync handler for [[env|VAR_NAME]] tokens
183
- * Detokeniser.addCallback((token) => {
184
- * const [type, name] = token.split('|');
185
- * if (type !== 'env') return undefined;
186
- * return process.env[name] ?? '';
187
- * });
188
- * Detokeniser.do('Path: [[env|HOME]]'); // → 'Path: /home/user'
189
- *
190
- * @example
191
- * // Async handler for [[db|table|column|where]] tokens
192
- * Detokeniser.addCallback(async (token) => {
193
- * const [type, table, column, where] = token.split('|');
194
- * if (type !== 'db') return undefined;
195
- * const row = await db.query(`SELECT ${column} FROM ${table} WHERE ${where} LIMIT 1`);
196
- * return String(row[column]);
197
- * });
198
- * const result = await Detokeniser.doAsync('ID: [[db|users|id|active=1]]');
199
- *
200
- * @see {@link Detokeniser.resetCallbacks} to remove all registered callbacks
201
- * @see {@link Detokeniser.doAsync} for async token resolution
202
- */
203
- static addCallback(callback) {
204
- if (!this._callbacks) {
205
- this._callbacks = new Array();
206
- }
207
- this._callbacks.push(callback);
208
- }
209
- /**
210
- * Removes all registered callbacks. Built-in token handlers are unaffected.
211
- * Use between tests or scenarios to ensure callback isolation.
212
- * @see {@link Detokeniser.reset} to also restore all defaults
213
- */
214
- static resetCallbacks() {
215
- this._callbacks = undefined;
216
- }
217
- /**
218
- * Synchronously resolves all tokens in the given string and returns the result.
219
- *
220
- * Tokens are resolved innermost-first. Registered sync callbacks are invoked for token types not
221
- * handled by the built-in set. Use {@link Detokeniser.doAsync} if you need async callbacks or any
222
- * of the async-only date expressions (`addWorkingDays`, `followingWorkingDay`, `nextPublicHoliday`).
223
- *
224
- * @param tokenisedString - String potentially containing `[[...]]` tokens
225
- * @param options - Optional processing options
226
- * @returns The input string with all tokens replaced by their resolved values
227
- * @throws If any token is malformed, unsupported, or a callback throws
228
- *
229
- * @example
230
- * Detokeniser.do('Ref-[[random|digits|6]]'); // → e.g. 'Ref-482910'
231
- * Detokeniser.do('Expires [[date|addDays(30)|dd/MM/yyyy]]'); // → e.g. 'Expires 01/05/2026'
232
- * Detokeniser.do('[[random|uppercaseletters|3]]-[[random|digits|4]]'); // → e.g. 'XKP-7391'
233
- *
234
- * @example
235
- * // Nested tokens — innermost resolved first
236
- * Detokeniser.do('[[date|addDays([[random|digits|1]])|dd-MM-yyyy]]');
237
- *
238
- * @example
239
- * // Context parameters for [[setting|...]] tokens
240
- * Detokeniser.do('Hello [[setting|profileParameterName: "username"]]', {
241
- * contextParameters: { username: 'Alice' }
242
- * }); // → 'Hello Alice'
243
- *
244
- * @example
245
- * // Escaping — produce literal [[ / ]] in output
246
- * Detokeniser.do('Press /[[Enter/]] to continue'); // → 'Press [[Enter]] to continue'
247
- */
248
- static do(tokenisedString, options = {}) {
249
- let deEscape = true;
250
- try {
251
- const runningString = this.doPreamble(tokenisedString);
252
- // Loop until last token find found no tokens
253
- while (runningString.currentToken.hasToken) {
254
- // Process the last found token, prepend it to the text after the last found token then find any token in the resulting string
255
- runningString.currentToken = new InnermostToken(this.doToken(runningString.currentToken.childToken.substring(this._startTokenChar.length), options) + runningString.currentToken.postamble, this._startTokenChar, this._endTokenChar, this.EscapeChar);
256
- // 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
257
- runningString.outputString = this.do(runningString.outputString + runningString.currentToken.preamble);
258
- deEscape = false;
259
- }
260
- //
261
- // Okay, so there is a bug here. But it shall remain unfixed as it requires time.
262
- //
263
- // The bug is that is the resolved token string contains a sequence of characters that makes doDeEscapesIfRequired think it sees an escaped special char
264
- // 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.
265
- //
266
- // IE.
267
- // const encoded = Buffer.from('this/>escaped').toString('base64');
268
- // const decoded = Detokeniser.do(`<base64;decode;${encoded}>`);
269
- //
270
- // decoded would now be "this>escaped". Should be "this/>escaped".
271
- //
272
- runningString.outputString = this.doDeEscapesIfRequired(tokenisedString, deEscape, runningString.outputString);
273
- return runningString.outputString;
274
- }
275
- catch (err) {
276
- const errText = `Error processing [${tokenisedString}]: ${typeof err === "string" ? err : err instanceof Error ? err.message : "<unknown>"}`;
277
- index_1.Log.writeLine(index_1.LogLevels.Error, errText);
278
- throw Error(errText);
279
- }
280
- }
281
- /**
282
- * Asynchronously resolves all tokens in the given string and returns a Promise of the result.
283
- *
284
- * Functionally equivalent to {@link Detokeniser.do} but additionally supports:
285
- * - Async callbacks registered via {@link Detokeniser.addCallbackAsync}
286
- * - Async-only date expressions: `addWorkingDays`, `followingWorkingDay`, `nextPublicHoliday`
287
- *
288
- * Note: sync callbacks registered via {@link Detokeniser.addCallbackSync} are **not** invoked
289
- * during async processing — re-register them with {@link Detokeniser.addCallbackAsync} if needed.
290
- *
291
- * @param tokenisedString - String potentially containing `[[...]]` tokens
292
- * @returns Promise resolving to the input string with all tokens replaced
293
- * @throws If any token is malformed, unsupported, or a callback throws
294
- *
295
- * @example
296
- * // Async-only date expressions require doAsync and a region qualifier
297
- * await Detokeniser.doAsync('[[date(us-east)|addWorkingDays(5)|dd-MM-yyyy]]');
298
- * await Detokeniser.doAsync('[[date(eu-london)|nextPublicHoliday([[date|today|epoch]],90)|dd-MM-yyyy]]');
299
- *
300
- * @example
301
- * // Async callback for database-driven tokens
302
- * Detokeniser.addCallbackAsync(async (token) => {
303
- * const [type, key] = token.split('|');
304
- * if (type !== 'db') return undefined;
305
- * return await fetchFromDatabase(key);
306
- * });
307
- * await Detokeniser.doAsync('User: [[db|users.name.first]]');
308
- */
309
- static async doAsync(tokenisedString) {
310
- let deEscape = true;
311
- try {
312
- const runningString = this.doPreamble(tokenisedString);
313
- // Loop until last token find found no tokens
314
- while (runningString.currentToken.hasToken) {
315
- // Process the last found token, prepend it to the text after the last found token then find any token in the resulting string
316
- runningString.currentToken = new InnermostToken((await this.asyncDoToken(runningString.currentToken.childToken.substring(this._startTokenChar.length))) +
317
- runningString.currentToken.postamble, this._startTokenChar, this._endTokenChar, this.EscapeChar);
318
- const logToken = runningString.currentToken.hasToken ? runningString.currentToken.childToken : '<No Token>';
319
- index_1.Log.writeLine(index_1.LogLevels.FrameworkDebug, `Preamble:[${runningString.currentToken.preamble}] Token:[${logToken}] Postamble:[${runningString.currentToken.postamble}]`);
320
- // 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
321
- runningString.outputString = await this.doAsync(runningString.outputString + runningString.currentToken.preamble);
322
- deEscape = false;
323
- }
324
- runningString.outputString = this.doDeEscapesIfRequired(tokenisedString, deEscape, runningString.outputString);
325
- return runningString.outputString;
326
- }
327
- catch (err) {
328
- const errText = `Error processing [${tokenisedString}]: ${typeof err === "string" ? err : err instanceof Error ? err.message : "<unknown>"}`;
329
- index_1.Log.writeLine(index_1.LogLevels.Error, errText);
330
- throw Error(errText);
331
- }
332
- }
333
- static doDeEscapesIfRequired(tokenisedString, deEscape, stringToProcess) {
334
- // After all tokens are resolved, remove escaping from any chars that were escaped to prevent token recognition.
335
- // Uses a 3-pass approach to correctly handle escaped escape chars (e.g. //) adjacent to other escapes (e.g. /[):
336
- // Pass 1: replace // with \x00 placeholder
337
- // Pass 2: de-escape each char that makes up the start/end token sequences
338
- // Pass 3: restore \x00 back to the escape char
339
- let processedString = stringToProcess;
340
- if (deEscape && !index_1.StringUtils.isBlank(processedString)) {
341
- const doubleEscapes = this.EscapeChar + this.EscapeChar;
342
- processedString = index_1.StringUtils.replaceAll(processedString, doubleEscapes, "\x00");
343
- const tokenChars = new Set([...this._startTokenChar, ...this._endTokenChar]);
344
- for (const char of tokenChars) {
345
- processedString = index_1.StringUtils.replaceAll(processedString, this.EscapeChar + char, char);
346
- }
347
- processedString = index_1.StringUtils.replaceAll(processedString, "\x00", this.EscapeChar);
348
- }
349
- if (tokenisedString !== processedString) {
350
- index_1.Log.writeLine(index_1.LogLevels.FrameworkDebug, `Processed [${tokenisedString}]\nto [${processedString}]`);
351
- }
352
- return processedString;
353
- }
354
- static doPreamble(tokenisedString) {
355
- // Drill down to the left-most deepest (if nested) token
356
- const token = new InnermostToken(tokenisedString, this._startTokenChar, this._endTokenChar, this.EscapeChar);
357
- // 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
358
- const outputString = token.preamble;
359
- return { currentToken: token, outputString };
360
- }
361
- static doTokenMain(token, options = {}) {
362
- const doTokenReturn = {};
363
- doTokenReturn.processedToken = undefined;
364
- if (!token || token === "") {
365
- throw new Error("Empty token! Token must be populated.");
366
- }
367
- const [tokenName, postAmble] = index_1.StringUtils.splitRemaining(token, this._delimiter, 2);
368
- const loweredTokenName = tokenName.toLowerCase().trim();
369
- const postAmbleDeEscaped = this.doDeEscapesIfRequired(postAmble, true, postAmble);
370
- if (typeof doTokenReturn.processedToken === "undefined") {
371
- switch (loweredTokenName) {
372
- case "random":
373
- if (index_1.StringUtils.isBlank(postAmbleDeEscaped))
374
- throw new Error(`Random token [${token}] needs at least 2 parts (IE. {{random;type[;<length>]}} etc.)`);
375
- doTokenReturn.processedToken = this.doRandomToken(postAmbleDeEscaped);
376
- break;
377
- case "setting":
378
- {
379
- if (index_1.StringUtils.isBlank(postAmbleDeEscaped))
380
- throw new Error(`Setting token [${token}] needs at least 2 parts (IE. {{setting;processEnvName: "TEST_LOG_TO_CONSOLE")`);
381
- doTokenReturn.processedToken = this.doSettingToken(postAmbleDeEscaped, options);
382
- break;
383
- }
384
- case "mockintercepts":
385
- if (index_1.StringUtils.isBlank(postAmbleDeEscaped))
386
- throw new Error(`Request token [${token}] needs 2 parts {{mockintercepts;JSONPath}}`);
387
- doTokenReturn.processedToken = this.doMockRequests(postAmbleDeEscaped);
388
- break;
389
- case "jwt":
390
- if (index_1.StringUtils.isBlank(postAmbleDeEscaped))
391
- throw new Error(`JWT token [${token}] needs at least 2 parts (IE. {{jwt;payload[;signature[;options]]}} etc.)`);
392
- doTokenReturn.processedToken = this.doJWTToken(postAmbleDeEscaped);
393
- break;
394
- case "base64":
395
- // aha, we actuall need three parts <base64;encode|decode;<string>>
396
- if (!index_1.StringUtils.isBlank(postAmbleDeEscaped)) {
397
- const [direction, value] = index_1.StringUtils.splitRemaining(postAmbleDeEscaped, this._delimiter, 2);
398
- if (direction == "encode" || direction == "decode") {
399
- doTokenReturn.processedToken = this.doBase64(value, direction);
400
- break;
401
- }
402
- }
403
- throw new Error(`BASE64 token [${token}] needs 3 parts (IE. {{base64;encode|decode;value}})`);
404
- default:
405
- if (loweredTokenName.startsWith("date")) {
406
- doTokenReturn.tokenBodyIfDateToken = postAmbleDeEscaped;
407
- if (loweredTokenName[4] === "(" && loweredTokenName.endsWith(")")) {
408
- if (index_1.StringUtils.isBlank(postAmbleDeEscaped)) {
409
- throw new Error(`Date token [${token}] needs 2 or 3 parts {{date(<state>);<offset>;<format>}} or {{date;timezone}}`);
410
- }
411
- else {
412
- const state = index_1.StringUtils.trimChar(index_1.StringUtils.splitRemaining(tokenName, "(", 2)[1], ")");
413
- const body = postAmbleDeEscaped;
414
- index_1.Log.writeLine(index_1.LogLevels.FrameworkDebug, `Calling doDateToken. State = [${state}]. Body = [${body}]`);
415
- doTokenReturn.state = state;
416
- }
417
- }
418
- }
419
- }
420
- }
421
- return doTokenReturn;
422
- }
423
- static doToken(token, options = {}) {
424
- const doTokenReturn = this.doTokenMain(token, options);
425
- let processedToken;
426
- if (index_1.Utils.isNullOrUndefined(doTokenReturn.processedToken) && doTokenReturn.tokenBodyIfDateToken) {
427
- processedToken = this.doDateToken(doTokenReturn.state, doTokenReturn.tokenBodyIfDateToken);
428
- }
429
- else {
430
- processedToken = doTokenReturn.processedToken;
431
- }
432
- //
433
- // If token still not been processed itterate through the callbacks, breaking as soon as it is processed
434
- //
435
- try {
436
- if (index_1.Utils.isNullOrUndefined(processedToken)) {
437
- if (typeof this._callbacks != "undefined") {
438
- this._callbacks.every((callback) => {
439
- const result = callback(token);
440
- if (result instanceof Promise)
441
- return true; // skip async callbacks in sync context
442
- processedToken = result;
443
- return index_1.Utils.isNullOrUndefined(processedToken);
444
- });
445
- }
446
- }
447
- }
448
- catch (e) {
449
- throw new Error(`Error processing callback for [${token}]: ${e?.message ?? "<Unknown Error>"}}`);
450
- }
451
- if (index_1.Utils.isNullOrUndefined(processedToken)) {
452
- throw new Error(`Unsupported token [${index_1.StringUtils.splitRemaining(token, this._delimiter, 2)[0]}]`);
453
- }
454
- else {
455
- return processedToken;
456
- }
457
- }
458
- static async asyncDoToken(token, options = {}) {
459
- const doTokenReturn = this.doTokenMain(token, options);
460
- let processedToken;
461
- if (index_1.Utils.isNullOrUndefined(doTokenReturn.processedToken) && doTokenReturn.tokenBodyIfDateToken) {
462
- processedToken = await this.asyncDoDateToken(doTokenReturn.state, doTokenReturn.tokenBodyIfDateToken);
463
- }
464
- else {
465
- processedToken = doTokenReturn.processedToken;
466
- }
467
- //
468
- // If token still not been processed itterate through the callbacks, breaking as soon as it is processed. Dirty
469
- // ucky loop. But it works.... If you know a sexier way of doing it please change!!
470
- //
471
- try {
472
- if (index_1.Utils.isNullOrUndefined(processedToken)) {
473
- if (typeof this._callbacks != "undefined") {
474
- for (const callback of this._callbacks) {
475
- processedToken = await Promise.resolve(callback(token));
476
- if (!index_1.Utils.isNullOrUndefined(processedToken))
477
- break;
478
- }
479
- }
480
- }
481
- }
482
- catch (e) {
483
- index_1.Log.writeLine(index_1.LogLevels.Error, `Error processing async callback (ignoring and setting response to undefined):${e?.message ?? "<Unknown Error>"}}`);
484
- processedToken = undefined;
485
- }
486
- if (index_1.Utils.isNullOrUndefined(processedToken)) {
487
- throw new Error(`Unsupported token [${index_1.StringUtils.splitRemaining(token, this._delimiter, 2)[0]}]`);
488
- }
489
- else {
490
- return processedToken;
491
- }
492
- }
493
- static doSettingToken(tokenBody, options = {}) {
494
- const bodyJSON = `{${tokenBody}}`;
495
- if (!index_1.JsonUtils.isJson(bodyJSON, true)) {
496
- 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`;
497
- index_1.Log.writeLine(index_1.LogLevels.Error, errText);
498
- throw new Error(errText);
499
- }
500
- const result = index_1.Utils.getSetting(index_1.LogLevels.TestInformation, "From Detokeniser", index_1.JsonUtils.parse(bodyJSON, true), options.contextParameters);
501
- if (index_1.Utils.isNullOrUndefined(result)) {
502
- return result;
503
- }
504
- else {
505
- return typeof result === 'object' ? JSON.stringify(result) : String(result);
506
- }
507
- }
508
- /**
509
- * Base64-encodes or decodes a string.
510
- *
511
- * This is the underlying handler for `[[base64|encode|...]]` and `[[base64|decode|...]]` tokens
512
- * but is also exposed for direct use.
513
- *
514
- * @param original - The string to encode or decode
515
- * @param direction - `"encode"` to base64-encode; `"decode"` to base64-decode
516
- * @returns The encoded or decoded string
517
- * @throws If the conversion fails (e.g. invalid base64 input for decode)
518
- *
519
- * @example
520
- * Detokeniser.doBase64('Hello World', 'encode'); // → 'SGVsbG8gV29ybGQ='
521
- * Detokeniser.doBase64('SGVsbG8gV29ybGQ=', 'decode'); // → 'Hello World'
522
- */
523
- static doBase64(original, direction) {
524
- try {
525
- return direction == "encode" ? Buffer.from(original).toString("base64") : Buffer.from(original, "base64").toString();
526
- }
527
- catch (err) {
528
- const errText = `Converting:\n[${original}]\n ${direction == "encode" ? "to" : "from"} base64:\n {${err.message}}`;
529
- index_1.Log.writeLine(index_1.LogLevels.Error, errText);
530
- throw new Error(errText);
531
- }
532
- }
533
- //
534
- // So, this has been created quickly with little/no checking/testing. Lots and lots of error handling needs adding or testers will get
535
- // errors/fails they have no idea how to fix!!!!
536
- //
537
- static doJWTToken(tokenBody) {
538
- index_1.Log.writeLine(index_1.LogLevels.FrameworkDebug, `JWT token [${tokenBody}]`);
539
- const typeAndLengthOrFormat = index_1.StringUtils.splitRemaining(tokenBody, this._delimiter, 3);
540
- if (typeAndLengthOrFormat.length == 3) {
541
- return index_1.Utils.createJWT(typeAndLengthOrFormat[0], typeAndLengthOrFormat[1], typeAndLengthOrFormat[2]);
542
- }
543
- else if (typeAndLengthOrFormat.length == 2) {
544
- return index_1.Utils.createJWT(typeAndLengthOrFormat[0], typeAndLengthOrFormat[1]);
545
- }
546
- else {
547
- return index_1.Utils.createJWT(typeAndLengthOrFormat[0], "DummySignature");
548
- }
549
- }
550
- static doMockRequests(jsonPath) {
551
- //
552
- // 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,
553
- // whether MSW OR Playwright.
554
- //
555
- if (!MockUtils.interceptedRequests || MockUtils.interceptedRequests.length <= 0) {
556
- const errMessage = "No Mock Intercepted requests to harvest from!?";
557
- index_1.Log.writeLine(index_1.LogLevels.Error, errMessage);
558
- throw new Error(errMessage);
559
- }
560
- const jsonProperties = index_1.JsonUtils.getPropertiesMatchingPath(MockUtils.interceptedRequests, jsonPath);
561
- if (jsonProperties.length != 1) {
562
- throw new Error(`Expected path (${jsonPath}) to match exactly one JSON node. However, path matched ${jsonProperties.length} nodes!`);
563
- }
564
- else {
565
- return jsonProperties[0].value;
566
- }
567
- }
568
- /**
569
- * Extracts the content inside the first set of parentheses in `input`, respecting the escape char.
570
- * An escaped `)` (i.e. `/)`) is treated as a literal `)` and does not end the content.
571
- * @example parseParenContent("from(abc/))") → "abc)"
572
- * @example parseParenContent("from(xyz/[[)") → "xyz[[" (with default escape char `/`)
573
- */
574
- static parseParenContent(input) {
575
- const openParen = input.indexOf("(");
576
- if (openParen === -1)
577
- return "";
578
- let result = "";
579
- let i = openParen + 1;
580
- while (i < input.length) {
581
- if (input[i] === this.EscapeChar && i + 1 < input.length) {
582
- result += input[i + 1];
583
- i += 2;
584
- }
585
- else if (input[i] === ")") {
586
- break;
587
- }
588
- else {
589
- result += input[i];
590
- i++;
591
- }
592
- }
593
- return result;
594
- }
595
- static doRandomToken(tokenBody) {
596
- const typeAndLengthOrFormat = index_1.StringUtils.splitRemaining(tokenBody, this._delimiter, 2);
597
- let result = "";
598
- let select = "";
599
- const verb = typeAndLengthOrFormat[0].toLowerCase().trim();
600
- if (verb.startsWith("date(")) {
601
- const randomDate = this.doRandomDate(verb.substring(verb.indexOf("(") + 1, verb.indexOf(")")));
602
- result = typeAndLengthOrFormat[1].toLowerCase() === "epoch" ? "" + randomDate : (0, date_fns_1.format)(randomDate, typeAndLengthOrFormat[1]);
603
- }
604
- else if (verb.startsWith("float(")) {
605
- // 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
606
- // to decimal places if only a single number given (then change would be non-breaking)....
607
- if (isNaN(parseInt(typeAndLengthOrFormat[1])))
608
- throw `Invalid Float format. Expect {{random.float(min;max),<number of decimals>}}. Format was: [${typeAndLengthOrFormat[1]}]`;
609
- const numberOfDecimalPlaces = parseInt(typeAndLengthOrFormat[1]);
610
- const powerDPs = Math.pow(10, numberOfDecimalPlaces);
611
- result = (Math.trunc(this.doRandomFloat(verb.substring(verb.indexOf("(") + 1, verb.indexOf(")"))) * powerDPs) / powerDPs).toFixed(numberOfDecimalPlaces);
612
- }
613
- else {
614
- if (verb.startsWith("from(")) {
615
- select = this.parseParenContent(typeAndLengthOrFormat[0]);
616
- }
617
- else {
618
- switch (verb) {
619
- case "letters":
620
- select = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
621
- break;
622
- case "lowercaseletters":
623
- select = "abcdefghijklmnopqrstuvwxyz";
624
- break;
625
- case "uppercaseletters":
626
- select = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
627
- break;
628
- case "digits":
629
- select = "0123456789";
630
- break;
631
- case "alphanumerics":
632
- select = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567890";
633
- break;
634
- default:
635
- throw `Unrecognised random Type [${typeAndLengthOrFormat[0]}] - Expect letters, lowercaseletters, uppercaseletters digits or alphanumerics`;
636
- }
637
- }
638
- if (isNaN(parseInt(typeAndLengthOrFormat[1])) || parseInt(typeAndLengthOrFormat[1]) < 0)
639
- throw `Invalid length part in Random token {{random;<type>;<length>}}. Length was: [${typeAndLengthOrFormat[1]}]`;
640
- for (let count = 0; count < parseInt(typeAndLengthOrFormat[1]); count++) {
641
- result += select[index_1.Utils.getRandomInt(0, select.length - 1)];
642
- }
643
- }
644
- return result;
645
- }
646
- static doDateTokenPreamble(date, state, tokenBody) {
647
- const offsetAndFormat = index_1.StringUtils.splitRemaining(tokenBody, this._delimiter, 2);
648
- const stateIANAZone = state ? PublicHolidays.getIANAZone(state) : undefined;
649
- index_1.Log.writeLine(index_1.LogLevels.FrameworkInformation, `Time now is [${date}] (Epoch) and we are in [${stateIANAZone ?? "<No state defined>"}]`);
650
- if (offsetAndFormat.length != 2 && !(offsetAndFormat.length == 1 && offsetAndFormat[0] == "timezoneoffset"))
651
- throw "Date token does not have a format parameter; example: {date;today;dd-MM-yyyy}";
652
- return {
653
- offsetAndFormat: offsetAndFormat,
654
- verb: (offsetAndFormat[0].includes("(") && offsetAndFormat[0].endsWith(")") ? offsetAndFormat[0].split("(")[0] : offsetAndFormat[0]).toLowerCase().trim(),
655
- params: offsetAndFormat[0].includes("(") && offsetAndFormat[0].endsWith(")")
656
- ? ((x) => {
657
- return x.substring(0, x.length - 1);
658
- })(offsetAndFormat[0].split("(")[1])
659
- : undefined,
660
- errParseDateOffset: "Invalid Active Date offset. Expect AddYears(n) AddMonths(n) or AddDays(n)",
661
- errInvalidEpoch: "Invalid Epoch offset. Expect number of milliseconds since 1/1/1970",
662
- errRandomParams: `Invalid Random params ([${offsetAndFormat[0]}]). Expect Random(<start date>,<end date>). Example: {date;random(1708990784000,1701360784000);yyy-MM-dd}`,
663
- errFollowingDayParams: `FollowingDay requires epoch and day name. IE. FollowingDay(123456,wednesday) Got ${offsetAndFormat[0]}`,
664
- errNextPublicHoldayParams: `Invalid Next Public Holiday Params. Expect nextPublicHoliday(searchFromDateEpoch,maxDaysInFuture). Got ${offsetAndFormat[0]}`,
665
- errAddWorkingDaysParams: `Invalid Add Working Days Params. Expect addWorkingDays(numberOfDaysToAdd,[state/s]). Got Got ${offsetAndFormat[0]}`,
666
- errInvalidDateVerb: `Invalid date verb [${offsetAndFormat[0]}]. Need: Random, Today, Now, Yesterday, Tomorrow, AddYears(n) etc... EG {date;AddDays(5);yyyy-MM-dd}`,
667
- stateIANAZone: stateIANAZone,
668
- };
669
- }
670
- static doDateTokenPostamble(offsetAndFormat, date, stateIANAZone) {
671
- if (offsetAndFormat[1].toLowerCase() === "epoch") {
672
- return date.toString();
673
- }
674
- if (offsetAndFormat[1].toLowerCase() === "second-epoch") {
675
- return Math.floor(date / 1000).toString();
676
- }
677
- try {
678
- //date += await getEpochOffset(state);
679
- const processedDate = stateIANAZone ? (0, date_fns_tz_1.formatInTimeZone)(date, stateIANAZone, offsetAndFormat[1]) : (0, date_fns_1.format)(date, offsetAndFormat[1]);
680
- index_1.Log.writeLine(index_1.LogLevels.FrameworkDebug, `Formatted [${offsetAndFormat[0]}] date [${date}] (Epoch) with [${offsetAndFormat[1]}] to: [${processedDate}]`);
681
- return processedDate;
682
- }
683
- catch (err) {
684
- const errorText = `Error formatting date [${date}] with format string[${offsetAndFormat[1]}]: ${err?.message ?? "<Unknown error>"}}`;
685
- throw new Error(errorText + "/r" + (err?.stack ?? ""));
686
- }
687
- }
688
- static doDateTokenNonAsyncVerbs(date, dateTokenPrep) {
689
- let returnDate = date;
690
- switch (dateTokenPrep.verb) {
691
- case "random": {
692
- if (index_1.Utils.isNullOrUndefined(dateTokenPrep.params)) {
693
- throw new Error(dateTokenPrep.errRandomParams);
694
- }
695
- else {
696
- returnDate = this.doRandomDate(dateTokenPrep.params);
697
- }
698
- break;
699
- }
700
- case "today":
701
- case "now":
702
- break;
703
- case "yesterday":
704
- returnDate = (0, date_fns_1.addDays)(date, -1).getTime();
705
- break;
706
- case "tomorrow":
707
- returnDate = (0, date_fns_1.addDays)(date, 1).getTime();
708
- break;
709
- case "addyears":
710
- returnDate = (0, date_fns_1.addYears)(date, this.getParsedDateOffset(dateTokenPrep.params, dateTokenPrep.errParseDateOffset)).getTime();
711
- break;
712
- case "addmonths":
713
- returnDate = (0, date_fns_1.addMonths)(date, this.getParsedDateOffset(dateTokenPrep.params, dateTokenPrep.errParseDateOffset)).getTime();
714
- break;
715
- case "adddays":
716
- returnDate = (0, date_fns_1.addDays)(date, this.getParsedDateOffset(dateTokenPrep.params, dateTokenPrep.errParseDateOffset)).getTime();
717
- break;
718
- case "addhours":
719
- returnDate = (0, date_fns_1.addHours)(date, this.getParsedDateOffset(dateTokenPrep.params, dateTokenPrep.errParseDateOffset)).getTime();
720
- break;
721
- case "addminutes":
722
- returnDate = (0, date_fns_1.addMinutes)(date, this.getParsedDateOffset(dateTokenPrep.params, dateTokenPrep.errParseDateOffset)).getTime();
723
- break;
724
- case "followingday": {
725
- if (index_1.Utils.isNullOrUndefined(dateTokenPrep.params)) {
726
- throw new Error(dateTokenPrep.errFollowingDayParams);
727
- }
728
- else {
729
- const currentAndDay = dateTokenPrep.params.split(",");
730
- if (currentAndDay.length != 2)
731
- throw new Error(dateTokenPrep.errFollowingDayParams);
732
- returnDate = this.getFollowingDay(this.getParsedDateOffset(currentAndDay[0], dateTokenPrep.errInvalidEpoch), currentAndDay[1].toLowerCase(), dateTokenPrep.errFollowingDayParams).getTime();
733
- }
734
- break;
735
- }
736
- default:
737
- if (/^\d{4}-\d{1,2}-\d{1,2}$/.test(dateTokenPrep.verb)) {
738
- // we have a fixed date. User must be wanted to just format a date....
739
- const date = dateTokenPrep.verb.match(/^(\d{4})-(\d{1,2})-(\d{1,2})$/);
740
- const year = Number(date[1]) ?? 0;
741
- const month = (Number(date[2]) ?? 0) - 1;
742
- const day = Number(date[3]) ?? 0;
743
- index_1.Log.writeLine(index_1.LogLevels.FrameworkInformation, `Date got fixed date:- >>${year}<<>>${month}<<>>${day}<<`);
744
- const fullEpoch = new Date(Date.UTC(year, month, day, 0, 0, 0));
745
- index_1.Log.writeLine(index_1.LogLevels.FrameworkInformation, `... which is:- >>${fullEpoch}<<`);
746
- returnDate = fullEpoch.getTime();
747
- }
748
- else {
749
- throw dateTokenPrep.errInvalidDateVerb;
750
- }
751
- }
752
- return returnDate;
753
- }
754
- static doDateToken(state, tokenBody) {
755
- let date = new Date().getTime();
756
- const dateTokenPrep = this.doDateTokenPreamble(date, state, tokenBody);
757
- switch (dateTokenPrep.verb) {
758
- case "timezoneoffset": {
759
- return this.getOffset(state, date);
760
- }
761
- case "addworkingdays":
762
- case "followingworkingday":
763
- case "nextpublicholiday": {
764
- const errorString = `${dateTokenPrep.verb} uses asynchoronous calls. Use Detokenise.asyncDo`;
765
- index_1.Log.writeLine(index_1.LogLevels.Error, errorString);
766
- throw `Detokeniser: ${errorString}`;
767
- }
768
- default:
769
- date = this.doDateTokenNonAsyncVerbs(date, dateTokenPrep);
770
- }
771
- return this.doDateTokenPostamble(dateTokenPrep.offsetAndFormat, date, dateTokenPrep.stateIANAZone);
772
- }
773
- static async asyncDoDateToken(state, tokenBody) {
774
- let date = new Date().getTime();
775
- index_1.Log.writeLine(index_1.LogLevels.FrameworkDebug, `asyncDoDateToken - Calling preAmble: Dates [${date}], State [${state}], Token Body [${tokenBody}]`);
776
- const dateTokenPrep = this.doDateTokenPreamble(date, state, tokenBody);
777
- index_1.Log.writeLine(index_1.LogLevels.FrameworkDebug, `asyncDoDateToken - Back from doDateTokenPreamble. Verb ${dateTokenPrep.verb}`);
778
- switch (dateTokenPrep.verb) {
779
- case "timezoneoffset": {
780
- return this.getOffset(state, date);
781
- }
782
- case "addworkingdays": {
783
- // Adds days to current date not counting public holidays or weekends
784
- try {
785
- if (index_1.Utils.isNullOrUndefined(dateTokenPrep.params)) {
786
- throw new Error(dateTokenPrep.errAddWorkingDaysParams);
787
- }
788
- const paramArray = dateTokenPrep.params.split(",");
789
- if (paramArray.length < 1) {
790
- throw new Error(dateTokenPrep.errAddWorkingDaysParams);
791
- }
792
- const numberOfDays = Number(paramArray[0]);
793
- const ascending = !(numberOfDays < 0);
794
- // Get the date not counting weekends
795
- const startDate = date;
796
- let endDate = startDate;
797
- let daysRemaining = numberOfDays;
798
- while (daysRemaining != 0) {
799
- endDate = (0, date_fns_1.addDays)(endDate, ascending ? +1 : -1).getTime();
800
- while ((await PublicHolidays.isDatePublicHoliday(endDate, state)) ||
801
- Number(dateTokenPrep.stateIANAZone ? (0, date_fns_tz_1.formatInTimeZone)(endDate, dateTokenPrep.stateIANAZone, "e") : (0, date_fns_1.format)(endDate, "e")) == 1 ||
802
- Number(dateTokenPrep.stateIANAZone ? (0, date_fns_tz_1.formatInTimeZone)(endDate, dateTokenPrep.stateIANAZone, "e") : (0, date_fns_1.format)(endDate, "e")) == 7) {
803
- endDate = (0, date_fns_1.addDays)(endDate, ascending ? +1 : -1).getTime();
804
- }
805
- daysRemaining += ascending ? -1 : 1;
806
- }
807
- date = endDate;
808
- }
809
- catch (err) {
810
- index_1.Log.writeLine(index_1.LogLevels.Error, `Error processing addworkingdays: ${err?.message ?? "<Unknown error>"}`);
811
- }
812
- break;
813
- }
814
- case "followingworkingday": {
815
- if (index_1.Utils.isNullOrUndefined(dateTokenPrep.params)) {
816
- throw new Error(dateTokenPrep.errFollowingDayParams);
817
- }
818
- else {
819
- // So, first get the followingday...
820
- const body = `followingDay(${dateTokenPrep.params});epoch`;
821
- index_1.Log.writeLine(index_1.LogLevels.FrameworkDebug, `asyncDoDateToken - Calling doDateToken for followingDay. State = [${state}]. Body = [${body}]`);
822
- let workingDate = Number(this.doDateToken(state, body));
823
- index_1.Log.writeLine(index_1.LogLevels.FrameworkDebug, `got from doDateToken followingDay. workingDate = ${workingDate}`);
824
- // Then, check it is not a weekend or public holiday anywhere. Move forward if it is...
825
- while (Number(dateTokenPrep.stateIANAZone ? (0, date_fns_tz_1.formatInTimeZone)(workingDate, dateTokenPrep.stateIANAZone, "e") : (0, date_fns_1.format)(workingDate, "e")) == 7 ||
826
- Number(dateTokenPrep.stateIANAZone ? (0, date_fns_tz_1.formatInTimeZone)(workingDate, dateTokenPrep.stateIANAZone, "e") : (0, date_fns_1.format)(workingDate, "e")) == 1 ||
827
- (await PublicHolidays.isDatePublicHoliday(workingDate, state))) {
828
- workingDate = (0, date_fns_1.addDays)(workingDate, 1).getTime();
829
- }
830
- date = workingDate;
831
- }
832
- break;
833
- }
834
- case "nextpublicholiday": {
835
- if (index_1.Utils.isNullOrUndefined(dateTokenPrep.params)) {
836
- throw new Error(dateTokenPrep.errNextPublicHoldayParams);
837
- }
838
- else {
839
- const paramArray = dateTokenPrep.params.split(",");
840
- if (paramArray.length != 2) {
841
- throw new Error(dateTokenPrep.errNextPublicHoldayParams);
842
- }
843
- else {
844
- date = (await this.getNextPublicHoliday(Number(paramArray[0]), state, Number(paramArray[1]))).getTime();
845
- }
846
- }
847
- break;
848
- }
849
- default:
850
- date = this.doDateTokenNonAsyncVerbs(date, dateTokenPrep);
851
- }
852
- return this.doDateTokenPostamble(dateTokenPrep.offsetAndFormat, date, dateTokenPrep.stateIANAZone);
853
- }
854
- static async getNextPublicHoliday(dateEpoch, state, maxDaysAway) {
855
- // So, to do this we get all public holidays upto maxDaysAway; first holiday is day we want! easy!!!
856
- if (state == undefined) {
857
- const errMsg = "Cannot get next public holiday, State not defined (Use {Date(<state>);......})";
858
- index_1.Log.writeLine(index_1.LogLevels.Error, errMsg);
859
- throw new Error(errMsg);
860
- }
861
- const startDate = new Date(dateEpoch).getTime();
862
- const endDate = (0, date_fns_1.addDays)(startDate, maxDaysAway).getTime();
863
- const stateIANAZone = PublicHolidays.getIANAZone(state);
864
- const nextPublicHoliday = await PublicHolidays.getFirstPublicHolidayBetweenDates(startDate, endDate, state);
865
- if (index_1.Utils.isNullOrUndefined(nextPublicHoliday)) {
866
- throw new Error(`No public holiday found between ${(0, date_fns_tz_1.formatInTimeZone)(startDate, stateIANAZone, "YYYY-MM-dd")} and ${(0, date_fns_tz_1.formatInTimeZone)(startDate, stateIANAZone, "YYYY-MM-dd")} for State: ${state} (${stateIANAZone})`);
867
- }
868
- index_1.Log.writeLine(index_1.LogLevels.FrameworkInformation, `Found Public holiday: ${(0, date_fns_tz_1.formatInTimeZone)(nextPublicHoliday.date, stateIANAZone, "yyyy-MM-dd")} State: ${state} (${stateIANAZone})`);
869
- return nextPublicHoliday.date;
870
- }
871
- static getFollowingDay(currentDateEpoch, requiredDayOfTheWeek, errorString) {
872
- const dayName = requiredDayOfTheWeek.toLowerCase();
873
- const requiredDayOfWeek = ["monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"].findIndex((item) => {
874
- if (item == dayName)
875
- return true;
876
- return false;
877
- });
878
- if (requiredDayOfWeek == -1)
879
- throw new Error(errorString);
880
- const actualDayOfWeek = new Date(currentDateEpoch).getUTCDay() - 1;
881
- if (actualDayOfWeek < requiredDayOfWeek) {
882
- return (0, date_fns_1.addDays)(currentDateEpoch, requiredDayOfWeek - actualDayOfWeek);
883
- }
884
- else {
885
- return (0, date_fns_1.addDays)(currentDateEpoch, requiredDayOfWeek + 7 - actualDayOfWeek);
886
- }
887
- }
888
- static getParsedDateOffset(numString, errorMessage) {
889
- if (index_1.Utils.isNullOrUndefined(numString)) {
890
- throw `${errorMessage} Got [No Params!]`;
891
- }
892
- else {
893
- const offsetValue = parseInt(numString?.trim());
894
- if (isNaN(offsetValue))
895
- throw `${errorMessage} Got [${numString?.trim()}]`;
896
- return offsetValue;
897
- }
898
- }
899
- static doRandomDate(maxAndMinDates) {
900
- const maxAndMin = maxAndMinDates.split(",");
901
- if (maxAndMin.length != 2 || Number.isNaN(+maxAndMin[0]) || Number.isNaN(+maxAndMin[1]))
902
- throw new Error(`Invalid Maximum and Minimum dates. Expect {random;date(fromEpoch,toEpoch);<format>}. Max/min was: [${maxAndMinDates}]`);
903
- const minDate = Number(maxAndMin[0]);
904
- const maxDate = Number(maxAndMin[1]);
905
- if (minDate > maxDate)
906
- throw new Error(`Minimum date greater than maximum!! Max/min was: [${maxAndMinDates}]`);
907
- return minDate + Math.abs(index_1.Utils.getRandomInt(0, this.numberOfDays(minDate, maxDate) - 1)) * 1000 * 60 * 60 * 24;
908
- }
909
- static getOffset(state, dateOfOffset) {
910
- if (state == undefined) {
911
- const errMsg = "Unable to get timezone offset as no state provided. Expect {Date(<state>);TimezoneOffset}";
912
- index_1.Log.writeLine(index_1.LogLevels.Error, errMsg);
913
- throw new Error(errMsg);
914
- }
915
- const offsetMilliseconds = (0, date_fns_tz_1.getTimezoneOffset)(PublicHolidays.getIANAZone(state), dateOfOffset);
916
- const offsetSeconds = Math.floor(offsetMilliseconds / 1000);
917
- const offsetHours = (0, date_fns_1.secondsToHours)(offsetSeconds);
918
- const offsetMinutes = (0, date_fns_1.secondsToMinutes)(offsetSeconds - offsetHours * 3600);
919
- const offset = (offsetSeconds < 0 ? "-" : "+") + index_1.Utils.pad(offsetHours, 2) + index_1.Utils.pad(offsetMinutes, 2);
920
- index_1.Log.writeLine(index_1.LogLevels.FrameworkInformation, `Got current offset (${offset}) from UTC for [${state}]`);
921
- return offset;
922
- }
923
- static numberOfDays(minDate, maxDate) {
924
- index_1.Log.writeLine(index_1.LogLevels.FrameworkDebug, `Min date [${minDate}], Max date[${maxDate}]`);
925
- const MS_PER_DAY = 1000 * 60 * 60 * 24;
926
- const numberOfDays = Math.floor(Math.abs((minDate - maxDate) / MS_PER_DAY)) + 1;
927
- index_1.Log.writeLine(index_1.LogLevels.FrameworkDebug, `Min date [${minDate}], Max date[${maxDate}]. Number of days [${numberOfDays}]`);
928
- return numberOfDays;
929
- }
930
- static doRandomFloat(limits) {
931
- const minimumAndMaximum = limits.split(",");
932
- if (minimumAndMaximum.length != 2)
933
- throw new Error(`Invalid Maximum and Minimum floats. Expect {{random.float(min;max),<format>}}. Max/min was: [${limits}]`);
934
- const min = parseFloat(minimumAndMaximum[0]);
935
- const max = parseFloat(minimumAndMaximum[1]);
936
- if (isNaN(min))
937
- throw new Error(`Invalid Minimum float. Expect {{random.float(min;max),<format>}}. Max/min was: [${limits}]`);
938
- if (isNaN(max))
939
- throw new Error(`Invalid Maximum float. Expect {{random.float(min;max),<format>}}. Max/min was: [${limits}]`);
940
- return index_1.Utils.getRandomFloat(min, max);
941
- }
942
- }
943
- exports.Detokeniser = Detokeniser;
944
- Detokeniser._endTokenChar = "]]";
945
- Detokeniser._startTokenChar = "[[";
946
- Detokeniser.EscapeChar = "/";
947
- Detokeniser._delimiter = "|";
948
- Detokeniser._callbacks = undefined;
949
- class InnermostToken {
950
- constructor(inputString, StartTokenChar, EndTokenChar, EscapeChar) {
951
- let startIndex = -1;
952
- let endIndex = -1;
953
- this.escapeChar = EscapeChar;
954
- // Find the first (leftmost) non-escaped end token sequence. Because we search left-to-right this naturally
955
- // gives us the innermost token when tokens are nested.
956
- for (let index = 0; index <= inputString.length - EndTokenChar.length; index++) {
957
- if (inputString.startsWith(EndTokenChar, index) && !this.isEscaped(inputString, index)) {
958
- endIndex = index;
959
- break;
960
- }
961
- }
962
- // Now scan right-to-left from just before the end sequence to find the matching non-escaped start sequence.
963
- if (endIndex !== -1) {
964
- for (let index = endIndex - 1; index >= 0; index--) {
965
- if (inputString.startsWith(StartTokenChar, index) && !this.isEscaped(inputString, index)) {
966
- startIndex = index;
967
- break;
968
- }
969
- }
970
- }
971
- if (startIndex !== -1 && endIndex !== -1) {
972
- this.tokenPreamble = inputString.substring(0, startIndex);
973
- this.tokenPostamble = inputString.substring(endIndex + EndTokenChar.length);
974
- this._childToken = inputString.substring(startIndex, endIndex); // includes StartTokenChar, excludes EndTokenChar
975
- }
976
- else {
977
- this.tokenPreamble = inputString;
978
- this.tokenPostamble = "";
979
- this._childToken = "";
980
- }
981
- this.foundToken = startIndex !== -1 && endIndex !== -1;
982
- }
983
- get preamble() {
984
- return this.tokenPreamble;
985
- }
986
- get postamble() {
987
- return this.tokenPostamble;
988
- }
989
- get childToken() {
990
- return this._childToken;
991
- }
992
- get hasToken() {
993
- return this.foundToken;
994
- }
995
- isEscaped(fullString, positionToTest) {
996
- let index;
997
- let escapeCharCount = 0;
998
- let returnIsEscaped = false;
999
- if (positionToTest > 0) {
1000
- index = positionToTest;
1001
- while (!(--index < 0)) {
1002
- if (fullString[index] == this.escapeChar) {
1003
- escapeCharCount++;
1004
- }
1005
- else {
1006
- break;
1007
- }
1008
- }
1009
- if (escapeCharCount % 2 == 1) {
1010
- returnIsEscaped = true;
1011
- }
1012
- }
1013
- return returnIsEscaped;
1014
- }
1015
- }