@bessemer/cornerstone 0.5.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/jest.config.js +3 -0
- package/package.json +39 -0
- package/src/array.ts +142 -0
- package/src/async.ts +114 -0
- package/src/cache.ts +236 -0
- package/src/combinable.ts +40 -0
- package/src/comparator.ts +78 -0
- package/src/content.ts +138 -0
- package/src/context.ts +6 -0
- package/src/crypto.ts +11 -0
- package/src/date.ts +18 -0
- package/src/duration.ts +57 -0
- package/src/either.ts +29 -0
- package/src/entry.ts +21 -0
- package/src/equalitor.ts +12 -0
- package/src/error-event.ts +126 -0
- package/src/error.ts +16 -0
- package/src/expression/array-expression.ts +29 -0
- package/src/expression/expression-evaluator.ts +34 -0
- package/src/expression/expression.ts +188 -0
- package/src/expression/internal.ts +34 -0
- package/src/expression/numeric-expression.ts +182 -0
- package/src/expression/string-expression.ts +38 -0
- package/src/expression.ts +48 -0
- package/src/function.ts +3 -0
- package/src/glob.ts +19 -0
- package/src/global-variable.ts +40 -0
- package/src/hash.ts +28 -0
- package/src/hex-code.ts +6 -0
- package/src/index.ts +82 -0
- package/src/lazy.ts +11 -0
- package/src/logger.ts +144 -0
- package/src/math.ts +132 -0
- package/src/misc.ts +22 -0
- package/src/object.ts +236 -0
- package/src/patch.ts +128 -0
- package/src/precondition.ts +25 -0
- package/src/promise.ts +16 -0
- package/src/property.ts +29 -0
- package/src/reference.ts +68 -0
- package/src/resource.ts +32 -0
- package/src/result.ts +66 -0
- package/src/retry.ts +70 -0
- package/src/rich-text.ts +24 -0
- package/src/set.ts +46 -0
- package/src/signature.ts +20 -0
- package/src/store.ts +91 -0
- package/src/string.ts +173 -0
- package/src/tag.ts +68 -0
- package/src/types.ts +21 -0
- package/src/ulid.ts +28 -0
- package/src/unit.ts +4 -0
- package/src/uri.ts +321 -0
- package/src/url.ts +155 -0
- package/src/uuid.ts +37 -0
- package/src/zod.ts +24 -0
- package/test/comparator.test.ts +1 -0
- package/test/expression.test.ts +12 -0
- package/test/object.test.ts +104 -0
- package/test/patch.test.ts +170 -0
- package/test/set.test.ts +20 -0
- package/test/string.test.ts +22 -0
- package/test/uri.test.ts +111 -0
- package/test/url.test.ts +174 -0
- package/tsconfig.build.json +13 -0
- package/tsup.config.ts +4 -0
package/src/logger.ts
ADDED
@@ -0,0 +1,144 @@
|
|
1
|
+
import pino from 'pino'
|
2
|
+
import { Lazy, Objects } from '@bessemer/cornerstone'
|
3
|
+
import { createGlobalVariable } from '@bessemer/cornerstone/global-variable'
|
4
|
+
import { LazyValue } from '@bessemer/cornerstone/lazy'
|
5
|
+
import { UnknownRecord } from 'type-fest'
|
6
|
+
|
7
|
+
type PinoLogger = pino.Logger
|
8
|
+
type PinoBindings = pino.Bindings
|
9
|
+
export type LoggerOptions = pino.LoggerOptions
|
10
|
+
|
11
|
+
type LogOptions = { error?: unknown; context?: UnknownRecord }
|
12
|
+
type LogFunction = (message: LazyValue<string>, options?: LogOptions) => void
|
13
|
+
|
14
|
+
export class Logger {
|
15
|
+
constructor(private readonly logger: PinoLogger) {}
|
16
|
+
|
17
|
+
trace: LogFunction = (message: LazyValue<string>, options?: LogOptions): void => {
|
18
|
+
if (this.logger.isLevelEnabled?.('trace') ?? true) {
|
19
|
+
this.logger.trace({ err: options?.error, context: options?.context }, Lazy.evaluate(message))
|
20
|
+
}
|
21
|
+
}
|
22
|
+
|
23
|
+
debug: LogFunction = (message: LazyValue<string>, options?: LogOptions): void => {
|
24
|
+
if (this.logger.isLevelEnabled?.('debug') ?? true) {
|
25
|
+
this.logger.debug({ err: options?.error, context: options?.context }, Lazy.evaluate(message))
|
26
|
+
}
|
27
|
+
}
|
28
|
+
|
29
|
+
info: LogFunction = (message: LazyValue<string>, options?: LogOptions): void => {
|
30
|
+
if (this.logger.isLevelEnabled?.('info') ?? true) {
|
31
|
+
this.logger.info({ err: options?.error, context: options?.context }, Lazy.evaluate(message))
|
32
|
+
}
|
33
|
+
}
|
34
|
+
|
35
|
+
warn: LogFunction = (message: LazyValue<string>, options?: LogOptions): void => {
|
36
|
+
if (this.logger.isLevelEnabled?.('warn') ?? true) {
|
37
|
+
this.logger.warn({ err: options?.error, context: options?.context }, Lazy.evaluate(message))
|
38
|
+
}
|
39
|
+
}
|
40
|
+
|
41
|
+
error: LogFunction = (message: LazyValue<string>, options?: LogOptions): void => {
|
42
|
+
if (this.logger.isLevelEnabled?.('error') ?? true) {
|
43
|
+
this.logger.error({ err: options?.error, context: options?.context }, Lazy.evaluate(message))
|
44
|
+
}
|
45
|
+
}
|
46
|
+
|
47
|
+
fatal: LogFunction = (message: LazyValue<string>, options?: LogOptions): void => {
|
48
|
+
if (this.logger.isLevelEnabled?.('fatal') ?? true) {
|
49
|
+
this.logger.fatal({ err: options?.error, context: options?.context }, Lazy.evaluate(message))
|
50
|
+
}
|
51
|
+
}
|
52
|
+
}
|
53
|
+
|
54
|
+
const getPrettyTransport = (): LoggerOptions => {
|
55
|
+
if (process.env.NODE_ENV === 'production' || typeof window !== 'undefined') {
|
56
|
+
return {}
|
57
|
+
}
|
58
|
+
|
59
|
+
return {
|
60
|
+
transport: {
|
61
|
+
target: 'pino-pretty',
|
62
|
+
options: {
|
63
|
+
colorize: true,
|
64
|
+
ignore: 'pid,hostname,module',
|
65
|
+
messageFormat: '{if module}{module} - {end}{msg}',
|
66
|
+
},
|
67
|
+
},
|
68
|
+
}
|
69
|
+
}
|
70
|
+
|
71
|
+
const applyDefaultOptions = (options?: LoggerOptions): LoggerOptions => {
|
72
|
+
const defaultOptions: LoggerOptions = {
|
73
|
+
browser: {
|
74
|
+
asObject: true,
|
75
|
+
},
|
76
|
+
...getPrettyTransport(),
|
77
|
+
}
|
78
|
+
|
79
|
+
return Objects.merge(defaultOptions, options)
|
80
|
+
}
|
81
|
+
|
82
|
+
const createProxyHandler = (getLogger: () => PinoLogger): ProxyHandler<PinoLogger> => {
|
83
|
+
let cachedLogger: PinoLogger | null = null
|
84
|
+
let cachedVersion = GlobalLoggerState.getValue().version
|
85
|
+
|
86
|
+
const getOrCreateLogger = () => {
|
87
|
+
if (cachedVersion !== GlobalLoggerState.getValue().version) {
|
88
|
+
cachedLogger = null
|
89
|
+
cachedVersion = GlobalLoggerState.getValue().version
|
90
|
+
}
|
91
|
+
|
92
|
+
if (Objects.isNil(cachedLogger)) {
|
93
|
+
cachedLogger = getLogger()
|
94
|
+
}
|
95
|
+
|
96
|
+
return cachedLogger
|
97
|
+
}
|
98
|
+
|
99
|
+
return {
|
100
|
+
get(_: any, prop: string): any {
|
101
|
+
if (prop === 'child') {
|
102
|
+
return (bindings: PinoBindings) => {
|
103
|
+
return new Proxy(
|
104
|
+
{} as PinoLogger,
|
105
|
+
createProxyHandler(() => getOrCreateLogger().child(bindings))
|
106
|
+
)
|
107
|
+
}
|
108
|
+
}
|
109
|
+
|
110
|
+
return (getOrCreateLogger() as any)[prop]
|
111
|
+
},
|
112
|
+
}
|
113
|
+
}
|
114
|
+
|
115
|
+
const GlobalLoggerState = createGlobalVariable<{
|
116
|
+
version: number
|
117
|
+
logger: pino.Logger
|
118
|
+
}>('GlobalLoggerState', () => ({
|
119
|
+
version: 0,
|
120
|
+
logger: pino(applyDefaultOptions({ level: 'info' })),
|
121
|
+
}))
|
122
|
+
|
123
|
+
const LoggerProxy: PinoLogger = new Proxy(
|
124
|
+
{} as PinoLogger,
|
125
|
+
createProxyHandler(() => GlobalLoggerState.getValue().logger)
|
126
|
+
)
|
127
|
+
|
128
|
+
const Primary: Logger = new Logger(LoggerProxy)
|
129
|
+
|
130
|
+
export const initialize = (initialOptions?: LoggerOptions): void => {
|
131
|
+
const options = applyDefaultOptions(initialOptions)
|
132
|
+
GlobalLoggerState.setValue({ version: GlobalLoggerState.getValue().version + 1, logger: pino(options) })
|
133
|
+
}
|
134
|
+
|
135
|
+
export const child = (module: string): Logger => {
|
136
|
+
return new Logger(LoggerProxy.child({ module }))
|
137
|
+
}
|
138
|
+
|
139
|
+
export const trace = Primary.trace
|
140
|
+
export const debug = Primary.debug
|
141
|
+
export const info = Primary.info
|
142
|
+
export const warn = Primary.warn
|
143
|
+
export const error = Primary.error
|
144
|
+
export const fatal = Primary.fatal
|
package/src/math.ts
ADDED
@@ -0,0 +1,132 @@
|
|
1
|
+
import { isNumber as _isNumber } from 'lodash-es'
|
2
|
+
|
3
|
+
export const isNumber = (value?: unknown): value is number => {
|
4
|
+
return _isNumber(value)
|
5
|
+
}
|
6
|
+
|
7
|
+
export const isPositive = (value?: unknown): value is number => {
|
8
|
+
return isNumber(value) && value > 0
|
9
|
+
}
|
10
|
+
|
11
|
+
export const isEven = (d: number) => d % 2 === 0
|
12
|
+
|
13
|
+
export enum RoundingMode {
|
14
|
+
Nearest = 'Nearest',
|
15
|
+
Down = 'Down',
|
16
|
+
Up = 'Up',
|
17
|
+
HalfEven = 'HalfEven',
|
18
|
+
}
|
19
|
+
|
20
|
+
export const round = (value: number, scale: number, roundingMode: RoundingMode): number => {
|
21
|
+
switch (roundingMode) {
|
22
|
+
case RoundingMode.Nearest:
|
23
|
+
return roundNearest(value, scale)
|
24
|
+
case RoundingMode.Down:
|
25
|
+
return roundDown(value, scale)
|
26
|
+
case RoundingMode.Up:
|
27
|
+
return roundUp(value, scale)
|
28
|
+
case RoundingMode.HalfEven:
|
29
|
+
return roundHalfEven(value, scale)
|
30
|
+
}
|
31
|
+
}
|
32
|
+
|
33
|
+
export const roundNearest = (value: number, scale: number): number => {
|
34
|
+
const factor = Math.pow(10, scale)
|
35
|
+
return Math.round((value + Number.EPSILON) * factor) / factor
|
36
|
+
}
|
37
|
+
|
38
|
+
export const roundDown = (value: number, scale: number) => {
|
39
|
+
const factor = Math.pow(10, scale)
|
40
|
+
return Math.floor((value + +Number.EPSILON) * factor) / factor
|
41
|
+
}
|
42
|
+
|
43
|
+
export const roundUp = (value: number, scale: number) => {
|
44
|
+
const factor = Math.pow(10, scale)
|
45
|
+
return Math.ceil((value + +Number.EPSILON) * factor) / factor
|
46
|
+
}
|
47
|
+
|
48
|
+
/**
|
49
|
+
* Round Half-Even (Banker's Rounding) Utility
|
50
|
+
* https://en.wikipedia.org/wiki/Rounding#Round_half_to_even
|
51
|
+
*
|
52
|
+
* Mostly copied from this Github: https://github.com/schowdhuri/round-half-even
|
53
|
+
*/
|
54
|
+
export const roundHalfEven = (value: number, scale: number): number => {
|
55
|
+
if (value < 0) {
|
56
|
+
return -roundHalfEven(-value, scale)
|
57
|
+
}
|
58
|
+
if (scale === 0) {
|
59
|
+
return roundHalfEven(value / 10, 1) * 10
|
60
|
+
}
|
61
|
+
|
62
|
+
const MAX_DECIMALS_ALLOWED = 20
|
63
|
+
if (scale > MAX_DECIMALS_ALLOWED) {
|
64
|
+
throw new Error(`Cannot handle more than ${MAX_DECIMALS_ALLOWED} decimals`)
|
65
|
+
}
|
66
|
+
|
67
|
+
// convert to string; remove trailing 0s
|
68
|
+
const isExponentialForm = value.toString().includes('e') || value.toString().includes('E')
|
69
|
+
const strNum = (isExponentialForm ? value.toFixed(MAX_DECIMALS_ALLOWED).toString() : value.toString()).replace(/0+$/, '')
|
70
|
+
const decimalIndex = strNum.indexOf('.')
|
71
|
+
if (decimalIndex < 0) {
|
72
|
+
// no fractional part
|
73
|
+
return value
|
74
|
+
}
|
75
|
+
let intPart: string = strNum.slice(0, decimalIndex)
|
76
|
+
if (intPart.length == 0) {
|
77
|
+
intPart = '0'
|
78
|
+
}
|
79
|
+
let fractPart = strNum.slice(decimalIndex + 1) // extract fractional part
|
80
|
+
if (fractPart.length < scale) {
|
81
|
+
return value
|
82
|
+
}
|
83
|
+
const followingDig = parseInt(fractPart[scale]!, 10)
|
84
|
+
if (followingDig < 5) {
|
85
|
+
// rounding not required
|
86
|
+
const newFractPart = fractPart.slice(0, scale)
|
87
|
+
return parseFloat(`${intPart}.${newFractPart}`)
|
88
|
+
}
|
89
|
+
if (followingDig === 5) {
|
90
|
+
const newFractPart = fractPart.slice(0, scale + 1)
|
91
|
+
if (parseInt(fractPart.slice(scale + 1), 10) > 0) {
|
92
|
+
fractPart = `${newFractPart}9`
|
93
|
+
} else {
|
94
|
+
fractPart = newFractPart
|
95
|
+
}
|
96
|
+
}
|
97
|
+
|
98
|
+
let nextDig = parseInt(fractPart[fractPart.length - 1]!, 10)
|
99
|
+
let carriedOver = 0
|
100
|
+
for (let ptr = fractPart.length - 1; ptr >= scale; ptr--) {
|
101
|
+
let dig = parseInt(fractPart[ptr - 1]!, 10) + carriedOver
|
102
|
+
if (nextDig > 5 || (nextDig == 5 && !isEven(dig))) {
|
103
|
+
++dig
|
104
|
+
}
|
105
|
+
if (dig > 9) {
|
106
|
+
dig -= 10
|
107
|
+
carriedOver = 1
|
108
|
+
} else {
|
109
|
+
carriedOver = 0
|
110
|
+
}
|
111
|
+
nextDig = dig
|
112
|
+
}
|
113
|
+
|
114
|
+
let newFractPart = ''
|
115
|
+
for (let ptr = scale - 2; ptr >= 0; ptr--) {
|
116
|
+
let d = parseInt(fractPart[ptr]!, 10) + carriedOver
|
117
|
+
if (d > 9) {
|
118
|
+
d -= 10
|
119
|
+
carriedOver = 1
|
120
|
+
} else {
|
121
|
+
carriedOver = 0
|
122
|
+
}
|
123
|
+
newFractPart = `${d}${newFractPart}`
|
124
|
+
}
|
125
|
+
|
126
|
+
const resolvedIntPart = parseInt(intPart, 10) + carriedOver
|
127
|
+
return parseFloat(`${resolvedIntPart}.${newFractPart}${nextDig}`)
|
128
|
+
}
|
129
|
+
|
130
|
+
export const random = (min: number, max: number): number => {
|
131
|
+
return Math.random() * (max - min) + min
|
132
|
+
}
|
package/src/misc.ts
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
import { Objects, Preconditions } from '@bessemer/cornerstone'
|
2
|
+
import { Equalitor } from '@bessemer/cornerstone/equalitor'
|
3
|
+
|
4
|
+
export const doUntilConsistent = <T>(supplier: (previous: T | null) => T, equals: Equalitor<T>): T => {
|
5
|
+
let done = false
|
6
|
+
let previousValue: T | null = null
|
7
|
+
let attempts = 0
|
8
|
+
do {
|
9
|
+
Preconditions.isTrue(attempts < 10)
|
10
|
+
|
11
|
+
const currentValue = supplier(previousValue)
|
12
|
+
|
13
|
+
if (Objects.isPresent(previousValue) && equals(previousValue, currentValue)) {
|
14
|
+
done = true
|
15
|
+
}
|
16
|
+
|
17
|
+
previousValue = currentValue
|
18
|
+
attempts++
|
19
|
+
} while (!done)
|
20
|
+
|
21
|
+
return previousValue
|
22
|
+
}
|
package/src/object.ts
ADDED
@@ -0,0 +1,236 @@
|
|
1
|
+
import {
|
2
|
+
clone as _clone,
|
3
|
+
cloneDeep as _cloneDeep,
|
4
|
+
invert as _invert,
|
5
|
+
isEqual as _isEqual,
|
6
|
+
isNil as _isNil,
|
7
|
+
isNumber,
|
8
|
+
isObject as _isObject,
|
9
|
+
isPlainObject as _isPlainObject,
|
10
|
+
isString,
|
11
|
+
isUndefined as _isUndefined,
|
12
|
+
mapValues as _mapValues,
|
13
|
+
merge as unsafeMerge,
|
14
|
+
mergeWith as unsafeMergeWith,
|
15
|
+
} from 'lodash-es'
|
16
|
+
import { produce } from 'immer'
|
17
|
+
import { NominalType } from '@bessemer/cornerstone/types'
|
18
|
+
import { Primitive, UnknownRecord } from 'type-fest'
|
19
|
+
|
20
|
+
export const update: typeof produce = produce
|
21
|
+
|
22
|
+
export const isUndefined = _isUndefined
|
23
|
+
export const isNil = _isNil
|
24
|
+
export const isPresent = <T>(value: T): value is NonNullable<T> => {
|
25
|
+
return !isNil(value)
|
26
|
+
}
|
27
|
+
export const isObject = _isObject
|
28
|
+
export const isPlainObject = _isPlainObject
|
29
|
+
export const deepEqual = _isEqual
|
30
|
+
export const invert = _invert
|
31
|
+
export const mapValues = _mapValues
|
32
|
+
|
33
|
+
export const clone = _clone
|
34
|
+
export const cloneDeep = _cloneDeep
|
35
|
+
|
36
|
+
export const mergeAll = <T>(objects: Array<T>): T => {
|
37
|
+
return objects.reduce((x, y) => merge(x, y))
|
38
|
+
}
|
39
|
+
|
40
|
+
export function merge<TObject, TSource>(object: TObject, source: TSource): TObject & TSource
|
41
|
+
export function merge<TObject, TSource1, TSource2>(object: TObject, source1: TSource1, source2: TSource2): TObject & TSource1 & TSource2
|
42
|
+
export function merge<TObject, TSource1, TSource2, TSource3>(
|
43
|
+
object: TObject,
|
44
|
+
source1: TSource1,
|
45
|
+
source2: TSource2,
|
46
|
+
source3: TSource3
|
47
|
+
): TObject & TSource1 & TSource2 & TSource3
|
48
|
+
export function merge<TObject, TSource1, TSource2, TSource3, TSource4>(
|
49
|
+
object: TObject,
|
50
|
+
source1: TSource1,
|
51
|
+
source2: TSource2,
|
52
|
+
source3: TSource3,
|
53
|
+
source4: TSource4
|
54
|
+
): TObject & TSource1 & TSource2 & TSource3 & TSource4
|
55
|
+
export function merge(object: any, ...otherArgs: any[]): any {
|
56
|
+
return unsafeMerge({}, object, ...otherArgs)
|
57
|
+
}
|
58
|
+
|
59
|
+
export function mergeInto<Source1, Source2>(source: Source1, values: Source2): asserts source is Source1 & Source2 {
|
60
|
+
unsafeMerge(source, values)
|
61
|
+
}
|
62
|
+
|
63
|
+
export const mergeWith: typeof unsafeMergeWith = (...args: Array<any>) => {
|
64
|
+
const clone = cloneDeep(args[0])
|
65
|
+
return unsafeMergeWith.apply(null, [clone, ...args.slice(1)])
|
66
|
+
}
|
67
|
+
|
68
|
+
export type ObjectDiffResult = {
|
69
|
+
elementsUpdated: Record<string, { originalValue: unknown; updatedValue: unknown }>
|
70
|
+
elementsAdded: UnknownRecord
|
71
|
+
elementsRemoved: UnknownRecord
|
72
|
+
}
|
73
|
+
|
74
|
+
export function diffShallow(original: UnknownRecord, updated: UnknownRecord): ObjectDiffResult {
|
75
|
+
const result: ObjectDiffResult = {
|
76
|
+
elementsUpdated: {},
|
77
|
+
elementsAdded: {},
|
78
|
+
elementsRemoved: {},
|
79
|
+
}
|
80
|
+
|
81
|
+
for (const [key, originalValue] of Object.entries(original)) {
|
82
|
+
const updatedValue = updated[key]
|
83
|
+
if (updatedValue === undefined) {
|
84
|
+
result.elementsRemoved[key] = originalValue
|
85
|
+
} else if (!deepEqual(originalValue, updatedValue)) {
|
86
|
+
result.elementsUpdated[key] = { originalValue: originalValue, updatedValue: updatedValue }
|
87
|
+
}
|
88
|
+
}
|
89
|
+
|
90
|
+
for (const [key, updatedValue] of Object.entries(updated)) {
|
91
|
+
const originalValue = original[key]
|
92
|
+
if (originalValue === undefined) {
|
93
|
+
result.elementsAdded[key] = updatedValue
|
94
|
+
}
|
95
|
+
}
|
96
|
+
return result
|
97
|
+
}
|
98
|
+
|
99
|
+
export const isValidKey = (field: PropertyKey, obj: object): field is keyof typeof obj => {
|
100
|
+
return field in obj
|
101
|
+
}
|
102
|
+
|
103
|
+
/** Determines if the list of fields are present on the object (not null or undefined), with type inference */
|
104
|
+
export function fieldsPresent<T extends object, K extends keyof T>(
|
105
|
+
object: T,
|
106
|
+
fields: Array<K>
|
107
|
+
): object is Exclude<T, K> & Required<{ [P in K]: NonNullable<T[P]> }> {
|
108
|
+
return fields.every((field) => isPresent(object[field]))
|
109
|
+
}
|
110
|
+
|
111
|
+
export type ObjectPath = {
|
112
|
+
path: Array<string | number>
|
113
|
+
}
|
114
|
+
|
115
|
+
export const path = (path: Array<string | number>): ObjectPath => {
|
116
|
+
return { path }
|
117
|
+
}
|
118
|
+
|
119
|
+
export const parsePath = (path: string): ObjectPath => {
|
120
|
+
const result: Array<string | number> = []
|
121
|
+
const regex = /([^.\[\]]+)|\[(\d+)]/g
|
122
|
+
|
123
|
+
let match: RegExpExecArray | null
|
124
|
+
while ((match = regex.exec(path)) !== null) {
|
125
|
+
if (match[1] !== undefined) {
|
126
|
+
result.push(match[1])
|
127
|
+
} else if (match[2] !== undefined) {
|
128
|
+
result.push(Number(match[2]))
|
129
|
+
}
|
130
|
+
}
|
131
|
+
|
132
|
+
return { path: result }
|
133
|
+
}
|
134
|
+
|
135
|
+
const pathify = (path: ObjectPath | string): ObjectPath => {
|
136
|
+
if (isString(path)) {
|
137
|
+
return parsePath(path)
|
138
|
+
}
|
139
|
+
|
140
|
+
return path as ObjectPath
|
141
|
+
}
|
142
|
+
|
143
|
+
export const getPathValue = (object: UnknownRecord, initialPath: ObjectPath | string): unknown | undefined => {
|
144
|
+
const path = pathify(initialPath)
|
145
|
+
let current: any = object
|
146
|
+
|
147
|
+
for (const key of path.path) {
|
148
|
+
if (isPrimitive(current)) {
|
149
|
+
return undefined
|
150
|
+
}
|
151
|
+
|
152
|
+
current = current[key]
|
153
|
+
}
|
154
|
+
|
155
|
+
return current
|
156
|
+
}
|
157
|
+
|
158
|
+
export const applyPathValue = (object: UnknownRecord, initialPath: ObjectPath | string, value: unknown): UnknownRecord | undefined => {
|
159
|
+
const path = pathify(initialPath)
|
160
|
+
|
161
|
+
const newObject = update(object, (draft) => {
|
162
|
+
let current: any = draft
|
163
|
+
|
164
|
+
for (let i = 0; i < path.path.length; i++) {
|
165
|
+
const key = path.path[i]!
|
166
|
+
const isLastKey = i === path.path.length - 1
|
167
|
+
|
168
|
+
if (isPrimitive(current)) {
|
169
|
+
return
|
170
|
+
}
|
171
|
+
|
172
|
+
if (Array.isArray(current)) {
|
173
|
+
if (!isNumber(key)) {
|
174
|
+
return
|
175
|
+
}
|
176
|
+
|
177
|
+
if (key >= current.length) {
|
178
|
+
return
|
179
|
+
}
|
180
|
+
}
|
181
|
+
|
182
|
+
if (isLastKey) {
|
183
|
+
current[key] = value
|
184
|
+
} else {
|
185
|
+
current = current[key]
|
186
|
+
}
|
187
|
+
}
|
188
|
+
})
|
189
|
+
|
190
|
+
if (newObject === object) {
|
191
|
+
return undefined
|
192
|
+
}
|
193
|
+
|
194
|
+
return newObject
|
195
|
+
}
|
196
|
+
|
197
|
+
const isPrimitive = (value: any): value is Primitive => {
|
198
|
+
return value === null || (typeof value !== 'object' && typeof value !== 'function')
|
199
|
+
}
|
200
|
+
|
201
|
+
type TransformFunction = (value: any, path: (string | number)[], key: string | number, parent: any) => any
|
202
|
+
|
203
|
+
const walk = (value: any, transform: TransformFunction, path: (string | number)[] = []): any => {
|
204
|
+
if (isNil(value) || isPrimitive(value)) {
|
205
|
+
return value
|
206
|
+
}
|
207
|
+
|
208
|
+
if (Array.isArray(value)) {
|
209
|
+
return value.map((value, index) => {
|
210
|
+
const currentPath = [...path, index]
|
211
|
+
return walk(transform(value, currentPath, index, value), transform, currentPath)
|
212
|
+
})
|
213
|
+
}
|
214
|
+
|
215
|
+
const result: any = {}
|
216
|
+
for (const key in value) {
|
217
|
+
if (Object.prototype.hasOwnProperty.call(value, key)) {
|
218
|
+
const currentPath = [...path, key]
|
219
|
+
const transformedValue = transform(value[key], currentPath, key, value)
|
220
|
+
result[key] = walk(transformedValue, transform, currentPath)
|
221
|
+
}
|
222
|
+
}
|
223
|
+
|
224
|
+
return result
|
225
|
+
}
|
226
|
+
|
227
|
+
export type RecordAttribute<Type = unknown, Class extends string = 'RecordAttribute'> = NominalType<string, [Type, Class]>
|
228
|
+
type RecordAttributeType<Attribute> = Attribute extends RecordAttribute<infer Type, string> ? Type : never
|
229
|
+
|
230
|
+
export const getAttribute = <T extends RecordAttribute<unknown, string>>(record: UnknownRecord, attribute: T): RecordAttributeType<T> | undefined => {
|
231
|
+
return record[attribute] as RecordAttributeType<T> | undefined
|
232
|
+
}
|
233
|
+
|
234
|
+
export const coerceNil = <T>(value: T | null | undefined): T | undefined => {
|
235
|
+
return isNil(value) ? undefined : value
|
236
|
+
}
|
package/src/patch.ts
ADDED
@@ -0,0 +1,128 @@
|
|
1
|
+
import {
|
2
|
+
ArrayExpressions,
|
3
|
+
EvaluateExpression,
|
4
|
+
Expression,
|
5
|
+
Expressions,
|
6
|
+
NumericExpressions,
|
7
|
+
ReducingExpression,
|
8
|
+
} from '@bessemer/cornerstone/expression'
|
9
|
+
import { Objects, Preconditions } from '@bessemer/cornerstone'
|
10
|
+
import { UnknownRecord } from 'type-fest'
|
11
|
+
|
12
|
+
export enum PatchType {
|
13
|
+
Set = 'Set',
|
14
|
+
Apply = 'Apply',
|
15
|
+
Patch = 'Patch',
|
16
|
+
}
|
17
|
+
|
18
|
+
export type SetPatch<T> = {
|
19
|
+
_PatchType: PatchType.Set
|
20
|
+
value: Expression<T>
|
21
|
+
}
|
22
|
+
|
23
|
+
export type ApplyPatch<T> = {
|
24
|
+
_PatchType: PatchType.Apply
|
25
|
+
value: Expression<T>
|
26
|
+
reducer: ReducingExpression<T, T>
|
27
|
+
}
|
28
|
+
|
29
|
+
export type PatchPatch<T> = {
|
30
|
+
_PatchType: PatchType.Patch
|
31
|
+
patch: Patchable<T>
|
32
|
+
}
|
33
|
+
|
34
|
+
export type Patch<T> = SetPatch<T> | ApplyPatch<T> | PatchPatch<T>
|
35
|
+
|
36
|
+
export type PatchValue<T> = {
|
37
|
+
value: T
|
38
|
+
patch: Patch<T>
|
39
|
+
}
|
40
|
+
|
41
|
+
export type Patchable<T> = {
|
42
|
+
[P in keyof T]?: T[P] extends Array<infer U>
|
43
|
+
? Patch<U[]> | Patchable<U[]>
|
44
|
+
: T[P] extends object | undefined
|
45
|
+
? Patch<T[P]> | Patchable<T[P]>
|
46
|
+
: Patch<T[P]> | T[P]
|
47
|
+
}
|
48
|
+
|
49
|
+
export const set = <T>(value: Expression<T>): Patch<T> => {
|
50
|
+
return {
|
51
|
+
_PatchType: PatchType.Set,
|
52
|
+
value: value as any,
|
53
|
+
}
|
54
|
+
}
|
55
|
+
|
56
|
+
export const apply = <T>(value: Expression<T>, reducer: ReducingExpression<T, T>): Patch<T> => {
|
57
|
+
return {
|
58
|
+
_PatchType: PatchType.Apply,
|
59
|
+
value,
|
60
|
+
reducer,
|
61
|
+
}
|
62
|
+
}
|
63
|
+
|
64
|
+
export const patch = <T extends UnknownRecord, N extends Patchable<T> = Patchable<T>>(patch: N): Patch<T> => {
|
65
|
+
return {
|
66
|
+
_PatchType: PatchType.Patch,
|
67
|
+
patch,
|
68
|
+
}
|
69
|
+
}
|
70
|
+
|
71
|
+
export const sum = (value: Expression<number>): Patch<number> => {
|
72
|
+
return apply(value, Expressions.reference(NumericExpressions.SumExpression))
|
73
|
+
}
|
74
|
+
|
75
|
+
export const multiply = (value: Expression<number>): Patch<number> => {
|
76
|
+
return apply(value, Expressions.reference(NumericExpressions.MultiplyExpression))
|
77
|
+
}
|
78
|
+
|
79
|
+
export const concatenate = <T extends Array<Expression<unknown>>>(value: Expression<T>): Patch<T> => {
|
80
|
+
return apply(value, Expressions.reference(ArrayExpressions.ConcatenateExpression)) as Patch<T>
|
81
|
+
}
|
82
|
+
|
83
|
+
export type ResolvePatchesResult<T> = {
|
84
|
+
value: T
|
85
|
+
patchValues: Array<PatchValue<T>>
|
86
|
+
}
|
87
|
+
|
88
|
+
export const resolveWithDetails = <T>(value: T, patches: Array<Patch<T>>, evaluate: EvaluateExpression): ResolvePatchesResult<T> => {
|
89
|
+
let currentValue: T = value
|
90
|
+
|
91
|
+
const patchValues = patches.map((patch) => {
|
92
|
+
switch (patch._PatchType) {
|
93
|
+
case PatchType.Set:
|
94
|
+
currentValue = evaluate(patch.value)
|
95
|
+
break
|
96
|
+
case PatchType.Apply:
|
97
|
+
currentValue = evaluate(Expressions.dereference(patch.reducer, [currentValue, patch.value]))
|
98
|
+
break
|
99
|
+
case PatchType.Patch:
|
100
|
+
currentValue = applyPatch(currentValue, patch.patch, evaluate)
|
101
|
+
break
|
102
|
+
default:
|
103
|
+
Preconditions.isUnreachable(() => `Unrecognized PatchType for value: ${JSON.stringify(it)}`)
|
104
|
+
}
|
105
|
+
|
106
|
+
return { value: currentValue, patch }
|
107
|
+
})
|
108
|
+
|
109
|
+
return { value: currentValue, patchValues }
|
110
|
+
}
|
111
|
+
|
112
|
+
export const resolve = <T>(value: T, patches: Array<Patch<T>>, evaluate: EvaluateExpression): T => {
|
113
|
+
return resolveWithDetails(value, patches, evaluate).value
|
114
|
+
}
|
115
|
+
|
116
|
+
const applyPatch = <T>(value: T, patch: Patchable<T>, evaluate: EvaluateExpression): T => {
|
117
|
+
return Objects.mergeWith(value, patch, (value, patch) => {
|
118
|
+
if (Objects.isNil(patch)) {
|
119
|
+
return value
|
120
|
+
}
|
121
|
+
|
122
|
+
if (!Objects.isObject(patch) || !('_PatchType' in patch)) {
|
123
|
+
return undefined
|
124
|
+
}
|
125
|
+
|
126
|
+
return evaluate(resolve(value, [patch as Patch<T>], evaluate))
|
127
|
+
})
|
128
|
+
}
|
@@ -0,0 +1,25 @@
|
|
1
|
+
import { Lazy, Objects } from '@bessemer/cornerstone'
|
2
|
+
import { LazyValue } from '@bessemer/cornerstone/lazy'
|
3
|
+
import { Nil } from '@bessemer/cornerstone/types'
|
4
|
+
|
5
|
+
export function isUnreachable(message: LazyValue<string> = 'Preconditions.isUnreachable was reached'): never {
|
6
|
+
throw new Error(Lazy.evaluate(message))
|
7
|
+
}
|
8
|
+
|
9
|
+
export function isTrue(value: boolean, message: LazyValue<string> = 'Preconditions.isTrue failed validation'): asserts value is true {
|
10
|
+
if (!value) {
|
11
|
+
throw new Error(Lazy.evaluate(message))
|
12
|
+
}
|
13
|
+
}
|
14
|
+
|
15
|
+
export function isFalse(value: boolean, message: LazyValue<string> = 'Preconditions.isFalse failed validation'): asserts value is false {
|
16
|
+
return isTrue(!value, message)
|
17
|
+
}
|
18
|
+
|
19
|
+
export function isNil(value: any, message: LazyValue<string> = 'Preconditions.isNil failed validation'): asserts value is Nil {
|
20
|
+
return isTrue(Objects.isNil(value), message)
|
21
|
+
}
|
22
|
+
|
23
|
+
export function isPresent<T>(value: T, message: LazyValue<string> = 'Preconditions.isPresent failed validation'): asserts value is NonNullable<T> {
|
24
|
+
return isTrue(Objects.isPresent(value), message)
|
25
|
+
}
|
package/src/promise.ts
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
export type PromiseContext<T> = { promise: Promise<T>; resolve: (value: T) => void; reject: (reason?: any) => void }
|
2
|
+
|
3
|
+
export const isPromise = <T>(element: T | Promise<T>): element is Promise<T> => {
|
4
|
+
return typeof (element as Promise<T>).then === 'function'
|
5
|
+
}
|
6
|
+
|
7
|
+
export const create = <T>(): PromiseContext<T> => {
|
8
|
+
let resolveVar
|
9
|
+
let rejectVar
|
10
|
+
const promise = new Promise<T>((resolve, reject) => {
|
11
|
+
resolveVar = resolve
|
12
|
+
rejectVar = reject
|
13
|
+
})
|
14
|
+
|
15
|
+
return { promise, resolve: resolveVar!, reject: rejectVar! }
|
16
|
+
}
|