@onivoro/isomorphic-common 24.39.0 → 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 +212 -330
- package/package.json +1 -1
- package/src/lib/functions/chunk.function.js +1 -1
- package/src/lib/functions/chunk.function.js.map +1 -1
- package/src/lib/functions/money.function.js +3 -3
- package/src/lib/functions/money.function.js.map +1 -1
- package/src/lib/functions/sort-numbers.function.d.ts +1 -1
- package/src/lib/functions/sort-numbers.function.js +3 -0
- package/src/lib/functions/sort-numbers.function.js.map +1 -1
- package/src/lib/functions/use-date.function.js +7 -3
- package/src/lib/functions/use-date.function.js.map +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @onivoro/isomorphic-common
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
###
|
|
13
|
+
### Headers
|
|
14
|
+
|
|
28
15
|
```typescript
|
|
29
|
-
import { apiKeyHeader, apiIdHeader
|
|
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
|
|
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
|
-
//
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
//
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
//
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
96
|
+
### Other string helpers
|
|
97
|
+
|
|
112
98
|
```typescript
|
|
113
|
-
import {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
//
|
|
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(
|
|
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
|
-
##
|
|
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
|
-
//
|
|
158
|
-
chunk([1, 2, 3, 4, 5, 6], 3); // [[1, 2], [3, 4], [5, 6]]
|
|
159
|
-
chunk([1, 2, 3, 4, 5], 2); // [[1, 2, 3], [4, 5]]
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
-
|
|
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
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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
|
-
//
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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
|
-
##
|
|
185
|
+
## Money and Numbers
|
|
252
186
|
|
|
253
187
|
```typescript
|
|
254
|
-
import {
|
|
255
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
212
|
+
## Objects and Enums
|
|
302
213
|
|
|
303
|
-
### Enum Utilities
|
|
304
214
|
```typescript
|
|
305
|
-
import {
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
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
|
-
|
|
314
|
-
PENDING = 'pending'
|
|
226
|
+
ON_HOLD = 'on_hold',
|
|
315
227
|
}
|
|
316
228
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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
|
-
|
|
322
|
-
|
|
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
|
-
|
|
325
|
-
|
|
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
|
-
|
|
244
|
+
## JSON
|
|
245
|
+
|
|
334
246
|
```typescript
|
|
335
|
-
import {
|
|
247
|
+
import { tryJsonParse, tryJsonStringify } from '@onivoro/isomorphic-common';
|
|
336
248
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
262
|
+
await sleep(1000);
|
|
263
|
+
await sleep(); // 0 ms
|
|
361
264
|
|
|
362
|
-
|
|
363
|
-
|
|
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
|
-
##
|
|
269
|
+
## Testing Helpers
|
|
383
270
|
|
|
384
271
|
```typescript
|
|
385
|
-
import {
|
|
386
|
-
|
|
387
|
-
//
|
|
388
|
-
await
|
|
389
|
-
|
|
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
|
-
|
|
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
|
-
|
|
413
|
-
|
|
414
|
-
|
|
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
|
-
|
|
421
|
-
|
|
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
|
-
//
|
|
425
|
-
const entity: TCreateable = {
|
|
426
|
-
createdAt: new Date()
|
|
427
|
-
};
|
|
313
|
+
const row: TCreateable = { createdAt: new Date() }; // createdAt: Date | string
|
|
428
314
|
|
|
429
|
-
//
|
|
430
|
-
type
|
|
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
|
-
//
|
|
433
|
-
|
|
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
|
-
//
|
|
438
|
-
const
|
|
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
|
-
|
|
327
|
+
MIT. See the LICENSE file in this package.
|
package/package.json
CHANGED
|
@@ -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
|
|
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;
|
|
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 (
|
|
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;
|
|
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;
|
|
@@ -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;
|
|
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(
|
|
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
|
-
|
|
19
|
-
|
|
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,
|
|
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"}
|