@onivoro/isomorphic-common 24.38.2 → 25.0.2

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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @onivoro/isomorphic-common
2
2
 
3
- Common utilities, functions, types, and constants shared between browser and server environments in the Onivoro monorepo. This library provides essential building blocks for TypeScript applications with a focus on type safety and consistent behavior across different runtime environments.
3
+ Utilities, types, and constants shared between browser and server code in the Onivoro monorepo. No runtime dependencies beyond `tslib`.
4
4
 
5
5
  ## Installation
6
6
 
@@ -8,438 +8,320 @@ Common utilities, functions, types, and constants shared between browser and ser
8
8
  npm install @onivoro/isomorphic-common
9
9
  ```
10
10
 
11
- ## Features
12
-
13
- - **Isomorphic Design**: Works identically in browser and Node.js environments
14
- - **String Manipulation**: Case conversion, formatting, sanitization, and validation utilities
15
- - **Array Operations**: Chunking, sorting, deduplication, and transformation functions
16
- - **Date/Time Utilities**: Calendar operations, offset calculations, and time constants
17
- - **Financial Functions**: Currency formatting and money calculations
18
- - **Type Utilities**: TypeScript type helpers and interfaces
19
- - **Validation Functions**: Data validation and parsing utilities
20
- - **Testing Helpers**: Mock functions and test arrangement utilities
21
- - **Constants**: Authentication headers, regex patterns, and time constants
22
- - **Object Utilities**: Property extraction and conversion functions
23
- - **JSON Utilities**: Safe parsing and stringifying operations
24
-
25
11
  ## Constants
26
12
 
27
- ### Authentication Headers
13
+ ### Headers
14
+
28
15
  ```typescript
29
- import { apiKeyHeader, apiIdHeader, authCookieName } from '@onivoro/isomorphic-common';
16
+ import { apiKeyHeader, apiIdHeader } from '@onivoro/isomorphic-common';
30
17
 
31
- // Use in HTTP requests
32
18
  const headers = {
33
19
  [apiKeyHeader]: 'your-api-key', // 'x-api-key'
34
- [apiIdHeader]: 'api-identifier' // 'x-api-id'
20
+ [apiIdHeader]: 'api-identifier', // 'x-api-id'
35
21
  };
36
-
37
- // Cookie name for authentication
38
- document.cookie = `${authCookieName}=your-token`; // 'token'
39
22
  ```
40
23
 
41
- ### Time Constants
24
+ ### Time
25
+
42
26
  ```typescript
43
27
  import { MILLIS_PER_DAY, MILLIS_PER_HOUR, MILLIS_PER_MINUTE } from '@onivoro/isomorphic-common';
44
28
 
45
- // Time constants (calculated from MILLIS_PER_MINUTE base)
46
- const hoursInDay = MILLIS_PER_DAY / MILLIS_PER_HOUR; // 24
47
- const minutesInHour = MILLIS_PER_HOUR / MILLIS_PER_MINUTE; // 60
48
-
49
- // Set timeouts
50
- setTimeout(() => {}, MILLIS_PER_MINUTE); // 1 minute timeout
29
+ MILLIS_PER_MINUTE; // 60_000
30
+ MILLIS_PER_HOUR; // 3_600_000
31
+ MILLIS_PER_DAY; // 86_400_000
51
32
  ```
52
33
 
53
34
  ### Regular Expressions
54
- ```typescript
55
- import {
56
- email,
57
- phone,
58
- zip,
59
- v4,
60
- dateIso8601,
61
- numeric,
62
- ssn,
63
- ein
64
- } from '@onivoro/isomorphic-common';
65
-
66
- // Validate email
67
- const isValidEmail = email.test('user@example.com'); // true
68
35
 
69
- // Validate phone number (format: 123-456-7890)
70
- const isValidPhone = phone.test('123-456-7890'); // true
71
-
72
- // Validate ZIP code (5 digits)
73
- const isValidZip = zip.test('12345'); // true
74
-
75
- // Validate UUID v4
76
- const isValidUuid = v4.test('550e8400-e29b-41d4-a716-446655440000'); // true
77
-
78
- // Validate ISO date (YYYY-MM-DD)
79
- const isValidDate = dateIso8601.test('2023-12-31'); // true
80
-
81
- // Validate numeric values
82
- const isNumeric = numeric.test('12345'); // true
83
-
84
- // Validate SSN (format: XXX-XX-XXXX)
85
- const isValidSSN = ssn.test('123-45-6789'); // true
86
-
87
- // Validate EIN (format: XX-XXXXXXX)
88
- const isValidEIN = ein.test('12-3456789'); // true
36
+ ```typescript
37
+ import { email, phone, zip, v4, dateIso8601, numeric, ssn, ein } from '@onivoro/isomorphic-common';
38
+
39
+ email.test('user@example.com'); // true
40
+ phone.test('123-456-7890'); // true
41
+ zip.test('12345'); // true
42
+ v4.test('550e8400-e29b-41d4-a716-446655440000'); // true
43
+ dateIso8601.test('2023-12-31'); // true (also accepts 1-digit month/day)
44
+ numeric.test('123.45'); // true (digits and dots only)
45
+ ssn.test('123-45-6789'); // true
46
+ ein.test('12-3456789'); // true
89
47
  ```
90
48
 
