@animaj/ts-utils 0.0.0-stage → 1.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 ADDED
@@ -0,0 +1,16 @@
1
+ ISC License
2
+
3
+ Copyright (c) 2020-2026 Julien Khong
4
+ Copyright (c) 2026 Animaj
5
+
6
+ Permission to use, copy, modify, and/or distribute this software for any
7
+ purpose with or without fee is hereby granted, provided that the above
8
+ copyright notice and this permission notice appear in all copies.
9
+
10
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
11
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
12
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
13
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
14
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
15
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
16
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,284 @@
1
- # Temporary Holding Version
1
+ # @animaj/ts-utils
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Provides syntactic sugar and shortcuts to bullet-proof your TypeScript/JavaScript code.
4
+
5
+ ## Contents
6
+
7
+ - [Install](#install)
8
+ - [DevUtils](#devutils) — `isSet` · `isNotSet` · `isAllSet` · `isOneNotSet` · `isEmpty` · `getFieldValue` · `getChildFieldValue` · `makeSingleton` · `debounce` · `getError`
9
+ - [StringUtils](#stringutils) — `isBlank` · `capitalize` · `uncapitalize` · `replaceTrailing` · `replaceLeading`
10
+ - [ArrayUtils](#arrayutils) — `isEmpty` · `getField`
11
+ - [DateUtils](#dateutils) — `addDays` · `addMinutes` · `addSeconds`
12
+ - [FunctionUtils](#functionutils) — `isAsync`
13
+ - [InMemory](#inmemory-cache) — TTL-based in-memory cache
14
+ - [TestUtils](#testutils) — async test helpers
15
+
16
+ ---
17
+
18
+ ## Install
19
+
20
+ ```sh
21
+ npm install @animaj/ts-utils
22
+ ```
23
+
24
+ ---
25
+
26
+ ## DevUtils
27
+
28
+ ### `isSet(value)`
29
+ Returns `true` if the value is not `null`, not `undefined`, and not the string `'undefined'`.
30
+
31
+ ```ts
32
+ import { DevUtils } from '@animaj/ts-utils';
33
+
34
+ DevUtils.isSet(null); // false
35
+ DevUtils.isSet(undefined); // false
36
+ DevUtils.isSet('undefined'); // false
37
+ DevUtils.isSet(0); // true
38
+ DevUtils.isSet(''); // true
39
+ DevUtils.isSet(false); // true
40
+ ```
41
+
42
+ ### `isNotSet(value)`
43
+ Inverse of `isSet`.
44
+
45
+ ### `isAllSet(memberNames, data)` / `isOneNotSet(memberNames, data)`
46
+ Check if all (or at least one) named fields of an object are set.
47
+
48
+ ```ts
49
+ DevUtils.isAllSet(['id', 'name'], { id: 1, name: 'foo' }); // true
50
+ DevUtils.isOneNotSet(['id', 'name'], { id: 1 }); // true
51
+ ```
52
+
53
+ ### `isEmpty(value)`
54
+ Returns `true` for `null`, `undefined`, empty/blank strings, empty arrays, and empty objects.
55
+
56
+ ```ts
57
+ DevUtils.isEmpty(null); // true
58
+ DevUtils.isEmpty(' '); // true
59
+ DevUtils.isEmpty([]); // true
60
+ DevUtils.isEmpty({}); // true
61
+ DevUtils.isEmpty([1]); // false
62
+ DevUtils.isEmpty({ a: 1 }); // false
63
+ ```
64
+
65
+ ### `getFieldValue(struct, fieldname, default)`
66
+ Returns the field value if set, otherwise the default.
67
+
68
+ ```ts
69
+ DevUtils.getFieldValue({ age: 30 }, 'age', 0); // 30
70
+ DevUtils.getFieldValue({}, 'age', 0); // 0
71
+ DevUtils.getFieldValue(null, 'age', 0); // 0
72
+ ```
73
+
74
+ ### `getChildFieldValue(struct, fieldnames, default)`
75
+ Traverses nested objects safely, returning the default if any level is missing.
76
+
77
+ ```ts
78
+ const obj = { a: { b: { c: 'deep' } } };
79
+ DevUtils.getChildFieldValue(obj, ['a', 'b', 'c'], 'default'); // 'deep'
80
+ DevUtils.getChildFieldValue(obj, ['a', 'x', 'c'], 'default'); // 'default'
81
+ ```
82
+
83
+ ### `makeSingleton(asyncFactory)`
84
+ Wraps an async factory so it is called only once. Subsequent calls return the cached instance.
85
+
86
+ ```ts
87
+ const getDb = DevUtils.makeSingleton(() => connectToDatabase());
88
+ await getDb(); // connects
89
+ await getDb(); // returns cached connection
90
+ ```
91
+
92
+ ### `debounce(fn, waitMs)`
93
+ Returns a debounced version of `fn` that fires only after `waitMs` ms of inactivity.
94
+
95
+ ```ts
96
+ const save = DevUtils.debounce(() => api.save(), 300);
97
+ save(); save(); save(); // only the last call fires after 300ms
98
+ ```
99
+
100
+ ### `getError(e)`
101
+ Normalizes any thrown value to an `Error` instance. Useful in `catch (e: unknown)` blocks.
102
+
103
+ ```ts
104
+ try { ... } catch (e: unknown) {
105
+ const err = DevUtils.getError(e); // always an Error
106
+ }
107
+ ```
108
+
109
+ ---
110
+
111
+ ## StringUtils
112
+
113
+ ```ts
114
+ import { StringUtils } from '@animaj/ts-utils';
115
+ ```
116
+
117
+ | Method | Description |
118
+ |---|---|
119
+ | `isBlank(str)` | `true` if string is null, undefined, or contains only whitespace |
120
+ | `capitalize(word)` | Uppercases the first character |
121
+ | `uncapitalize(word)` | Lowercases the first character |
122
+ | `replaceTrailing(value, char, replacement)` | Replaces trailing occurrences of `char` |
123
+ | `replaceLeading(value, char, replacement)` | Replaces leading occurrences of `char` |
124
+
125
+ ```ts
126
+ StringUtils.capitalize('hello'); // 'Hello'
127
+ StringUtils.uncapitalize('World'); // 'world'
128
+ StringUtils.replaceTrailing('foo///', '/', ''); // 'foo'
129
+ StringUtils.replaceLeading('///foo', '/', ''); // 'foo'
130
+ ```
131
+
132
+ ---
133
+
134
+ ## ArrayUtils
135
+
136
+ ```ts
137
+ import { ArrayUtils } from '@animaj/ts-utils';
138
+ ```
139
+
140
+ | Method | Description |
141
+ |---|---|
142
+ | `isEmpty(array)` | `true` if array is null, undefined, or has no elements |
143
+ | `getField(array, index, fieldname, default)` | Safely reads a field from an element at a given index |
144
+
145
+ ```ts
146
+ ArrayUtils.isEmpty([]); // true
147
+ ArrayUtils.getField([{ id: 1 }], 0, 'id', null); // 1
148
+ ArrayUtils.getField([], 0, 'id', null); // null
149
+ ```
150
+
151
+ ---
152
+
153
+ ## DateUtils
154
+
155
+ ```ts
156
+ import { DateUtils } from '@animaj/ts-utils';
157
+ ```
158
+
159
+ | Method | Description |
160
+ |---|---|
161
+ | `addDays(date, n)` | Returns a new Date with `n` days added |
162
+ | `addMinutes(date, n)` | Returns a new Date with `n` minutes added |
163
+ | `addSeconds(date, n)` | Returns a new Date with `n` seconds added |
164
+
165
+ Accepts negative values to subtract. The original date is never mutated.
166
+
167
+ ```ts
168
+ DateUtils.addDays(new Date(), 7); // one week from now
169
+ DateUtils.addSeconds(new Date(), -30); // 30 seconds ago
170
+ ```
171
+
172
+ ---
173
+
174
+ ## FunctionUtils
175
+
176
+ ```ts
177
+ import { FunctionUtils } from '@animaj/ts-utils';
178
+ ```
179
+
180
+ | Method | Description |
181
+ |---|---|
182
+ | `isAsync(fn)` | `true` if the function is async |
183
+
184
+ ```ts
185
+ FunctionUtils.isAsync(async () => {}); // true
186
+ FunctionUtils.isAsync(() => {}); // false
187
+ ```
188
+
189
+ ---
190
+
191
+ ## InMemory (cache)
192
+
193
+ A TTL-based in-memory cache with per-entry TTL support and automatic background cleanup.
194
+
195
+ ```ts
196
+ import { InMemory } from '@animaj/ts-utils';
197
+ ```
198
+
199
+ ### Constructor options
200
+
201
+ | Option | Default | Description |
202
+ |---|---|---|
203
+ | `ttlInSec` | 3600 (1h) | Default TTL for entries |
204
+ | `autoTtlCleanIntervalInSec` | 3600 (1h) | How often expired entries are swept |
205
+ | `autoWipeAllIntervalInSec` | disabled | How often the entire cache is cleared (opt-in) |
206
+
207
+ ### Methods
208
+
209
+ #### `add({ id, d, ttlInSec? })`
210
+ Adds or overwrites an entry. `ttlInSec` overrides the global default for this entry.
211
+
212
+ ```ts
213
+ const cache = new InMemory<User>({ ttlInSec: 300 });
214
+ await cache.add({ id: 'u1', d: user });
215
+ await cache.add({ id: 'u2', d: user, ttlInSec: 60 }); // custom TTL
216
+ ```
217
+
218
+ #### `fetch(id)`
219
+ Returns the cached value or `undefined` if missing or expired.
220
+
221
+ ```ts
222
+ const user = await cache.fetch('u1'); // User | undefined
223
+ ```
224
+
225
+ #### `fetchMultiple(ids)`
226
+ Splits a list of ids into cached hits and ids that still need to be fetched from the source.
227
+
228
+ ```ts
229
+ const { cachedData, idsToRequest } = await cache.fetchMultiple(['u1', 'u2', 'u3']);
230
+ // cachedData → [{ id: 'u1', data: User }, ...]
231
+ // idsToRequest → ['u3']
232
+
233
+ const fresh = await api.getUsers(idsToRequest);
234
+ ```
235
+
236
+ #### `remove(id)`
237
+ Removes a single entry immediately.
238
+
239
+ ```ts
240
+ await cache.remove('u1');
241
+ ```
242
+
243
+ #### `forceTtlClean()`
244
+ Triggers an immediate sweep to remove all expired entries.
245
+
246
+ #### `forceWipeAll()`
247
+ Clears all entries regardless of TTL.
248
+
249
+ #### `destroy()`
250
+ Stops background intervals and clears the cache. Call this when the cache instance is no longer needed to prevent timer leaks.
251
+
252
+ ```ts
253
+ await cache.destroy();
254
+ ```
255
+
256
+ ---
257
+
258
+ ## TestUtils
259
+
260
+ Helpers for structuring async tests with setup / run / assert phases.
261
+
262
+ ```ts
263
+ import { TestUtils } from '@animaj/ts-utils';
264
+ ```
265
+
266
+ ### `testExpectedSuccess({ setPreconditions, run, checkPostconditions })`
267
+ For testing a happy path. Returns `{ success: true }` or `{ success: false, error }`.
268
+
269
+ ```ts
270
+ const result = await TestUtils.testExpectedSuccess({
271
+ setPreconditions: async () => buildInput(),
272
+ run: async (input) => myService.process(input),
273
+ checkPostconditions: async (output) => {
274
+ expect(output.status).toBe('ok');
275
+ },
276
+ });
277
+ expect(result.success).toBe(true);
278
+ ```
279
+
280
+ ### `testExpectedError({ setPreconditions, run, checkError, checkPostconditions })`
281
+ For testing flows that return an error value (no exception thrown).
282
+
283
+ ### `testExpectedException({ setPreconditions, run, checkError, checkPostconditions })`
284
+ For testing flows that are expected to throw.
@@ -0,0 +1,4 @@
1
+ export declare class ArrayUtils {
2
+ static isEmpty(array: any[]): boolean;
3
+ static getField(array: any[], indexValue: any, fieldname: string, defval: any): any;
4
+ }
package/dist/array.js ADDED
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ArrayUtils = void 0;
4
+ const dev_1 = require("./dev");
5
+ class ArrayUtils {
6
+ static isEmpty(array) {
7
+ return dev_1.DevUtils.isEmpty(array);
8
+ }
9
+ static getField(array, indexValue, fieldname, defval) {
10
+ if (!Array.isArray(array)) {
11
+ return defval;
12
+ }
13
+ if (dev_1.DevUtils.isSet(array) && dev_1.DevUtils.isSet(array[indexValue]) && dev_1.DevUtils.isSet(array[indexValue][fieldname])) {
14
+ return array[indexValue][fieldname];
15
+ }
16
+ return defval;
17
+ }
18
+ }
19
+ exports.ArrayUtils = ArrayUtils;
@@ -0,0 +1,19 @@
1
+ export interface CacheManager<Data> {
2
+ fetch: (id: string) => Promise<Data | undefined>;
3
+ add: (o: {
4
+ id: string;
5
+ d: Data;
6
+ ttlInSec?: number;
7
+ }) => Promise<void>;
8
+ remove: (id: string) => Promise<void>;
9
+ fetchMultiple: (ids: string[]) => Promise<{
10
+ idsToRequest: string[];
11
+ cachedData: Array<{
12
+ id: string;
13
+ data: Data;
14
+ }>;
15
+ }>;
16
+ forceTtlClean: () => Promise<void>;
17
+ forceWipeAll: () => Promise<void>;
18
+ destroy: () => Promise<void>;
19
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,35 @@
1
+ import { CacheManager } from './cache-manager';
2
+ export declare class InMemory<Data> implements CacheManager<Data> {
3
+ private readonly cache;
4
+ private readonly ttlInSec;
5
+ private readonly autoTtlCleanIntervalInSec;
6
+ private autoTtlCleanIntervalHandle;
7
+ private readonly autoWipeAllIntervalInSec;
8
+ private autoWipeAllIntervalHandle;
9
+ constructor(options?: {
10
+ ttlInSec?: number;
11
+ autoTtlCleanIntervalInSec?: number;
12
+ autoWipeAllIntervalInSec?: number;
13
+ });
14
+ fetchMultiple(ids: string[]): Promise<{
15
+ idsToRequest: string[];
16
+ cachedData: Array<{
17
+ id: string;
18
+ data: Data;
19
+ }>;
20
+ }>;
21
+ fetch(id: string): Promise<Data | undefined>;
22
+ add(o: {
23
+ id: string;
24
+ d: Data;
25
+ ttlInSec?: number;
26
+ }): Promise<void>;
27
+ remove(id: string): Promise<void>;
28
+ forceTtlClean(): Promise<void>;
29
+ private setAutoTtlCleanIntervalHandle;
30
+ private ttlClean;
31
+ forceWipeAll(): Promise<void>;
32
+ private setWipeAllIntervalHandle;
33
+ private wipeAll;
34
+ destroy(): Promise<void>;
35
+ }
@@ -0,0 +1,121 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.InMemory = void 0;
13
+ const dates_1 = require("../dates");
14
+ const oneMinInSec = 60;
15
+ const _60minInSec = 60 * oneMinInSec;
16
+ const MAX_INTEGER_32 = Math.pow(2, 31) - 1;
17
+ class InMemory {
18
+ constructor(options) {
19
+ var _a, _b;
20
+ this.cache = new Map();
21
+ this.ttlInSec = _60minInSec;
22
+ this.autoTtlCleanIntervalInSec = _60minInSec;
23
+ if (options !== undefined) {
24
+ this.ttlInSec = (_a = options.ttlInSec) !== null && _a !== void 0 ? _a : this.ttlInSec;
25
+ this.autoTtlCleanIntervalInSec = (_b = options.autoTtlCleanIntervalInSec) !== null && _b !== void 0 ? _b : this.autoTtlCleanIntervalInSec;
26
+ this.autoWipeAllIntervalInSec = options.autoWipeAllIntervalInSec;
27
+ }
28
+ this.setAutoTtlCleanIntervalHandle();
29
+ this.setWipeAllIntervalHandle();
30
+ }
31
+ fetchMultiple(ids) {
32
+ return __awaiter(this, void 0, void 0, function* () {
33
+ const idsToRequest = [];
34
+ const cachedData = [];
35
+ yield Promise.all(ids.map((id) => __awaiter(this, void 0, void 0, function* () {
36
+ const data = yield this.fetch(id);
37
+ if (data !== undefined) {
38
+ cachedData.push({ id, data });
39
+ }
40
+ else {
41
+ idsToRequest.push(id);
42
+ }
43
+ })));
44
+ return {
45
+ idsToRequest,
46
+ cachedData,
47
+ };
48
+ });
49
+ }
50
+ fetch(id) {
51
+ return __awaiter(this, void 0, void 0, function* () {
52
+ const data = this.cache.get(id);
53
+ if (data === undefined) {
54
+ return undefined;
55
+ }
56
+ const timeLimit = dates_1.DateUtils.addSeconds(new Date(), -data.ttlInSec);
57
+ const isValidData = data.timestamp > timeLimit.getTime();
58
+ if (isValidData) {
59
+ return data.data;
60
+ }
61
+ this.remove(id);
62
+ return undefined;
63
+ });
64
+ }
65
+ add(o) {
66
+ return __awaiter(this, void 0, void 0, function* () {
67
+ var _a;
68
+ const ttlInSec = (_a = o.ttlInSec) !== null && _a !== void 0 ? _a : this.ttlInSec;
69
+ this.cache.set(o.id, { data: o.d, timestamp: new Date().getTime(), ttlInSec });
70
+ });
71
+ }
72
+ remove(id) {
73
+ return __awaiter(this, void 0, void 0, function* () {
74
+ this.cache.delete(id);
75
+ });
76
+ }
77
+ forceTtlClean() {
78
+ return __awaiter(this, void 0, void 0, function* () {
79
+ this.ttlClean();
80
+ this.setAutoTtlCleanIntervalHandle();
81
+ });
82
+ }
83
+ setAutoTtlCleanIntervalHandle() {
84
+ clearInterval(this.autoTtlCleanIntervalHandle);
85
+ this.autoTtlCleanIntervalHandle = setInterval(() => this.ttlClean(), toSafeIntervalMs(this.autoTtlCleanIntervalInSec));
86
+ }
87
+ ttlClean() {
88
+ this.cache.forEach((data, id) => {
89
+ const timeLimit = dates_1.DateUtils.addSeconds(new Date(), -data.ttlInSec);
90
+ if (data.timestamp <= timeLimit.getTime()) {
91
+ this.cache.delete(id);
92
+ }
93
+ });
94
+ }
95
+ forceWipeAll() {
96
+ return __awaiter(this, void 0, void 0, function* () {
97
+ this.wipeAll();
98
+ this.setWipeAllIntervalHandle();
99
+ });
100
+ }
101
+ setWipeAllIntervalHandle() {
102
+ clearInterval(this.autoWipeAllIntervalHandle);
103
+ if (this.autoWipeAllIntervalInSec !== undefined) {
104
+ this.autoWipeAllIntervalHandle = setInterval(() => this.wipeAll(), toSafeIntervalMs(this.autoWipeAllIntervalInSec));
105
+ }
106
+ }
107
+ wipeAll() {
108
+ this.cache.clear();
109
+ }
110
+ destroy() {
111
+ return __awaiter(this, void 0, void 0, function* () {
112
+ clearInterval(this.autoTtlCleanIntervalHandle);
113
+ clearInterval(this.autoWipeAllIntervalHandle);
114
+ this.cache.clear();
115
+ });
116
+ }
117
+ }
118
+ exports.InMemory = InMemory;
119
+ function toSafeIntervalMs(sec) {
120
+ return Math.min(sec * 1000, MAX_INTEGER_32);
121
+ }
@@ -0,0 +1,5 @@
1
+ export declare class DateUtils {
2
+ static addDays(d: Readonly<Date>, quantity: number): Date;
3
+ static addMinutes(d: Readonly<Date>, quantity: number): Date;
4
+ static addSeconds(d: Readonly<Date>, quantity: number): Date;
5
+ }
package/dist/dates.js ADDED
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DateUtils = void 0;
4
+ class DateUtils {
5
+ static addDays(d, quantity) {
6
+ const newDate = new Date(d);
7
+ newDate.setDate(newDate.getDate() + quantity);
8
+ return newDate;
9
+ }
10
+ static addMinutes(d, quantity) {
11
+ const newDate = new Date(d);
12
+ newDate.setMinutes(newDate.getMinutes() + quantity);
13
+ return newDate;
14
+ }
15
+ static addSeconds(d, quantity) {
16
+ const newDate = new Date(d);
17
+ newDate.setSeconds(newDate.getSeconds() + quantity);
18
+ return newDate;
19
+ }
20
+ }
21
+ exports.DateUtils = DateUtils;
package/dist/dev.d.ts ADDED
@@ -0,0 +1,20 @@
1
+ export declare class DevUtils {
2
+ static isSet<T = any>(obj: T | null | undefined): obj is T;
3
+ static isNotSet<T = any>(obj: T | null | undefined): obj is null | undefined;
4
+ static isOneNotSet(memberNames: string[], data: any): boolean;
5
+ static isAllSet(memberNames: string[], data: any): boolean;
6
+ static getFieldValue<T>(struct: Record<string, any>, fieldname: string, defval: T): T;
7
+ private static isEmptyArray;
8
+ private static isEmptyString;
9
+ static getChildFieldValue<T>(struct: Record<string, any>, fieldnames: string[], defval: T): T;
10
+ static isEmpty(value: any): boolean;
11
+ static makeSingleton<T>(callback: () => Promise<T>): () => Promise<T>;
12
+ /**
13
+ *
14
+ * @param func the function to execute
15
+ * @param waitForInMs the debounce time in milliseconds
16
+ * @returns
17
+ */
18
+ static debounce<F extends (...args: any[]) => void>(func: F, waitForInMs: number): (...args: Parameters<F>) => void;
19
+ static getError(e: unknown): Error;
20
+ }
package/dist/dev.js ADDED
@@ -0,0 +1,123 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DevUtils = void 0;
4
+ class DevUtils {
5
+ static isSet(obj) {
6
+ const res = undefined !== obj && null !== obj;
7
+ if (typeof obj === 'string') {
8
+ return res && 'undefined' !== obj;
9
+ }
10
+ return res;
11
+ }
12
+ static isNotSet(obj) {
13
+ return !this.isSet(obj);
14
+ }
15
+ static isOneNotSet(memberNames, data) {
16
+ let ret = false;
17
+ for (const member of memberNames) {
18
+ if (this.isNotSet(data[member])) {
19
+ ret = true;
20
+ break;
21
+ }
22
+ }
23
+ return ret;
24
+ }
25
+ static isAllSet(memberNames, data) {
26
+ return !this.isOneNotSet(memberNames, data);
27
+ }
28
+ static getFieldValue(struct, fieldname, defval) {
29
+ if (this.isSet(struct) && this.isSet(struct[fieldname])) {
30
+ return struct[fieldname];
31
+ }
32
+ return defval;
33
+ }
34
+ static isEmptyArray(array) {
35
+ if (!Array.isArray(array)) {
36
+ return true;
37
+ }
38
+ return this.isNotSet(array) || array.length === 0;
39
+ }
40
+ static isEmptyString(str) {
41
+ if ('string' !== typeof str) {
42
+ return true;
43
+ }
44
+ return this.isNotSet(str) || str.trim().length === 0;
45
+ }
46
+ static getChildFieldValue(struct, fieldnames, defval) {
47
+ let val = defval;
48
+ for (const fieldname of fieldnames) {
49
+ val = this.getFieldValue(struct, fieldname, defval);
50
+ if (val === defval) {
51
+ return defval;
52
+ }
53
+ struct = struct[fieldname];
54
+ }
55
+ return val;
56
+ }
57
+ static isEmpty(value) {
58
+ if (this.isNotSet(value)) {
59
+ return true;
60
+ }
61
+ if (Array.isArray(value)) {
62
+ return this.isEmptyArray(value);
63
+ }
64
+ if ('string' === typeof value) {
65
+ return this.isEmptyString(value);
66
+ }
67
+ if ('object' === typeof value) {
68
+ return Object.keys(value).length === 0;
69
+ }
70
+ return false;
71
+ }
72
+ static makeSingleton(callback) {
73
+ let singleton;
74
+ return () => {
75
+ return new Promise((resolve, reject) => {
76
+ if (singleton !== undefined) {
77
+ resolve(singleton);
78
+ return;
79
+ }
80
+ callback()
81
+ .then((sglton) => {
82
+ singleton = sglton;
83
+ resolve(singleton);
84
+ })
85
+ .catch((err) => {
86
+ reject(err);
87
+ });
88
+ });
89
+ };
90
+ }
91
+ /**
92
+ *
93
+ * @param func the function to execute
94
+ * @param waitForInMs the debounce time in milliseconds
95
+ * @returns
96
+ */
97
+ static debounce(func, waitForInMs) {
98
+ let timeout;
99
+ return (...args) => {
100
+ clearTimeout(timeout);
101
+ timeout = setTimeout(() => {
102
+ func(...args);
103
+ }, waitForInMs);
104
+ };
105
+ }
106
+ static getError(e) {
107
+ if (e instanceof Error) {
108
+ return e;
109
+ }
110
+ if (typeof e === 'string') {
111
+ return new Error(e);
112
+ }
113
+ let error = 'unknown error';
114
+ if (e !== undefined && e.message !== undefined) {
115
+ error = e.message;
116
+ }
117
+ if (e !== undefined && e.error !== undefined) {
118
+ error += ' > ' + e.error;
119
+ }
120
+ return new Error(error);
121
+ }
122
+ }
123
+ exports.DevUtils = DevUtils;
@@ -0,0 +1,3 @@
1
+ export declare class FunctionUtils {
2
+ static isAsync(fn: any): boolean;
3
+ }
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FunctionUtils = void 0;
4
+ const dev_1 = require("./dev");
5
+ class FunctionUtils {
6
+ static isAsync(fn) {
7
+ if (dev_1.DevUtils.isNotSet(fn)) {
8
+ return false;
9
+ }
10
+ return fn.toString().includes('__awaiter') || fn.constructor.name === 'AsyncFunction';
11
+ }
12
+ }
13
+ exports.FunctionUtils = FunctionUtils;
@@ -0,0 +1,7 @@
1
+ export { DevUtils } from './dev';
2
+ export { StringUtils } from './string';
3
+ export { ArrayUtils } from './array';
4
+ export { FunctionUtils } from './function';
5
+ export { TestUtils } from './tests';
6
+ export { DateUtils } from './dates';
7
+ export { InMemory } from './cache-manager/in-memory';
package/dist/index.js ADDED
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.InMemory = exports.DateUtils = exports.TestUtils = exports.FunctionUtils = exports.ArrayUtils = exports.StringUtils = exports.DevUtils = void 0;
4
+ var dev_1 = require("./dev");
5
+ Object.defineProperty(exports, "DevUtils", { enumerable: true, get: function () { return dev_1.DevUtils; } });
6
+ var string_1 = require("./string");
7
+ Object.defineProperty(exports, "StringUtils", { enumerable: true, get: function () { return string_1.StringUtils; } });
8
+ var array_1 = require("./array");
9
+ Object.defineProperty(exports, "ArrayUtils", { enumerable: true, get: function () { return array_1.ArrayUtils; } });
10
+ var function_1 = require("./function");
11
+ Object.defineProperty(exports, "FunctionUtils", { enumerable: true, get: function () { return function_1.FunctionUtils; } });
12
+ var tests_1 = require("./tests");
13
+ Object.defineProperty(exports, "TestUtils", { enumerable: true, get: function () { return tests_1.TestUtils; } });
14
+ var dates_1 = require("./dates");
15
+ Object.defineProperty(exports, "DateUtils", { enumerable: true, get: function () { return dates_1.DateUtils; } });
16
+ var in_memory_1 = require("./cache-manager/in-memory");
17
+ Object.defineProperty(exports, "InMemory", { enumerable: true, get: function () { return in_memory_1.InMemory; } });
@@ -0,0 +1,7 @@
1
+ export declare class StringUtils {
2
+ static isBlank(str: string): boolean;
3
+ static replaceTrailing(value: string, charToTrim: string, replacement: string): string;
4
+ static replaceLeading(value: string, charToTrim: string, replacement: string): string;
5
+ static uncapitalize(word: string): string;
6
+ static capitalize(word: string): string;
7
+ }
package/dist/string.js ADDED
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.StringUtils = void 0;
4
+ const dev_1 = require("./dev");
5
+ class StringUtils {
6
+ static isBlank(str) {
7
+ if ('string' !== typeof str) {
8
+ return true;
9
+ }
10
+ return dev_1.DevUtils.isEmpty(str);
11
+ }
12
+ static replaceTrailing(value, charToTrim, replacement) {
13
+ if (dev_1.DevUtils.isNotSet(value) || '' === value) {
14
+ return '';
15
+ }
16
+ const regExp = new RegExp(charToTrim[0] + '+$');
17
+ return value.replace(regExp, replacement);
18
+ }
19
+ static replaceLeading(value, charToTrim, replacement) {
20
+ if (dev_1.DevUtils.isNotSet(value) || '' === value) {
21
+ return '';
22
+ }
23
+ const regExp = new RegExp('^' + charToTrim[0] + '+');
24
+ return value.replace(regExp, replacement);
25
+ }
26
+ static uncapitalize(word) {
27
+ if (dev_1.DevUtils.isNotSet(word) || '' === word) {
28
+ return '';
29
+ }
30
+ const lowercase = word[0].toLowerCase();
31
+ if (word.length > 1) {
32
+ return lowercase + word.slice(1);
33
+ }
34
+ return lowercase;
35
+ }
36
+ static capitalize(word) {
37
+ if (dev_1.DevUtils.isNotSet(word) || '' === word) {
38
+ return '';
39
+ }
40
+ const uppercase = word[0].toUpperCase();
41
+ if (word.length > 1) {
42
+ return uppercase + word.slice(1);
43
+ }
44
+ return uppercase;
45
+ }
46
+ }
47
+ exports.StringUtils = StringUtils;
@@ -0,0 +1,26 @@
1
+ type Result = {
2
+ success: true;
3
+ } | {
4
+ success: false;
5
+ error: Error;
6
+ };
7
+ export declare class TestUtils {
8
+ static testExpectedSuccess<Input, Output>(o: {
9
+ setPreconditions: () => Promise<Input>;
10
+ run: (i: Input) => Promise<Output>;
11
+ checkPostconditions: (o: Output) => Promise<void>;
12
+ }): Promise<Result>;
13
+ static testExpectedError<Input, Output>(o: {
14
+ setPreconditions: () => Promise<Input>;
15
+ run: (i: Input) => Promise<Output>;
16
+ checkError: (o: Output) => Promise<void>;
17
+ checkPostconditions: (o: Output) => Promise<void>;
18
+ }): Promise<Result>;
19
+ static testExpectedException<Input, Output>(o: {
20
+ setPreconditions: () => Promise<Input>;
21
+ run: (i: Input) => Promise<Output>;
22
+ checkError: (e: unknown) => Promise<void>;
23
+ checkPostconditions: () => Promise<void>;
24
+ }): Promise<Result>;
25
+ }
26
+ export {};
package/dist/tests.js ADDED
@@ -0,0 +1,57 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.TestUtils = void 0;
13
+ const dev_1 = require("./dev");
14
+ class TestUtils {
15
+ static testExpectedSuccess(o) {
16
+ return __awaiter(this, void 0, void 0, function* () {
17
+ try {
18
+ const input = yield o.setPreconditions();
19
+ const output = yield o.run(input);
20
+ yield o.checkPostconditions(output);
21
+ return { success: true };
22
+ }
23
+ catch (e) {
24
+ return { success: false, error: dev_1.DevUtils.getError(e) };
25
+ }
26
+ });
27
+ }
28
+ static testExpectedError(o) {
29
+ return __awaiter(this, void 0, void 0, function* () {
30
+ try {
31
+ const input = yield o.setPreconditions();
32
+ const output = yield o.run(input);
33
+ yield o.checkError(output);
34
+ yield o.checkPostconditions(output);
35
+ return { success: true };
36
+ }
37
+ catch (e) {
38
+ return { success: false, error: dev_1.DevUtils.getError(e) };
39
+ }
40
+ });
41
+ }
42
+ static testExpectedException(o) {
43
+ return __awaiter(this, void 0, void 0, function* () {
44
+ try {
45
+ const input = yield o.setPreconditions();
46
+ yield o.run(input);
47
+ return { success: false, error: new Error('No exception thrown') };
48
+ }
49
+ catch (e) {
50
+ yield o.checkError(e);
51
+ yield o.checkPostconditions();
52
+ return { success: true };
53
+ }
54
+ });
55
+ }
56
+ }
57
+ exports.TestUtils = TestUtils;
package/package.json CHANGED
@@ -1,6 +1,49 @@
1
1
  {
2
- "name": "@animaj/ts-utils",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
2
+ "name": "@animaj/ts-utils",
3
+ "version": "1.0.0",
4
+ "description": "Shared TypeScript helpers for Animaj services: null/empty guards, safe nested reads, async singletons, TTL in-memory cache, test helpers",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "files": [
8
+ "dist/**/*"
9
+ ],
10
+ "scripts": {
11
+ "build": "make build",
12
+ "test": "make test",
13
+ "format": "make format",
14
+ "lint": "make lint",
15
+ "prepublishOnly": "npm run lint && npm test",
16
+ "preversion": "npm run lint",
17
+ "version": "npm run format && git add -A src"
18
+ },
19
+ "author": "Animaj",
20
+ "contributors": [
21
+ "Julien Khong (original author)",
22
+ "Stefano (current maintainer)"
23
+ ],
24
+ "license": "ISC",
25
+ "homepage": "https://github.com/animaj-lab/ts-utils#readme",
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "git+https://github.com/animaj-lab/ts-utils.git"
29
+ },
30
+ "bugs": {
31
+ "url": "https://github.com/animaj-lab/ts-utils/issues"
32
+ },
33
+ "publishConfig": {
34
+ "access": "public"
35
+ },
36
+ "devDependencies": {
37
+ "@typescript-eslint/eslint-plugin": "^2.30.0",
38
+ "@typescript-eslint/parser": "^2.30.0",
39
+ "eslint": "^6.8.0",
40
+ "eslint-config-prettier": "^6.11.0",
41
+ "husky": "^4.2.3",
42
+ "minimist": ">=1.2.2",
43
+ "mocha": "^5.1.0",
44
+ "prettier": "^2.0.2",
45
+ "typescript": "^6.0.3",
46
+ "vitest": "4.1.8"
47
+ },
48
+ "keywords": []
49
+ }