@lowdefy/helpers 5.5.1 → 5.6.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/README.md +404 -13
- package/dist/ReservedKeyError.js +38 -0
- package/dist/extractErrorProps.js +84 -25
- package/dist/get.js +61 -70
- package/dist/getKey.js +36 -0
- package/dist/index.js +7 -1
- package/dist/joinPath.js +24 -0
- package/dist/mergeObjects.js +55 -7
- package/dist/serializer.js +28 -8
- package/dist/set.js +53 -68
- package/dist/setKey.js +33 -0
- package/dist/splitPath.js +51 -0
- package/dist/type.js +35 -116
- package/dist/unset.js +49 -59
- package/dist/unsetKey.js +36 -0
- package/dist/urlQuery.js +12 -3
- package/package.json +3 -4
|
@@ -16,62 +16,121 @@
|
|
|
16
16
|
const proto = Object.getPrototypeOf(val);
|
|
17
17
|
return proto === Object.prototype || proto === null;
|
|
18
18
|
}
|
|
19
|
+
// `||`, not `??`: an anonymous class has the empty string as its constructor
|
|
20
|
+
// name, not undefined, and a marker whose only payload is the type name must
|
|
21
|
+
// not be emitted as '[Object: ]'.
|
|
22
|
+
function classInstanceMarker(val) {
|
|
23
|
+
return `[Object: ${val.constructor?.name || 'unknown'}]`;
|
|
24
|
+
}
|
|
25
|
+
// A function is never passed through: `get` would hand a live closure over
|
|
26
|
+
// server state to app config. (serializer.copy only throws on a function at
|
|
27
|
+
// the top level; nested, it silently drops the key or turns it into null in
|
|
28
|
+
// an array - since this output is always an object, that vanished key, not a
|
|
29
|
+
// throw, was the real pre-fix failure.) Same empty-string fallback as
|
|
30
|
+
// classInstanceMarker - an anonymous function's `name` is '' rather than
|
|
31
|
+
// undefined.
|
|
32
|
+
function functionMarker(val) {
|
|
33
|
+
return `[Function: ${val.name || 'unknown'}]`;
|
|
34
|
+
}
|
|
35
|
+
// A bigint's digits are its whole content, not internals, so the marker
|
|
36
|
+
// includes the value rather than just naming the type.
|
|
37
|
+
function bigintMarker(val) {
|
|
38
|
+
return `[BigInt: ${val}]`;
|
|
39
|
+
}
|
|
40
|
+
// `.description` gives the bare description; `String(sym)` would wrap it as
|
|
41
|
+
// 'Symbol(description)', polluting the marker's payload. Same empty-string
|
|
42
|
+
// fallback as the other markers - a descriptionless symbol's `description` is
|
|
43
|
+
// undefined, and `Symbol('')`'s is '', so both must fall back to 'unknown'.
|
|
44
|
+
function symbolMarker(val) {
|
|
45
|
+
return `[Symbol: ${val.description || 'unknown'}]`;
|
|
46
|
+
}
|
|
19
47
|
const MAX_CAUSE_DEPTH = 3;
|
|
20
48
|
const MAX_OBJECT_DEPTH = 5;
|
|
21
|
-
|
|
49
|
+
const NO_OMIT = [];
|
|
50
|
+
function cleanValue(val, seen, objectDepth, causeDepth, omit) {
|
|
22
51
|
if (objectDepth > MAX_OBJECT_DEPTH) return '[Truncated]';
|
|
52
|
+
if (typeof val === 'function') return functionMarker(val);
|
|
53
|
+
if (typeof val === 'bigint') return bigintMarker(val);
|
|
54
|
+
if (typeof val === 'symbol') return symbolMarker(val);
|
|
23
55
|
if (val === null || typeof val !== 'object') return val;
|
|
24
56
|
if (seen.has(val)) return '[Circular]';
|
|
25
57
|
seen.add(val);
|
|
26
58
|
if (Array.isArray(val)) {
|
|
27
|
-
return val.map((item)=>cleanValue(item, seen, objectDepth + 1, causeDepth));
|
|
59
|
+
return val.map((item)=>cleanValue(item, seen, objectDepth + 1, causeDepth, omit));
|
|
28
60
|
}
|
|
29
61
|
if (val instanceof Date) return val;
|
|
30
|
-
if (val instanceof Error) return _extractErrorProps(val, seen, objectDepth, causeDepth);
|
|
31
|
-
if (!isPlainObject(val)) return
|
|
62
|
+
if (val instanceof Error) return _extractErrorProps(val, seen, objectDepth, causeDepth, omit);
|
|
63
|
+
if (!isPlainObject(val)) return classInstanceMarker(val);
|
|
32
64
|
const cleaned = {};
|
|
33
65
|
for (const [k, v] of Object.entries(val)){
|
|
34
|
-
const cv = cleanValue(v, seen, objectDepth + 1, causeDepth);
|
|
66
|
+
const cv = cleanValue(v, seen, objectDepth + 1, causeDepth, omit);
|
|
35
67
|
if (cv !== undefined) cleaned[k] = cv;
|
|
36
68
|
}
|
|
37
69
|
return cleaned;
|
|
38
70
|
}
|
|
39
|
-
function _extractErrorProps(err, seen, objectDepth, causeDepth) {
|
|
71
|
+
function _extractErrorProps(err, seen, objectDepth, causeDepth, omit) {
|
|
40
72
|
if (!err) return err;
|
|
41
73
|
seen.add(err);
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
74
|
+
// Called at every error node rather than once for the walk, so a caller's
|
|
75
|
+
// policy can key on the node itself - its class, or its own cause value.
|
|
76
|
+
// Which fields an audience may see is the caller's decision, never this
|
|
77
|
+
// walk's; this function only applies the keys it is handed. Omitted fields
|
|
78
|
+
// are skipped before they are built, so a dropped cause is never deep-copied
|
|
79
|
+
// and never marked in `seen`.
|
|
80
|
+
const omitted = omit?.(err) ?? NO_OMIT;
|
|
81
|
+
const props = {};
|
|
82
|
+
if (!omitted.includes('message')) props.message = err.message;
|
|
83
|
+
if (!omitted.includes('name')) props.name = err.name;
|
|
84
|
+
if (!omitted.includes('stack')) props.stack = err.stack;
|
|
85
|
+
if (err.cause !== undefined && !omitted.includes('cause')) {
|
|
86
|
+
if (!(err.cause instanceof Error)) {
|
|
87
|
+
props.cause = cleanValue(err.cause, seen, objectDepth + 1, causeDepth, omit);
|
|
88
|
+
} else if (seen.has(err.cause)) {
|
|
89
|
+
props.cause = '[Circular]';
|
|
90
|
+
} else if (causeDepth >= MAX_CAUSE_DEPTH) {
|
|
91
|
+
props.cause = '[Truncated]';
|
|
92
|
+
} else {
|
|
93
|
+
props.cause = _extractErrorProps(err.cause, seen, objectDepth, causeDepth + 1, omit);
|
|
52
94
|
}
|
|
53
95
|
}
|
|
54
96
|
for (const key of Object.keys(err)){
|
|
55
|
-
if (key === 'cause') continue;
|
|
97
|
+
if (key === 'cause' || omitted.includes(key)) continue;
|
|
56
98
|
const value = err[key];
|
|
57
|
-
if (
|
|
99
|
+
if (typeof value === 'function') {
|
|
100
|
+
props[key] = functionMarker(value);
|
|
101
|
+
} else if (typeof value === 'bigint') {
|
|
102
|
+
props[key] = bigintMarker(value);
|
|
103
|
+
} else if (typeof value === 'symbol') {
|
|
104
|
+
props[key] = symbolMarker(value);
|
|
105
|
+
} else if (value === null || typeof value !== 'object') {
|
|
58
106
|
props[key] = value;
|
|
59
107
|
} else if (value instanceof Date) {
|
|
60
108
|
props[key] = value;
|
|
61
109
|
} else if (Array.isArray(value)) {
|
|
62
|
-
props[key] = cleanValue(value, seen, objectDepth + 1, causeDepth);
|
|
110
|
+
props[key] = cleanValue(value, seen, objectDepth + 1, causeDepth, omit);
|
|
63
111
|
} else if (value instanceof Error) {
|
|
64
|
-
if (
|
|
65
|
-
props[key] =
|
|
112
|
+
if (seen.has(value)) {
|
|
113
|
+
props[key] = '[Circular]';
|
|
114
|
+
} else {
|
|
115
|
+
props[key] = _extractErrorProps(value, seen, objectDepth, causeDepth, omit);
|
|
66
116
|
}
|
|
67
117
|
} else if (isPlainObject(value)) {
|
|
68
|
-
props[key] = cleanValue(value, seen, objectDepth + 1, causeDepth);
|
|
118
|
+
props[key] = cleanValue(value, seen, objectDepth + 1, causeDepth, omit);
|
|
119
|
+
} else {
|
|
120
|
+
// A class instance (Socket, Agent, ClientRequest, ...). Its internals are never exposed —
|
|
121
|
+
// only the type name — but the key must not vanish: an absent key is indistinguishable
|
|
122
|
+
// from "no such field", and this output is now read by `get` as app-visible config data.
|
|
123
|
+
props[key] = classInstanceMarker(value);
|
|
69
124
|
}
|
|
70
|
-
// Skip class instances (Socket, Agent, ClientRequest, etc.)
|
|
71
125
|
}
|
|
72
126
|
return props;
|
|
73
127
|
}
|
|
74
|
-
|
|
75
|
-
|
|
128
|
+
// omit: (err) => string[] - the field names to leave off THIS error node, called
|
|
129
|
+
// once per node. Returning 'message' or 'name' is legal but degrades the wire
|
|
130
|
+
// format rather than just trimming it: serializer's propsToError revives a cause
|
|
131
|
+
// as an Error only when it has a `message`, and looks the Lowdefy error class up
|
|
132
|
+
// by `name`. Omit either and a round-trip yields plain objects.
|
|
133
|
+
function extractErrorProps(err, { omit } = {}) {
|
|
134
|
+
return _extractErrorProps(err, new Set(), 0, 0, omit);
|
|
76
135
|
}
|
|
77
136
|
export default extractErrorProps;
|
package/dist/get.js
CHANGED
|
@@ -32,95 +32,86 @@
|
|
|
32
32
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
33
33
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
34
34
|
// THE SOFTWARE.
|
|
35
|
-
import
|
|
35
|
+
import type from './type.js';
|
|
36
|
+
import extractErrorProps from './extractErrorProps.js';
|
|
36
37
|
import serializer from './serializer.js';
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
38
|
+
import splitPath from './splitPath.js';
|
|
39
|
+
import { isReserved, ReservedKeyError } from './ReservedKeyError.js';
|
|
40
|
+
// "May I step into this value to reach a child?" Plain objects, arrays and errors are traversable;
|
|
41
|
+
// functions, Date, URL, Map, Set, RegExp, Promise, Buffer and typed arrays are not, because
|
|
42
|
+
// serializing their internals is information disclosure. Class instances are NOT excluded:
|
|
43
|
+
// type.isObject cannot distinguish them from plain objects, since kindOf maps every
|
|
44
|
+
// `[object Object]` tag to 'object', so an instance's own properties are reachable. Narrowing the
|
|
45
|
+
// predicate is a cross-cutting decision - mergeObjects sits on the same type.isObject boundary -
|
|
46
|
+
// and is tracked separately. Note the predicate cannot close the disclosure case anyway: when the
|
|
47
|
+
// live object is the endpoint of the path it is returned (and under copy, serialized) without
|
|
48
|
+
// isTraversable ever being consulted. Error is handled by forLookup, which converts it to plain
|
|
49
|
+
// data before a lookup.
|
|
50
|
+
// Kept local rather than shared with set/unset - a module imported by all three trips a Jest ESM
|
|
51
|
+
// module-cache bug in operators-js' mocked re-imports.
|
|
52
|
+
function isTraversable(value) {
|
|
53
|
+
return type.isObject(value) || Array.isArray(value) || value instanceof Error;
|
|
48
54
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
55
|
+
// A lookup on an Error reads the error's plain-data form, so `name` resolves (it is own on the
|
|
56
|
+
// extracted props, inherited on the instance) and an own key holding a class instance arrives as
|
|
57
|
+
// a marker rather than a live object. Applied only where a lookup happens, so an error that is
|
|
58
|
+
// the endpoint of the path is returned as an Error - `_actions: x.error` is unchanged.
|
|
59
|
+
function forLookup(value) {
|
|
60
|
+
if (value instanceof Error) {
|
|
61
|
+
return extractErrorProps(value);
|
|
52
62
|
}
|
|
53
|
-
return
|
|
54
|
-
}
|
|
55
|
-
function isValidObject(val) {
|
|
56
|
-
return typeTest.isObject(val) || Array.isArray(val) || typeof val === 'function';
|
|
63
|
+
return value;
|
|
57
64
|
}
|
|
58
65
|
function getter(target, path, options) {
|
|
59
|
-
if (
|
|
60
|
-
return
|
|
66
|
+
if (type.isNone(path) || !isTraversable(target)) {
|
|
67
|
+
return options.default;
|
|
61
68
|
}
|
|
62
|
-
if (
|
|
69
|
+
if (type.isNumber(path)) {
|
|
63
70
|
path = String(path);
|
|
64
71
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
const splitChar = options.separator || '.';
|
|
68
|
-
const joinChar = options.joinChar || (typeof splitChar === 'string' ? splitChar : '.');
|
|
69
|
-
if (isString && path in target) {
|
|
70
|
-
return isValid(path, target, options) ? target[path] : options.default;
|
|
72
|
+
if (!type.isString(path)) {
|
|
73
|
+
return options.default;
|
|
71
74
|
}
|
|
72
|
-
const segs =
|
|
75
|
+
const segs = splitPath(path);
|
|
76
|
+
// Scanned before the walk so a reserved key is rejected even when it is an own property - the
|
|
77
|
+
// reserved rule is about illegal input, not about what the target holds. This also means a
|
|
78
|
+
// literal dotted key whose segments include a reserved name, such as 'a.constructor', throws
|
|
79
|
+
// rather than resolving; set and unset reject it identically. A rejoined candidate always
|
|
80
|
+
// contains a dot, so it can never equal a reserved name; scanning the split segments stays
|
|
81
|
+
// sufficient.
|
|
82
|
+
const reservedSeg = segs.find(isReserved);
|
|
83
|
+
if (!type.isNone(reservedSeg)) {
|
|
84
|
+
throw new ReservedKeyError(reservedSeg);
|
|
85
|
+
}
|
|
86
|
+
let current = forLookup(target);
|
|
73
87
|
const len = segs.length;
|
|
74
88
|
let idx = 0;
|
|
75
|
-
|
|
76
|
-
let
|
|
77
|
-
|
|
78
|
-
|
|
89
|
+
while(idx < len){
|
|
90
|
+
let candidate = segs[idx];
|
|
91
|
+
let next = idx + 1;
|
|
92
|
+
// The strict segment wins if present. On a miss, grow the candidate one segment at a time
|
|
93
|
+
// looking for a literal dotted key - shortest-first, no backtracking, so segments are
|
|
94
|
+
// consumed monotonically and the walk stays a single forward pass.
|
|
95
|
+
while(!Object.hasOwn(current, candidate) && next < len){
|
|
96
|
+
candidate = `${candidate}.${segs[next]}`;
|
|
97
|
+
next += 1;
|
|
79
98
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
prop = join([
|
|
83
|
-
prop.slice(0, -1),
|
|
84
|
-
segs[idx] || ''
|
|
85
|
-
], joinChar, options);
|
|
99
|
+
if (!Object.hasOwn(current, candidate)) {
|
|
100
|
+
return options.default;
|
|
86
101
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
target = target[prop];
|
|
92
|
-
} else {
|
|
93
|
-
let hasProp = false;
|
|
94
|
-
let n = idx + 1;
|
|
95
|
-
while(n < len){
|
|
96
|
-
prop = join([
|
|
97
|
-
prop,
|
|
98
|
-
segs[n]
|
|
99
|
-
], joinChar, options);
|
|
100
|
-
n += 1;
|
|
101
|
-
hasProp = prop in target;
|
|
102
|
-
if (hasProp) {
|
|
103
|
-
if (!isValid(prop, target, options)) {
|
|
104
|
-
return options.default;
|
|
105
|
-
}
|
|
106
|
-
target = target[prop];
|
|
107
|
-
idx = n - 1;
|
|
108
|
-
break;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
if (!hasProp) {
|
|
102
|
+
current = current[candidate];
|
|
103
|
+
idx = next;
|
|
104
|
+
if (idx < len) {
|
|
105
|
+
if (!isTraversable(current)) {
|
|
112
106
|
return options.default;
|
|
113
107
|
}
|
|
108
|
+
current = forLookup(current);
|
|
114
109
|
}
|
|
115
|
-
// eslint-disable-next-line no-plusplus
|
|
116
|
-
}while (++idx < len && isValidObject(target))
|
|
117
|
-
if (idx === len) {
|
|
118
|
-
return target;
|
|
119
110
|
}
|
|
120
|
-
return
|
|
111
|
+
return current;
|
|
121
112
|
}
|
|
122
113
|
function get(target, path, options) {
|
|
123
|
-
if (!
|
|
114
|
+
if (!type.isObject(options)) {
|
|
124
115
|
options = {
|
|
125
116
|
default: options
|
|
126
117
|
};
|
package/dist/getKey.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/ import type from './type.js';
|
|
16
|
+
import { isReserved, ReservedKeyError } from './ReservedKeyError.js';
|
|
17
|
+
// Reads a single literal key off a plain object. Dots are not path separators.
|
|
18
|
+
function getKey(target, key, defaultValue) {
|
|
19
|
+
if (!type.isObject(target)) {
|
|
20
|
+
throw new TypeError('getKey: target must be a plain object');
|
|
21
|
+
}
|
|
22
|
+
if (!type.isString(key)) {
|
|
23
|
+
throw new TypeError('getKey: key must be a string');
|
|
24
|
+
}
|
|
25
|
+
// Rejected even on null-proto targets, where the read would be safe: a caller seeing
|
|
26
|
+
// ReservedKeyError always knows the key was rejected, regardless of target shape.
|
|
27
|
+
if (isReserved(key)) {
|
|
28
|
+
throw new ReservedKeyError(key);
|
|
29
|
+
}
|
|
30
|
+
// Own-property read so inherited Object.prototype members never leak as values.
|
|
31
|
+
if (Object.hasOwn(target, key)) {
|
|
32
|
+
return target[key];
|
|
33
|
+
}
|
|
34
|
+
return defaultValue;
|
|
35
|
+
}
|
|
36
|
+
export default getKey;
|
package/dist/index.js
CHANGED
|
@@ -17,19 +17,25 @@ import builtinMessages from './builtinMessages.js';
|
|
|
17
17
|
import cachedPromises from './cachedPromises.js';
|
|
18
18
|
import extractErrorProps from './extractErrorProps.js';
|
|
19
19
|
import get from './get.js';
|
|
20
|
+
import getKey from './getKey.js';
|
|
20
21
|
import getLocaleDateFormat from './getLocaleDateFormat.js';
|
|
21
22
|
import getLocaleDecimalSeparator from './getLocaleDecimalSeparator.js';
|
|
22
23
|
import getLocaleGroupSeparator from './getLocaleGroupSeparator.js';
|
|
24
|
+
import { isReserved, ReservedKeyError } from './ReservedKeyError.js';
|
|
25
|
+
import joinPath from './joinPath.js';
|
|
23
26
|
import LRUCache from './LRUCache.js';
|
|
24
27
|
import mergeObjects from './mergeObjects.js';
|
|
25
28
|
import omit from './omit.js';
|
|
26
29
|
import serializer from './serializer.js';
|
|
27
30
|
import set from './set.js';
|
|
31
|
+
import setKey from './setKey.js';
|
|
32
|
+
import splitPath from './splitPath.js';
|
|
28
33
|
import stableStringify from './stableStringify.js';
|
|
29
34
|
import swap from './swap.js';
|
|
30
35
|
import translate from './translate.js';
|
|
31
36
|
import type from './type.js';
|
|
32
37
|
import unset from './unset.js';
|
|
38
|
+
import unsetKey from './unsetKey.js';
|
|
33
39
|
import urlQuery from './urlQuery.js';
|
|
34
40
|
import wait from './wait.js';
|
|
35
|
-
export { applyArrayIndices, builtinMessages, cachedPromises, extractErrorProps, get, getLocaleDateFormat, getLocaleDecimalSeparator, getLocaleGroupSeparator, LRUCache, mergeObjects, omit, serializer, set, stableStringify, swap, translate, type, unset, urlQuery, wait };
|
|
41
|
+
export { applyArrayIndices, builtinMessages, cachedPromises, extractErrorProps, get, getKey, getLocaleDateFormat, getLocaleDecimalSeparator, getLocaleGroupSeparator, isReserved, joinPath, LRUCache, mergeObjects, omit, ReservedKeyError, serializer, set, setKey, splitPath, stableStringify, swap, translate, type, unset, unsetKey, urlQuery, wait };
|
package/dist/joinPath.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/ import type from './type.js';
|
|
16
|
+
function joinPath(segments) {
|
|
17
|
+
if (!type.isArray(segments)) {
|
|
18
|
+
throw new TypeError(`joinPath: segments must be an array. Received ${JSON.stringify(segments)}.`);
|
|
19
|
+
}
|
|
20
|
+
// Escape the escape character first, then the separator, so a segment ending in a
|
|
21
|
+
// backslash cannot swallow the dot that follows it when splitPath reads it back.
|
|
22
|
+
return segments.map((segment)=>String(segment).replace(/\\/g, '\\\\').replace(/\./g, '\\.')).join('.');
|
|
23
|
+
}
|
|
24
|
+
export default joinPath;
|
package/dist/mergeObjects.js
CHANGED
|
@@ -12,14 +12,62 @@
|
|
|
12
12
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
13
|
See the License for the specific language governing permissions and
|
|
14
14
|
limitations under the License.
|
|
15
|
-
*/ //
|
|
16
|
-
import
|
|
15
|
+
*/ // Imported from the source module, not index.js, to avoid a circular import.
|
|
16
|
+
import { isReserved } from './ReservedKeyError.js';
|
|
17
17
|
import type from './type.js';
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
// Plain objects and arrays are the config data model and are always copied; every other value is
|
|
19
|
+
// an opaque leaf, shared by reference. This restores what lodash.merge provided - it cloned
|
|
20
|
+
// plain-object and array source values into the destination while aliasing everything else. The
|
|
21
|
+
// split is not "is it a class instance" but type.isObject's own boundary: kindOf sorts a value as
|
|
22
|
+
// a plain object whenever it fails every built-in check and its Symbol.toStringTag tail check
|
|
23
|
+
// reads '[object Object]'. Date, RegExp, Map, Set, Error, Promise, typed arrays, Buffer, URL and
|
|
24
|
+
// any class that sets Symbol.toStringTag are shared by reference; a plain user class is not - it
|
|
25
|
+
// has no such tag, so it is copied and flattened into a bare object, losing its prototype.
|
|
26
|
+
//
|
|
27
|
+
// clone only walks Object.keys/array indices, so it carries own enumerable string keys forward
|
|
28
|
+
// and drops everything else: non-enumerable markers such as the build's `~k`/`~r`/`~l` keys, and
|
|
29
|
+
// symbol keys. Nothing merges those markers today, but a future caller that does would lose them
|
|
30
|
+
// silently. clone also always produces an object with Object.prototype, so a null-prototype
|
|
31
|
+
// object (e.g. Object.create(null), used deliberately for pollution hardening) loses that
|
|
32
|
+
// hardening if it passes through a merge; nothing does today.
|
|
33
|
+
function clone(value) {
|
|
34
|
+
if (type.isArray(value)) {
|
|
35
|
+
return value.map(clone);
|
|
36
|
+
}
|
|
37
|
+
if (!type.isObject(value)) {
|
|
38
|
+
return value;
|
|
39
|
+
}
|
|
40
|
+
const result = {};
|
|
41
|
+
for (const key of Object.keys(value)){
|
|
42
|
+
if (isReserved(key)) continue;
|
|
43
|
+
result[key] = clone(value[key]);
|
|
44
|
+
}
|
|
45
|
+
return result;
|
|
46
|
+
}
|
|
47
|
+
function deepMerge(target, source) {
|
|
48
|
+
const result = {};
|
|
49
|
+
for (const key of Object.keys(target)){
|
|
50
|
+
if (isReserved(key)) continue;
|
|
51
|
+
result[key] = clone(target[key]);
|
|
52
|
+
}
|
|
53
|
+
for (const key of Object.keys(source)){
|
|
54
|
+
// Value-side pollution guard. The reserved name arrives as data inside a merged value,
|
|
55
|
+
// not as a path a developer typed, so dropping it misroutes nothing — skip, don't throw.
|
|
56
|
+
if (isReserved(key)) continue;
|
|
57
|
+
if (type.isObject(result[key]) && type.isObject(source[key])) {
|
|
58
|
+
result[key] = deepMerge(result[key], source[key]);
|
|
59
|
+
} else {
|
|
60
|
+
// Everything that is not a plain object is a leaf, arrays included: a later array
|
|
61
|
+
// replaces an earlier one rather than index-merging into it.
|
|
62
|
+
result[key] = clone(source[key]);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return result;
|
|
66
|
+
}
|
|
67
|
+
function mergeObjects(objects) {
|
|
20
68
|
if (type.isArray(objects)) {
|
|
21
|
-
|
|
69
|
+
return objects.filter((obj)=>type.isObject(obj)).reduce((merged, obj)=>deepMerge(merged, obj), {});
|
|
22
70
|
}
|
|
23
|
-
return
|
|
24
|
-
}
|
|
71
|
+
return objects;
|
|
72
|
+
}
|
|
25
73
|
export default mergeObjects;
|
package/dist/serializer.js
CHANGED
|
@@ -41,7 +41,7 @@ function propsToError(data) {
|
|
|
41
41
|
}
|
|
42
42
|
return error;
|
|
43
43
|
}
|
|
44
|
-
const makeReplacer = (
|
|
44
|
+
const makeReplacer = ({ replacer, isoStringDates, skipMarkers, omitErrorProps } = {})=>(key, value)=>{
|
|
45
45
|
let dateReplacer = (date)=>({
|
|
46
46
|
'~d': date.valueOf()
|
|
47
47
|
});
|
|
@@ -51,12 +51,14 @@ const makeReplacer = (customReplacer, isoStringDates, skipMarkers)=>(key, value)
|
|
|
51
51
|
});
|
|
52
52
|
}
|
|
53
53
|
let newValue = value;
|
|
54
|
-
if (
|
|
55
|
-
newValue =
|
|
54
|
+
if (replacer) {
|
|
55
|
+
newValue = replacer(key, value);
|
|
56
56
|
}
|
|
57
57
|
if (type.isError(newValue)) {
|
|
58
58
|
return {
|
|
59
|
-
'~e': extractErrorProps(newValue
|
|
59
|
+
'~e': extractErrorProps(newValue, {
|
|
60
|
+
omit: omitErrorProps
|
|
61
|
+
})
|
|
60
62
|
};
|
|
61
63
|
}
|
|
62
64
|
if (type.isObject(newValue)) {
|
|
@@ -217,7 +219,13 @@ const serialize = (json, options = {})=>{
|
|
|
217
219
|
'~d': json.valueOf()
|
|
218
220
|
};
|
|
219
221
|
}
|
|
220
|
-
|
|
222
|
+
// skipMarkers is deliberately not threaded here - serialize has never applied
|
|
223
|
+
// it, and its callers depend on markers surviving.
|
|
224
|
+
return JSON.parse(JSON.stringify(json, makeReplacer({
|
|
225
|
+
replacer: options.replacer,
|
|
226
|
+
isoStringDates: options.isoStringDates,
|
|
227
|
+
omitErrorProps: options.omitErrorProps
|
|
228
|
+
})));
|
|
221
229
|
};
|
|
222
230
|
const serializeToString = (json, options = {})=>{
|
|
223
231
|
if (type.isUndefined(json)) return json;
|
|
@@ -229,11 +237,20 @@ const serializeToString = (json, options = {})=>{
|
|
|
229
237
|
}
|
|
230
238
|
if (options.stable) {
|
|
231
239
|
return stableStringify(json, {
|
|
232
|
-
replacer: makeReplacer(
|
|
240
|
+
replacer: makeReplacer({
|
|
241
|
+
replacer: options.replacer,
|
|
242
|
+
skipMarkers: options.skipMarkers,
|
|
243
|
+
omitErrorProps: options.omitErrorProps
|
|
244
|
+
}),
|
|
233
245
|
space: options.space
|
|
234
246
|
});
|
|
235
247
|
}
|
|
236
|
-
return JSON.stringify(json, makeReplacer(
|
|
248
|
+
return JSON.stringify(json, makeReplacer({
|
|
249
|
+
replacer: options.replacer,
|
|
250
|
+
isoStringDates: options.isoStringDates,
|
|
251
|
+
skipMarkers: options.skipMarkers,
|
|
252
|
+
omitErrorProps: options.omitErrorProps
|
|
253
|
+
}), options.space);
|
|
237
254
|
};
|
|
238
255
|
const deserialize = (json, options = {})=>{
|
|
239
256
|
if (type.isUndefined(json)) return json;
|
|
@@ -246,7 +263,10 @@ const deserializeFromString = (str, options = {})=>{
|
|
|
246
263
|
const copy = (json, options = {})=>{
|
|
247
264
|
if (type.isUndefined(json)) return undefined;
|
|
248
265
|
if (type.isDate(json)) return new Date(json.valueOf());
|
|
249
|
-
return JSON.parse(JSON.stringify(json, makeReplacer(
|
|
266
|
+
return JSON.parse(JSON.stringify(json, makeReplacer({
|
|
267
|
+
replacer: options.replacer,
|
|
268
|
+
omitErrorProps: options.omitErrorProps
|
|
269
|
+
})), makeReviver(options.reviver));
|
|
250
270
|
};
|
|
251
271
|
const serializer = {
|
|
252
272
|
copy,
|