91
- ## String Functions
49
+ All exported patterns:
50
+
51
+ | Name | Pattern / purpose |
52
+ | --------------------------------------------------------------------- | ------------------------------------------------------- | ------ |
53
+ | `oneOrMoreSpaces`, `multipleSpaces` | `/\s{1,}/g`, `/\s{2,}/g` (global, for `replace`) |
54
+ | `comma` | `/,/g` |
55
+ | `dashesLettersNumbers`, `dashesLettersNumbersSpaces` | letters, digits, `-` (and whitespace), case-insensitive |
56
+ | `zip` | 5 digits |
57
+ | `phone` | `123-456-7890` |
58
+ | `phonePlusOne` | `+1` followed by 10 digits |
59
+ | `code` | 6 digits |
60
+ | `url`, `protocol` | `http(s)://` URLs / the protocol prefix |
61
+ | `firstSpaceLast` | first and last name separated by a space |
62
+ | `passwordNumber`, `passwordUpper`, `passwordLower`, `passwordSpecial` | password character-class checks |
63
+ | `ssn`, `lastFourSocial` | `123-45-6789`, 4 digits |
64
+ | `ein` | `12-3456789` |
65
+ | `duns` | 9 digits |
66
+ | `numeric` | digits and `.` only (also matches `''`) |
67
+ | `stateShort` | two uppercase letters |
68
+ | `email`, `domain`, `emailDomain` | email address, domain name, email domain part |
69
+ | `v4` | UUID v4 |
70
+ | `dateIso8601` | `YYYY-M(M)-D(D)` |
71
+ | `dateIso8601WithTime` | `YYYY-MM-DDTHH:mm:ss.sssZ` |
72
+ | `decimalRegex` | `/(\d | \.)/g` |
73
+ | `slashR` | `/\r/g` |
74
+
75
+ Patterns with the `g` flag are stateful when used with `.test()`; prefer them with `replace`/`match`.
76
+
77
+ ## Strings
78
+
79
+ ### Case conversion (ported from lodash)
92
80
 
93
- ### Case Conversion
94
81
  ```typescript
95
- import { camelCase, kebabCase, snakeCase } from '@onivoro/isomorphic-common';
82
+ import { camelCase, kebabCase, snakeCase, upperFirst, words, unicodeWords } from '@onivoro/isomorphic-common';
96
83
 
97
- // camelCase(string: string): string
98
84
  camelCase('hello world'); // 'helloWorld'
99
- camelCase('--foo-bar--'); // 'fooBar'
100
85
  camelCase('__FOO_BAR__'); // 'fooBar'
101
-
102
- // kebabCase(string: string): string
103
86
  kebabCase('Hello World'); // 'hello-world'
104
87
  kebabCase('fooBar'); // 'foo-bar'
105
-
106
- // snakeCase(string: string): string
107
- snakeCase('Hello World'); // 'hello_world'
108
88
  snakeCase('fooBar'); // 'foo_bar'
89
+ snakeCase('foo2bar'); // 'foo_2_bar'
90
+ upperFirst('hELLO'); // 'Hello' (first char upper, rest lower)
91
+ words('fooBar baz'); // ['foo', 'Bar', 'baz']
92
+ words('a-b c', /[^ ]+/g); // ['a-b', 'c']
93
+ unicodeWords('fooBar'); // ['foo', 'Bar'] (the unicode-aware splitter used by words)
109
94
  ```
110
95
 
111
- ### String Utilities
96
+ ### Other string helpers
97
+
112
98
  ```typescript
113
- import {
114
- toString,
115
- getTag,
116
- sanitizeFilename,
117
- fromBooleanString,
118
- toBooleanString
119
- } from '@onivoro/isomorphic-common';
120
-
121
- // toString(value: any): string
122
- toString(null); // 'null'
123
- toString([1, 2, 3]); // '1,2,3'
124
- toString({foo: 'bar'}); // '[object Object]'
125
-
126
- // getTag(value: any): string
127
- getTag([]); // '[object Array]'
128
- getTag(new Date()); // '[object Date]'
129
-
130
- // sanitizeFilename(filename: string): string
131
- sanitizeFilename('file:name?.txt'); // 'filename.txt'
132
- sanitizeFilename('invalid/file\\name'); // 'invalidfilename'
133
-
134
- // fromBooleanString(value: string): boolean
135
- fromBooleanString('true'); // true
136
- fromBooleanString('false'); // false
137
-
138
- // toBooleanString(value: boolean): string
99
+ import { sanitizeFilename, removeAlphaChars, fromBooleanString, toBooleanString, parseBool, fromCsvString, toCsvString, isSymbol } from '@onivoro/isomorphic-common';
100
+
101
+ // Replaces non-ASCII and / ? : \ { ^ ' } % ` ] > [ ~ < # | " ! * with '_', collapses repeats, trims
102
+ sanitizeFilename('file:name?.txt'); // 'file_name_.txt'
103
+ sanitizeFilename('invalid/file\\name'); // 'invalid_file_name'
104
+
105
+ removeAlphaChars('$1,234.56'); // '1234.56' (keeps digits and dots; undefined for falsy input)
106
+
107
+ fromBooleanString('true'); // true (only the exact string 'true')
139
108
  toBooleanString(true); // 'true'
140
- toBooleanString(false); // 'false'
109
+ toBooleanString(); // 'false'
110
+
111
+ parseBool('true'); // true
112
+ parseBool(true); // true
113
+ parseBool('false'); // false
114
+ parseBool(null); // false
115
+
116
+ fromCsvString(' a, b ,c'); // ['a', 'b', 'c'] ([] for empty/undefined)
117
+ toCsvString(['a', 'b']); // 'a,b'
118
+
119
+ isSymbol(Symbol('x')); // true
141
120
  ```
142
121
 
143
- ## Array Functions
122
+ ## Arrays
144
123
 
145
- ### Array Manipulation
146
124
  ```typescript
147
- import {
148
- chunk,
149
- removeElementAtIndex,
150
- toUniqueArray,
151
- mapEnumToOptions,
152
- mapEnumToLookupArray,
153
- mapEnumToArrayOfValues
154
- } from '@onivoro/isomorphic-common';
125
+ import { chunk, removeElementAtIndex, toUniqueArray } from '@onivoro/isomorphic-common';
155
126
 
156
127
  // chunk<T>(array: T[], numDivisions: number): T[][]
157
- // Divides array into N divisions (NOT chunks of size N)
158
- chunk([1, 2, 3, 4, 5, 6], 3); // [[1, 2], [3, 4], [5, 6]] - 3 divisions
159
- chunk([1, 2, 3, 4, 5], 2); // [[1, 2, 3], [4, 5]] - 2 divisions
128
+ // Splits into at most numDivisions groups (NOT groups of size N); throws if numDivisions <= 0
129
+ chunk([1, 2, 3, 4, 5, 6], 3); // [[1, 2], [3, 4], [5, 6]]
130
+ chunk([1, 2, 3, 4, 5], 2); // [[1, 2, 3], [4, 5]]
160
131
 
161
- // removeElementAtIndex<T>(array: T[], indexToRemove: number): T[]
162
- removeElementAtIndex(['a', 'b', 'c'], 1); // ['a', 'c']
163
-
164
- // toUniqueArray<TElement>(elements: TElement[]): TElement[]
132
+ removeElementAtIndex(['a', 'b', 'c'], 1); // ['a', 'c'] (new array)
165
133
  toUniqueArray([1, 2, 2, 3, 3, 4]); // [1, 2, 3, 4]
166
134
  ```
167
135
 
168
- ### Sorting Functions
136
+ ### Sorting comparators
137
+
169
138
  ```typescript
170
- import {
171
- sortByName,
172
- sortById,
173
- sortNumbers,
174
- sortByPropertyFactory,
175
- sortByNumericPropertyFactory
176
- } from '@onivoro/isomorphic-common';
177
-
178
- // sortByName<TEntity extends { name: string }>(a: TEntity, b: TEntity): number
179
- const users = [{ name: 'Bob' }, { name: 'Alice' }];
180
- users.sort(sortByName); // [{ name: 'Alice' }, { name: 'Bob' }]
181
-
182
- // sortById<TEntity extends { id: string }>(a: TEntity, b: TEntity): number
183
- const items = [{ id: '3' }, { id: '1' }, { id: '2' }];
184
- items.sort(sortById); // [{ id: '1' }, { id: '2' }, { id: '3' }]
185
-
186
- // sortNumbers(a: number, b: number): number
187
- const numbers = [3, 1, 4, 1, 5];
188
- numbers.sort(sortNumbers); // [1, 1, 3, 4, 5]
189
-
190
- // sortByPropertyFactory<TEntity>(property: keyof TEntity): (a: TEntity, b: TEntity) => number
191
- const sortByAge = sortByPropertyFactory<{age: number}>('age');
192
- const people = [{age: 30}, {age: 20}, {age: 25}];
193
- people.sort(sortByAge); // [{age: 20}, {age: 25}, {age: 30}]
194
-
195
- // sortByNumericPropertyFactory<TEntity>(property: keyof TEntity): (a: TEntity, b: TEntity) => number
196
- const sortByScore = sortByNumericPropertyFactory<{score: number}>('score');
197
- ```
139
+ import { sortByName, sortById, sortByFullName, sortByCreatedAt, sortNumbers, sortByStringFactory, sortByNumberFactory } from '@onivoro/isomorphic-common';
198
140
 
199
- ## Date/Time Functions
141
+ [{ name: 'Bob' }, { name: 'Alice' }].sort(sortByName); // Alice, Bob (localeCompare)
142
+ [{ id: '3' }, { id: '1' }].sort(sortById); // '1', '3' (localeCompare)
143
+ users.sort(sortByFullName); // by `${firstName} ${lastName}`
144
+ rows.sort(sortByCreatedAt); // ascending createdAt (Date or string)
145
+ [3, 1, 4, 1, 5].sort(sortNumbers); // [1, 1, 3, 4, 5] (returns 0 for equal values)
200
146
 
201
- ### Date Operations
202
- ```typescript
203
- import {
204
- addOffset,
205
- subtractOffset,
206
- getDateRangeForMonth,
207
- getDateLastMonth,
208
- tryParseDate,
209
- useDate
210
- } from '@onivoro/isomorphic-common';
211
-
212
- // addOffset(input: string | Date | undefined | null): Date | undefined
213
- const date = new Date('2023-01-15');
214
- const withOffset = addOffset(date); // Adds timezone offset
215
-
216
- // subtractOffset(input: string | Date | undefined | null): Date | undefined
217
- const withoutOffset = subtractOffset(date); // Subtracts timezone offset
218
-
219
- // getDateRangeForMonth(year: number, month: number): {startDate: Date, endDate: Date}
220
- const { startDate, endDate } = getDateRangeForMonth(2023, 0); // January 2023 (month 0-indexed)
221
-
222
- // getDateLastMonth(): string
223
- const lastMonth = getDateLastMonth(); // Returns date string for last month
224
-
225
- // tryParseDate(value: any): Date | null
226
- const parsedDate = tryParseDate('2023-01-15'); // Date object
227
- const failedParse = tryParseDate('invalid'); // null
228
-
229
- // useDate(dateString: string | Date): Date
230
- const ensuredDate = useDate('2023-01-15'); // Always returns Date object
147
+ // Factories build a comparator for one property
148
+ people.sort(sortByStringFactory<Person>('lastName')); // localeCompare on the stringified value
149
+ people.sort(sortByNumberFactory<Person>('age')); // numeric; missing values treated as 0
231
150
  ```
232
151
 
233
- ### Date Utilities
152
+ ## Dates
153
+
234
154
  ```typescript
235
- import {
236
- isValidDate,
237
- parseBool
238
- } from '@onivoro/isomorphic-common';
155
+ import { tryParseDate, isValidDate, addOffset, subtractOffset, fromCalendarDate, toCalendarDate, toStartOfDay, toEndOfDay, getDateRangeForMonth, getDateLastMonth, splitDateRangeIntoDays } from '@onivoro/isomorphic-common';
239
156
 
240
- // isValidDate(dateString: string | Date): Date | undefined
241
- const validDate = isValidDate('2023-01-15'); // Date object
242
- const invalidDate = isValidDate('invalid'); // undefined
157
+ tryParseDate('2023-01-15'); // Date
158
+ tryParseDate('invalid'); // undefined (Date inputs are returned as-is)
159
+ isValidDate('2023-01-15'); // Date
160
+ isValidDate('invalid'); // undefined
243
161
 
244
- // parseBool(asc: string | boolean | null | undefined): boolean
245
- parseBool('true'); // true
246
- parseBool('false'); // false
247
- parseBool(true); // true
248
- parseBool(null); // false
162
+ // Shift by the local timezone offset
163
+ addOffset('2023-12-23'); // in UTC-6: 2023-12-23T06:00:00.000Z
164
+ subtractOffset(new Date()); // Date | undefined
165
+
166
+ // Calendar dates (YYYY-MM-DD) <-> Dates, compensating for the local offset
167
+ fromCalendarDate('2023-12-23'); // Date at local midnight, or null
168
+ toCalendarDate(new Date()); // 'YYYY-MM-DD' or undefined
169
+
170
+ // Date-only strings snap to the UTC day boundary; strings containing 'T' are parsed unchanged
171
+ toStartOfDay('2023-01-15'); // 2023-01-15T00:00:00.000Z
172
+ toEndOfDay('2023-01-15'); // 2023-01-15T23:59:59.999Z
173
+
174
+ // month is 1-based; endDate is the first instant of the next month (UTC)
175
+ getDateRangeForMonth(2023, 1);
176
+ // { startDate: 2023-01-01T00:00:00.000Z, endDate: 2023-02-01T00:00:00.000Z }
177
+
178
+ getDateLastMonth(); // a Date that falls in the previous month (today minus (UTC day-of-month + 1) days)
179
+
180
+ // Inclusive list of YYYY-MM-DD strings; [] if either bound is missing or not YYYY-MM-DD
181
+ splitDateRangeIntoDays({ from: '2024-01-01', to: '2024-01-03' });
182
+ // ['2024-01-01', '2024-01-02', '2024-01-03']
249
183
  ```
250
184
 
251
- ## Financial Functions
185
+ ## Money and Numbers
252
186
 
253
187
  ```typescript
254
- import {
255
- formatUsd,
256
- money,
257
- toDollarsAndCents,
258
- round
259
- } from '@onivoro/isomorphic-common';
260
-
261
- // formatUsd(rawAmount?: number | string): string
188
+ import { formatUsd, money, toDollarsAndCents, toWords, round, toDecimalBase } from '@onivoro/isomorphic-common';
189
+
262
190
  formatUsd(1234.56); // '$1,234.56'
263
191
  formatUsd('1234.56'); // '$1,234.56'
264
192
  formatUsd(); // '$0.00'
265
193
 
266
- // money(rawValue: number | string): string | undefined
267
194
  money(19.99); // '$19.99'
195
+ money('$1,234.567'); // '$1,234.57' (non-numeric chars stripped first)
268
196
  money('abc'); // undefined
197
+ money(0); // '$0.00'
198
+ money(undefined); // undefined (also null and '')
269
199
 
270
- // toDollarsAndCents(input: string | number): string
200
+ toWords(1234); // 'one thousand, two hundred thirty-four' (integer part only)
271
201
  toDollarsAndCents(19.99); // 'nineteen dollars and ninety-nine cents'
272
202
  toDollarsAndCents(20); // 'twenty dollars'
273
203
 
274
- // round(numberToRound: number, scalingFactor: number): number
275
- round(19.999, 100); // 20.00 (rounds to nearest cent)
204
+ round(19.999, 100); // 20
276
205
  round(19.994, 100); // 19.99
277
- ```
278
206
 
279
- ## Object Utilities
280
-
281
- ```typescript
282
- import {
283
- propertiesToArray,
284
- convertObjectToLiteral,
285
- toDecimalBase
286
- } from '@onivoro/isomorphic-common';
287
-
288
- // propertiesToArray(obj: any, parentKey?: string): string[]
289
- const obj = { a: { b: { c: 1 } }, d: 2 };
290
- propertiesToArray(obj); // ['a.b.c', 'd']
291
-
292
- // convertObjectToLiteral(obj: any): string
293
- const literal = convertObjectToLiteral({ foo: 'bar', num: 42 });
294
- // Returns string representation of object
295
-
296
- // toDecimalBase(num: string | number, base: number): number
297
- toDecimalBase('FF', 16); // 255
207
+ // toDecimalBase(n: string | number, base = 16)
208
+ toDecimalBase('FF'); // 255
298
209
  toDecimalBase('101', 2); // 5
299
210
  ```
300
211
 
301
- ## Data Transformation
212
+ ## Objects and Enums
302
213
 
303
- ### Enum Utilities
304
214
  ```typescript
305
- import {
306
- mapEnumToOptions,
307
- mapEnumToLookupArray,
308
- mapEnumToArrayOfValues
309
- } from '@onivoro/isomorphic-common';
215
+ import { propertiesToArray, convertObjectToLiteral, mapEnumToOptions, mapEnumToLookupArray, mapEnumToArrayOfValues, mapEntitiesToOptions, getUserFullName } from '@onivoro/isomorphic-common';
216
+
217
+ propertiesToArray({ a: { b: { c: 1 } }, d: 2 }); // ['a.b.c', 'd']
218
+
219
+ // convertObjectToLiteral(literalFn, delimiter, keyValuePairs)
220
+ // Skips null/undefined/'' values (keeps 0 and false); values are stringified before literalFn
221
+ convertObjectToLiteral((k, v) => `${k}=${v}`, ' AND ', { qty: 337, price: 0, note: null });
222
+ // 'qty=337 AND price=0'
310
223
 
311
224
  enum Status {
312
225
  ACTIVE = 'active',
313
- INACTIVE = 'inactive',
314
- PENDING = 'pending'
226
+ ON_HOLD = 'on_hold',
315
227
  }
316
228
 
317
- // mapEnumToOptions<TEntity extends object>(enumeration: TEntity, includeBlank = true)
318
- const options = mapEnumToOptions(Status);
319
- // [{ display: '', value: '' }, { display: 'ACTIVE', value: 'active' }, ...]
229
+ mapEnumToOptions(Status);
230
+ // [{ display: '', value: '' }, { value: 'active', display: 'ACTIVE' }, { value: 'on_hold', display: 'ON HOLD' }]
231
+ mapEnumToOptions(Status, false); // same, without the blank option
232
+ mapEnumToLookupArray(Status); // same as mapEnumToOptions(Status, false)
233
+ mapEnumToArrayOfValues(Status); // ['active', 'on_hold']
234
+ // Underscores in keys become spaces in `display`. Numeric enums include TypeScript's reverse mappings.
320
235
 
321
- const optionsNoBlank = mapEnumToOptions(Status, false);
322
- // [{ display: 'ACTIVE', value: 'active' }, ...]
236
+ // Options from a normalized entity map, in `ids` order; display falls back to id
237
+ mapEntitiesToOptions({ a: { id: 'a', name: 'Alpha' }, b: { id: 'b' } }, ['a', 'b']);
238
+ // [{ display: '', value: '' }, { value: 'a', display: 'Alpha' }, { value: 'b', display: 'b' }]
323
239
 
324
- // mapEnumToLookupArray<TEntity extends object>(enumeration: TEntity)
325
- const lookupArray = mapEnumToLookupArray(Status);
326
- // Array of lookup objects with display/value pairs
327
-
328
- // mapEnumToArrayOfValues<TEntity extends object>(enumeration: TEntity)
329
- const values = mapEnumToArrayOfValues(Status);
330
- // ['active', 'inactive', 'pending']
240
+ getUserFullName({ firstName: 'John', lastName: 'Doe' }); // 'John Doe'
241
+ getUserFullName(undefined); // 'undefined undefined'
331
242
  ```
332
243
 
333
- ### Entity Utilities
244
+ ## JSON
245
+
334
246
  ```typescript
335
- import { getUserFullName } from '@onivoro/isomorphic-common';
247
+ import { tryJsonParse, tryJsonStringify } from '@onivoro/isomorphic-common';
336
248
 
337
- // getUserFullName(user: TNameable | undefined): string
338
- const user = { firstName: 'John', lastName: 'Doe' };
339
- getUserFullName(user); // 'John Doe'
340
- getUserFullName(undefined); // 'undefined undefined'
249
+ tryJsonParse<{ name: string }>('{"name":"John"}'); // { name: 'John' }
250
+ tryJsonParse('invalid json'); // null
251
+
252
+ tryJsonStringify({ name: 'John' }); // '{"name":"John"}'
253
+ tryJsonStringify({ name: 'John' }, null, 2); // pretty-printed
254
+ tryJsonStringify(undefined); // null (also null if stringify throws)
341
255
  ```
342
256
 
343
- ## JSON Utilities
257
+ ## Async and Profiling
344
258
 
345
259
  ```typescript
346
- import {
347
- tryJsonParse,
348
- tryJsonStringify
349
- } from '@onivoro/isomorphic-common';
350
-
351
- // tryJsonParse<T>(parseable: string | null | undefined): T | null
352
- const parsed = tryJsonParse<{name: string}>('{"name":"John"}'); // {name: 'John'}
353
- const failed = tryJsonParse('invalid json'); // null
354
-
355
- // tryJsonStringify<T>(object: T | null | undefined, fmtr?: any, spaces?: number): string | null
356
- const json = tryJsonStringify({name: 'John'}); // '{"name":"John"}'
357
- const formatted = tryJsonStringify({name: 'John'}, null, 2); // Pretty formatted JSON
358
- ```
260
+ import { sleep, profileTime } from '@onivoro/isomorphic-common';
359
261
 
360
- ## Testing Utilities
262
+ await sleep(1000);
263
+ await sleep(); // 0 ms
361
264
 
362
- ```typescript
363
- import {
364
- arrangeActAssert,
365
- mockCalls
366
- } from '@onivoro/isomorphic-common';
367
-
368
- // arrangeActAssert<T>(arrange: () => T, act: (arranged: T) => any, assert: (arranged: T, acted: any) => any)
369
- arrangeActAssert(
370
- () => ({ value: 5 }), // arrange
371
- (arranged) => arranged.value * 2, // act
372
- (arranged, result) => expect(result).toBe(10) // assert
373
- );
374
-
375
- // mockCalls(mockFn: jest.Mock): any[]
376
- const mock = jest.fn();
377
- mock('first');
378
- mock('second');
379
- const calls = mockCalls(mock); // ['first', 'second']
265
+ // Logs start, end, and elapsed seconds via console.log, then returns fn's result
266
+ const users = await profileTime(() => fetchUsers());
380
267
  ```
381
268
 
382
- ## Utility Functions
269
+ ## Testing Helpers
383
270
 
384
271
  ```typescript
385
- import { sleep } from '@onivoro/isomorphic-common';
386
-
387
- // sleep(milliseconds = 0): Promise<void>
388
- await sleep(1000); // Wait 1 second
389
- await sleep(); // Wait 0 milliseconds (next tick)
272
+ import { arrangeActAssert, mockCalls, useDate } from '@onivoro/isomorphic-common';
273
+
274
+ // Must be awaited. Each step may be async; assert receives the arranged object plus `result`.
275
+ await arrangeActAssert({
276
+ arrange: () => ({ value: 5 }),
277
+ act: ({ value }) => value * 2,
278
+ assert: ({ result }) => expect(result).toBe(10),
279
+ });
280
+
281
+ // Labels a jest mock's calls, handy for snapshots
282
+ const fn = jest.fn();
283
+ fn('first');
284
+ fn('second');
285
+ mockCalls(fn); // { 'mock-calls -->> 2 invocation(s)': [['first'], ['second']] }
286
+ mockCalls(fn, 'save'); // { 'save -->> 2 invocation(s)': [...] }
287
+
288
+ // Replaces the global Date so `new Date()` and `Date.now()` return the given instant while fn runs
289
+ await useDate('2020-01-01T00:00:00.000Z', async () => {
290
+ expect(new Date().toISOString()).toBe('2020-01-01T00:00:00.000Z');
291
+ });
390
292
  ```
391
293
 
392
- ## Type Definitions
294
+ `useDate` restores `Date` once `fn` settles, whether it resolves or rejects; a rejection is rethrown.
295
+
296
+ ## Types
393
297
 
394
- ### Core Interfaces
395
298
  ```typescript
396
- import {
397
- ILookup,
398
- TNameable,
399
- IAccessToken,
400
- TCreateable,
401
- TKeysOf,
402
- IEntityProvider,
403
- IAxiosWrappedNestException
404
- } from '@onivoro/isomorphic-common';
405
-
406
- // Lookup interface for key-value pairs
407
- const lookup: ILookup<string, number> = {
408
- display: 'Option 1',
409
- value: 1
410
- };
299
+ import { ILookup, TNameable, IAccessToken, TCreateable, TKeysOf, IEntityProvider, IAxiosWrappedNestException } from '@onivoro/isomorphic-common';
411
300
 
412
- // Nameable type
413
- const user: TNameable = {
414
- firstName: 'John',
415
- lastName: 'Doe'
416
- };
301
+ const option: ILookup<string, number> = { display: 'Option 1', value: 1 };
302
+
303
+ const person: TNameable = { firstName: 'John', lastName: 'Doe' };
417
304
 
418
- // Access token interface
419
305
  const token: IAccessToken = {
420
- token: 'jwt-token-here',
421
- expires: 1234567890
306
+ id: 'user-1',
307
+ roleId: 'admin',
308
+ type: 'user', // 'user' | 'machine'
309
+ companyId: 'co-1', // optional, as are brokerId and exp
310
+ exp: 1735689600,
422
311
  };
423
312
 
424
- // Createable type with timestamp
425
- const entity: TCreateable = {
426
- createdAt: new Date()
427
- };
313
+ const row: TCreateable = { createdAt: new Date() }; // createdAt: Date | string
428
314
 
429
- // Type utility for extracting keys with specific value types
430
- type StringKeys = TKeysOf<{a: string, b: number, c: string}, string>; // 'a' | 'c'
315
+ // Maps every key of the source type to the given value type
316
+ type Flags = TKeysOf<{ a: string; b: number }, boolean>; // { a: boolean; b: boolean }
431
317
 
432
- // Entity provider interface
433
- const provider: IEntityProvider = {
434
- // Implementation details depend on usage
435
- };
318
+ // CRUD contract: getOne, getMany, postOne, postMany, delete, put, patch
319
+ type UserProvider = IEntityProvider<User, FindOneOptions, FindManyOptions, FindOptionsWhere, DeepPartial<User>>;
436
320
 
437
- // Axios error wrapper for NestJS
438
- const error: IAxiosWrappedNestException = {
439
- // Error structure for HTTP exception handling
440
- };
321
+ // Shape of a NestJS HttpException body as seen in an axios error response
322
+ const err: IAxiosWrappedNestException = { statusCode: 400, error: 'Bad Request', message: 'Invalid id' };
441
323
  ```
442
324
 
443
325
  ## License
444
326
 
445
- This library is licensed under the MIT License. See the LICENSE file in this package for details.
327
+ MIT. See the LICENSE file in this package.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onivoro/isomorphic-common",
3
- "version": "24.38.2",
3
+ "version": "25.0.2",
4
4
  "license": "MIT",
5
5
  "type": "commonjs",
6
6
  "main": "./src/index.js",
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.chunk = chunk;
4
4
  function chunk(array, numDivisions) {
5
5
  if (numDivisions <= 0) {
6
- throw new Error('Number of divisions must be greater than or equal to 0');
6
+ throw new Error('Number of divisions must be greater than 0');
7
7
  }
8
8
  if (array.length === 0) {
9
9
  return [];
@@ -1 +1 @@
1
- {"version":3,"file":"chunk.function.js","sourceRoot":"","sources":["../../../../../../../libs/isomorphic/common/src/lib/functions/chunk.function.ts"],"names":[],"mappings":";;AAAA,sBAwBC;AAxBD,SAAgB,KAAK,CAAI,KAAU,EAAE,YAAoB;IACrD,IAAI,YAAY,IAAI,CAAC,EAAE,CAAC;QACpB,MAAM,IAAI,KAAK,CAAC,wDAAwD,CAAC,CAAC;IAC9E,CAAC;IAED,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACrB,OAAO,EAAE,CAAC;IACd,CAAC;IAED,MAAM,MAAM,GAAU,EAAE,CAAC;IACzB,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,GAAG,YAAY,CAAC,CAAC;IAEzD,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,IAAI,SAAS,EAAE,CAAC;QAC/C,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,GAAG,SAAS,CAAC,CAAC,CAAC;IAC/C,CAAC;IAED,OAAO,MAAM,CAAC,MAAM,GAAG,YAAY,EAAE,CAAC;QAClC,MAAM,SAAS,GAAG,MAAM,CAAC,GAAG,EAAE,CAAC;QAC/B,IAAI,SAAS,EAAE,CAAC;YACZ,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,SAAS,CAAC,CAAC;QACjD,CAAC;IACL,CAAC;IAED,OAAO,MAAM,CAAC;AAClB,CAAC"}
1
+ {"version":3,"file":"chunk.function.js","sourceRoot":"","sources":["../../../../../../../libs/isomorphic/common/src/lib/functions/chunk.function.ts"],"names":[],"mappings":";;AAAA,sBAwBC;AAxBD,SAAgB,KAAK,CAAI,KAAU,EAAE,YAAoB;IACvD,IAAI,YAAY,IAAI,CAAC,EAAE,CAAC;QACtB,MAAM,IAAI,KAAK,CAAC,4CAA4C,CAAC,CAAC;IAChE,CAAC;IAED,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACvB,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,MAAM,MAAM,GAAU,EAAE,CAAC;IACzB,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,GAAG,YAAY,CAAC,CAAC;IAEzD,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,IAAI,SAAS,EAAE,CAAC;QACjD,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,GAAG,SAAS,CAAC,CAAC,CAAC;IAC7C,CAAC;IAED,OAAO,MAAM,CAAC,MAAM,GAAG,YAAY,EAAE,CAAC;QACpC,MAAM,SAAS,GAAG,MAAM,CAAC,GAAG,EAAE,CAAC;QAC/B,IAAI,SAAS,EAAE,CAAC;YACd,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,SAAS,CAAC,CAAC;QAC/C,CAAC;IACH,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC"}
@@ -3,17 +3,17 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.money = money;
4
4
  const remove_alpha_chars_function_1 = require("./remove-alpha-chars.function");
5
5
  function money(rawValue) {
6
- if (!rawValue) {
6
+ if (rawValue === undefined || rawValue === null || rawValue === '') {
7
7
  return;
8
8
  }
9
- const scrubbed = (0, remove_alpha_chars_function_1.removeAlphaChars)(rawValue);
9
+ const scrubbed = (0, remove_alpha_chars_function_1.removeAlphaChars)(String(rawValue));
10
10
  const amount = Number(scrubbed);
11
11
  if (!amount && amount !== 0) {
12
12
  return;
13
13
  }
14
14
  const formatter = new Intl.NumberFormat('en-US', {
15
15
  style: 'currency',
16
- currency: 'USD'
16
+ currency: 'USD',
17
17
  });
18
18
  return formatter.format(Number(amount));
19
19
  }
@@ -1 +1 @@
1
- {"version":3,"file":"money.function.js","sourceRoot":"","sources":["../../../../../../../libs/isomorphic/common/src/lib/functions/money.function.ts"],"names":[],"mappings":";;AAEA,sBAmBC;AArBD,+EAAiE;AAEjE,SAAgB,KAAK,CAAC,QAAyB;IAC3C,IAAG,CAAC,QAAQ,EAAE,CAAC;QACX,OAAO;IACX,CAAC;IAED,MAAM,QAAQ,GAAG,IAAA,8CAAgB,EAAC,QAAQ,CAAC,CAAC;IAE5C,MAAM,MAAM,GAAG,MAAM,CAAC,QAAQ,CAAC,CAAC;IAEhC,IAAG,CAAC,MAAM,IAAI,MAAM,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO;IACX,CAAC;IAED,MAAM,SAAS,GAAG,IAAI,IAAI,CAAC,YAAY,CAAC,OAAO,EAAE;QAC7C,KAAK,EAAE,UAAU;QACjB,QAAQ,EAAE,KAAK;KAClB,CAAC,CAAC;IAEH,OAAO,SAAS,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC;AAC5C,CAAC"}
1
+ {"version":3,"file":"money.function.js","sourceRoot":"","sources":["../../../../../../../libs/isomorphic/common/src/lib/functions/money.function.ts"],"names":[],"mappings":";;AAEA,sBAmBC;AArBD,+EAAiE;AAEjE,SAAgB,KAAK,CAAC,QAAyB;IAC7C,IAAI,QAAQ,KAAK,SAAS,IAAI,QAAQ,KAAK,IAAI,IAAI,QAAQ,KAAK,EAAE,EAAE,CAAC;QACnE,OAAO;IACT,CAAC;IAED,MAAM,QAAQ,GAAG,IAAA,8CAAgB,EAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC;IAEpD,MAAM,MAAM,GAAG,MAAM,CAAC,QAAQ,CAAC,CAAC;IAEhC,IAAI,CAAC,MAAM,IAAI,MAAM,KAAK,CAAC,EAAE,CAAC;QAC5B,OAAO;IACT,CAAC;IAED,MAAM,SAAS,GAAG,IAAI,IAAI,CAAC,YAAY,CAAC,OAAO,EAAE;QAC/C,KAAK,EAAE,UAAU;QACjB,QAAQ,EAAE,KAAK;KAChB,CAAC,CAAC;IAEH,OAAO,SAAS,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC;AAC1C,CAAC"}
@@ -1 +1 @@
1
- export declare const sortNumbers: (a: number, b: number) => 1 | -1;
1
+ export declare const sortNumbers: (a: number, b: number) => 0 | 1 | -1;
@@ -2,6 +2,9 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.sortNumbers = void 0;
4
4
  const sortNumbers = (a, b) => {
5
+ if (Number(a) === Number(b)) {
6
+ return 0;
7
+ }
5
8
  return Number(a) < Number(b) ? -1 : 1;
6
9
  };
7
10
  exports.sortNumbers = sortNumbers;
@@ -1 +1 @@
1
- {"version":3,"file":"sort-numbers.function.js","sourceRoot":"","sources":["../../../../../../../libs/isomorphic/common/src/lib/functions/sort-numbers.function.ts"],"names":[],"mappings":";;;AAAO,MAAM,WAAW,GAAG,CAAC,CAAS,EAAE,CAAS,EAAE,EAAE;IAChD,OAAO,MAAM,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAC1C,CAAC,CAAC;AAFW,QAAA,WAAW,eAEtB"}
1
+ {"version":3,"file":"sort-numbers.function.js","sourceRoot":"","sources":["../../../../../../../libs/isomorphic/common/src/lib/functions/sort-numbers.function.ts"],"names":[],"mappings":";;;AAAO,MAAM,WAAW,GAAG,CAAC,CAAS,EAAE,CAAS,EAAE,EAAE;IAClD,IAAI,MAAM,CAAC,CAAC,CAAC,KAAK,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC;QAC5B,OAAO,CAAC,CAAC;IACX,CAAC;IAED,OAAO,MAAM,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AACxC,CAAC,CAAC;AANW,QAAA,WAAW,eAMtB"}
@@ -9,14 +9,18 @@ function useDate(date, fn) {
9
9
  const OriginalDate = Date;
10
10
  Date = class extends OriginalDate {
11
11
  constructor(...args) {
12
- super((args.length === 0) ? mockedDate : new OriginalDate(...args));
12
+ super(args.length === 0 ? mockedDate : new OriginalDate(...args));
13
13
  }
14
14
  static now() {
15
15
  return mockedDate.getTime();
16
16
  }
17
17
  };
18
- yield fn();
19
- Date = OriginalDate;
18
+ try {
19
+ yield fn();
20
+ }
21
+ finally {
22
+ Date = OriginalDate;
23
+ }
20
24
  });
21
25
  }
22
26
  //# sourceMappingURL=use-date.function.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"use-date.function.js","sourceRoot":"","sources":["../../../../../../../libs/isomorphic/common/src/lib/functions/use-date.function.ts"],"names":[],"mappings":";;AACA,0BAmBC;;AApBD,+BAA+B;AAC/B,SAAsB,OAAO,CAAC,IAAY,EAAE,EAAuB;;QAC/D,MAAM,UAAU,GAAG,IAAI,IAAI,CAAC,IAAI,CAAC,CAAC;QAElC,MAAM,YAAY,GAAG,IAAI,CAAC;QAE1B,IAAI,GAAG,KAAM,SAAQ,YAAY;YAC7B,YAAY,GAAG,IAAW;gBACtB,KAAK,CACD,CAAC,IAAI,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,IAAK,YAAoB,CAAC,GAAG,IAAI,CAAC,CACxE,CAAC;YACN,CAAC;YACD,MAAM,CAAU,GAAG;gBACf,OAAO,UAAU,CAAC,OAAO,EAAE,CAAC;YAChC,CAAC;SACG,CAAC;QAET,MAAM,EAAE,EAAE,CAAC;QAEX,IAAI,GAAG,YAAY,CAAC;IACxB,CAAC;CAAA"}
1
+ {"version":3,"file":"use-date.function.js","sourceRoot":"","sources":["../../../../../../../libs/isomorphic/common/src/lib/functions/use-date.function.ts"],"names":[],"mappings":";;AACA,0BAqBC;;AAtBD,+BAA+B;AAC/B,SAAsB,OAAO,CAAC,IAAY,EAAE,EAAuB;;QACjE,MAAM,UAAU,GAAG,IAAI,IAAI,CAAC,IAAI,CAAC,CAAC;QAElC,MAAM,YAAY,GAAG,IAAI,CAAC;QAE1B,IAAI,GAAG,KAAM,SAAQ,YAAY;YAC/B,YAAY,GAAG,IAAW;gBACxB,KAAK,CACH,IAAI,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,IAAK,YAAoB,CAAC,GAAG,IAAI,CAAC,CACpE,CAAC;YACJ,CAAC;YACD,MAAM,CAAU,GAAG;gBACjB,OAAO,UAAU,CAAC,OAAO,EAAE,CAAC;YAC9B,CAAC;SACK,CAAC;QAET,IAAI,CAAC;YACH,MAAM,EAAE,EAAE,CAAC;QACb,CAAC;gBAAS,CAAC;YACT,IAAI,GAAG,YAAY,CAAC;QACtB,CAAC;IACH,CAAC;CAAA"}