@lowdefy/helpers 5.6.0 → 6.1.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.
@@ -24,6 +24,7 @@
24
24
  'errorPage.name': 'Error',
25
25
  'errorPage.message': 'An error has occurred.',
26
26
  'errorPage.homeLink': 'Return to home page',
27
+ 'server.genericError': 'Something went wrong.',
27
28
  // Agent runtime — surfaces in browser as HTTP error responses / streamed errors
28
29
  'agent.runtime.methodNotAllowed': 'Only POST requests are supported.',
29
30
  'agent.runtime.invalidPath': 'Invalid agent path',
@@ -46,8 +46,22 @@ function symbolMarker(val) {
46
46
  }
47
47
  const MAX_CAUSE_DEPTH = 3;
48
48
  const MAX_OBJECT_DEPTH = 5;
49
- const NO_OMIT = [];
50
- function cleanValue(val, seen, objectDepth, causeDepth, omit) {
49
+ // Today's fields in today's order. The own `cause` key is skipped in the spread
50
+ // so an assigned (and therefore enumerable) cause is not listed twice.
51
+ function defaultProjection(err) {
52
+ const props = {
53
+ message: err.message,
54
+ name: err.name,
55
+ stack: err.stack,
56
+ cause: err.cause
57
+ };
58
+ for (const key of Object.keys(err)){
59
+ if (key === 'cause') continue;
60
+ props[key] = err[key];
61
+ }
62
+ return props;
63
+ }
64
+ function cleanValue(val, seen, objectDepth, causeDepth, project) {
51
65
  if (objectDepth > MAX_OBJECT_DEPTH) return '[Truncated]';
52
66
  if (typeof val === 'function') return functionMarker(val);
53
67
  if (typeof val === 'bigint') return bigintMarker(val);
@@ -56,81 +70,73 @@ function cleanValue(val, seen, objectDepth, causeDepth, omit) {
56
70
  if (seen.has(val)) return '[Circular]';
57
71
  seen.add(val);
58
72
  if (Array.isArray(val)) {
59
- return val.map((item)=>cleanValue(item, seen, objectDepth + 1, causeDepth, omit));
73
+ return val.map((item)=>cleanValue(item, seen, objectDepth + 1, causeDepth, project));
60
74
  }
61
75
  if (val instanceof Date) return val;
62
- if (val instanceof Error) return _extractErrorProps(val, seen, objectDepth, causeDepth, omit);
76
+ if (val instanceof Error) return _extractErrorProps(val, seen, objectDepth, causeDepth, project);
63
77
  if (!isPlainObject(val)) return classInstanceMarker(val);
64
78
  const cleaned = {};
65
79
  for (const [k, v] of Object.entries(val)){
66
- const cv = cleanValue(v, seen, objectDepth + 1, causeDepth, omit);
80
+ const cv = cleanValue(v, seen, objectDepth + 1, causeDepth, project);
67
81
  if (cv !== undefined) cleaned[k] = cv;
68
82
  }
69
83
  return cleaned;
70
84
  }
71
- function _extractErrorProps(err, seen, objectDepth, causeDepth, omit) {
85
+ function cleanCause(cause, seen, objectDepth, causeDepth, project) {
86
+ if (!(cause instanceof Error)) {
87
+ return cleanValue(cause, seen, objectDepth + 1, causeDepth, project);
88
+ }
89
+ if (seen.has(cause)) return '[Circular]';
90
+ if (causeDepth >= MAX_CAUSE_DEPTH) return '[Truncated]';
91
+ return _extractErrorProps(cause, seen, objectDepth, causeDepth + 1, project);
92
+ }
93
+ function cleanPropValue(value, seen, objectDepth, causeDepth, project) {
94
+ if (typeof value === 'function') return functionMarker(value);
95
+ if (typeof value === 'bigint') return bigintMarker(value);
96
+ if (typeof value === 'symbol') return symbolMarker(value);
97
+ if (value === null || typeof value !== 'object') return value;
98
+ if (value instanceof Date) return value;
99
+ if (Array.isArray(value)) return cleanValue(value, seen, objectDepth + 1, causeDepth, project);
100
+ if (value instanceof Error) {
101
+ if (seen.has(value)) return '[Circular]';
102
+ return _extractErrorProps(value, seen, objectDepth, causeDepth, project);
103
+ }
104
+ if (isPlainObject(value)) return cleanValue(value, seen, objectDepth + 1, causeDepth, project);
105
+ // A class instance (Socket, Agent, ClientRequest, ...). Its internals are never exposed —
106
+ // only the type name — but the key must not vanish: an absent key is indistinguishable
107
+ // from "no such field", and this output is now read by `get` as app-visible config data.
108
+ return classInstanceMarker(value);
109
+ }
110
+ function _extractErrorProps(err, seen, objectDepth, causeDepth, project) {
72
111
  if (!err) return err;
73
112
  seen.add(err);
74
113
  // Called at every error node rather than once for the walk, so a caller's
75
114
  // policy can key on the node itself - its class, or its own cause value.
76
115
  // 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;
116
+ // walk's; this function only cleans the entries it is handed. An entry the
117
+ // projection leaves out is never built, so a dropped cause is never
118
+ // deep-copied and never marked in `seen`.
119
+ const projected = (project ?? defaultProjection)(err);
81
120
  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);
94
- }
95
- }
96
- for (const key of Object.keys(err)){
97
- if (key === 'cause' || omitted.includes(key)) continue;
98
- const value = err[key];
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') {
106
- props[key] = value;
107
- } else if (value instanceof Date) {
108
- props[key] = value;
109
- } else if (Array.isArray(value)) {
110
- props[key] = cleanValue(value, seen, objectDepth + 1, causeDepth, omit);
111
- } else if (value instanceof Error) {
112
- if (seen.has(value)) {
113
- props[key] = '[Circular]';
114
- } else {
115
- props[key] = _extractErrorProps(value, seen, objectDepth, causeDepth, omit);
116
- }
117
- } else if (isPlainObject(value)) {
118
- props[key] = cleanValue(value, seen, objectDepth + 1, causeDepth, omit);
121
+ for (const [key, value] of Object.entries(projected)){
122
+ if (value === undefined) continue;
123
+ if (key === 'cause') {
124
+ props.cause = cleanCause(value, seen, objectDepth, causeDepth, project);
119
125
  } 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);
126
+ props[key] = cleanPropValue(value, seen, objectDepth, causeDepth, project);
124
127
  }
125
128
  }
126
129
  return props;
127
130
  }
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);
131
+ // project: (err) => object - the props to emit for THIS error node, called once
132
+ // per node. Entries are cleaned as own properties are: Errors recurse through
133
+ // the same `project`, the returned `cause` is walked in place of the original,
134
+ // and `undefined` entries are dropped. Returning neither `message` nor `name` is
135
+ // legal but degrades the wire format rather than just trimming it: serializer's
136
+ // propsToError revives a cause as an Error only when it has a `message`, and
137
+ // looks the Lowdefy error class up by `name`. Leave either out and a round-trip
138
+ // yields plain objects.
139
+ function extractErrorProps(err, { project } = {}) {
140
+ return _extractErrorProps(err, new Set(), 0, 0, project);
135
141
  }
136
142
  export default extractErrorProps;
@@ -0,0 +1,35 @@
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
+ // Keys that look like operators (single key starting with _) but are not.
17
+ const KNOWN_NON_OPERATORS = new Set([
18
+ '_id'
19
+ ]);
20
+ // Returns the normalized operator name ('_get' for { '_get.key': ... },
21
+ // '_state' for { __state: ... }) when the value is an operator object, else
22
+ // null. An operator object has exactly one non-~-prefixed key starting with _
23
+ // (~ keys like ~k configKeys may sit alongside the operator key).
24
+ function getOperatorType(value) {
25
+ if (!type.isObject(value)) return null;
26
+ const nonTildeKeys = Object.keys(value).filter((key)=>!key.startsWith('~'));
27
+ if (nonTildeKeys.length !== 1) return null;
28
+ const [op] = nonTildeKeys[0].split('.');
29
+ const operator = op.replace(/^_+/, '_');
30
+ if (operator.length > 1 && operator[0] === '_' && !KNOWN_NON_OPERATORS.has(operator)) {
31
+ return operator;
32
+ }
33
+ return null;
34
+ }
35
+ export default getOperatorType;
package/dist/index.js CHANGED
@@ -21,11 +21,14 @@ import getKey from './getKey.js';
21
21
  import getLocaleDateFormat from './getLocaleDateFormat.js';
22
22
  import getLocaleDecimalSeparator from './getLocaleDecimalSeparator.js';
23
23
  import getLocaleGroupSeparator from './getLocaleGroupSeparator.js';
24
+ import getOperatorType from './getOperatorType.js';
24
25
  import { isReserved, ReservedKeyError } from './ReservedKeyError.js';
25
26
  import joinPath from './joinPath.js';
26
27
  import LRUCache from './LRUCache.js';
28
+ import mapPlainValues from './mapPlainValues.js';
27
29
  import mergeObjects from './mergeObjects.js';
28
30
  import omit from './omit.js';
31
+ import projectCaughtError from './projectCaughtError.js';
29
32
  import serializer from './serializer.js';
30
33
  import set from './set.js';
31
34
  import setKey from './setKey.js';
@@ -38,4 +41,4 @@ import unset from './unset.js';
38
41
  import unsetKey from './unsetKey.js';
39
42
  import urlQuery from './urlQuery.js';
40
43
  import wait from './wait.js';
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 };
44
+ export { applyArrayIndices, builtinMessages, cachedPromises, extractErrorProps, get, getKey, getLocaleDateFormat, getLocaleDecimalSeparator, getLocaleGroupSeparator, getOperatorType, isReserved, joinPath, LRUCache, mapPlainValues, mergeObjects, omit, projectCaughtError, ReservedKeyError, serializer, set, setKey, splitPath, stableStringify, swap, translate, type, unset, unsetKey, urlQuery, wait };
@@ -0,0 +1,51 @@
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
+ */ // type.isObject also accepts class instances, whose internals a copy would expose.
16
+ function isPlainObject(value) {
17
+ if (value === null || typeof value !== 'object') return false;
18
+ const proto = Object.getPrototypeOf(value);
19
+ return proto === Object.prototype || proto === null;
20
+ }
21
+ function mapNode(value, key, visit, copies) {
22
+ const visited = visit(value, key);
23
+ if (visited !== value) return visited;
24
+ if (!Array.isArray(value) && !isPlainObject(value)) return value;
25
+ // Reusing the copy of an object already seen keeps shared references and cycles
26
+ // intact instead of recursing forever.
27
+ if (copies.has(value)) return copies.get(value);
28
+ if (Array.isArray(value)) {
29
+ const mappedArray = [];
30
+ copies.set(value, mappedArray);
31
+ value.forEach((item)=>{
32
+ mappedArray.push(mapNode(item, undefined, visit, copies));
33
+ });
34
+ return mappedArray;
35
+ }
36
+ const mappedObject = {};
37
+ copies.set(value, mappedObject);
38
+ Object.entries(value).forEach(([entryKey, item])=>{
39
+ mappedObject[entryKey] = mapNode(item, entryKey, visit, copies);
40
+ });
41
+ return mappedObject;
42
+ }
43
+ // Copies the plain objects and arrays in a value, calling visit(value, key) on every
44
+ // node first - key is the object key, undefined for the root and array items. A
45
+ // different return replaces the node; the same value is kept, and descended into
46
+ // when it is a plain object or array. Class instances, Dates and Errors are kept
47
+ // by reference. The input is never mutated.
48
+ function mapPlainValues(value, visit) {
49
+ return mapNode(value, undefined, visit, new Map());
50
+ }
51
+ export default mapPlainValues;
@@ -0,0 +1,84 @@
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 { lowdefyErrorTypes, readErrorCodes } from '@lowdefy/errors';
16
+ import mapPlainValues from './mapPlainValues.js';
17
+ import type from './type.js';
18
+ function identity(value) {
19
+ return value;
20
+ }
21
+ function scrubStrings(value, scrub) {
22
+ return mapPlainValues(value, (item)=>type.isString(item) ? scrub(item) : item);
23
+ }
24
+ // Own-key lookup: an error named after an Object.prototype member, such as
25
+ // 'constructor', must fall back to Error rather than pick up that member.
26
+ function prototypeFor(name) {
27
+ if (Object.hasOwn(lowdefyErrorTypes, name)) {
28
+ return lowdefyErrorTypes[name].prototype;
29
+ }
30
+ return Error.prototype;
31
+ }
32
+ function projectNode(error, scrub, seen) {
33
+ seen.add(error);
34
+ const projected = Object.create(prototypeFor(error.name));
35
+ // Non-enumerable, as on a constructed Error, so JSON.stringify skips it: an
36
+ // `_error` sent to a plugin as data (an HTTP body, a database insert) then
37
+ // carries no message. Assigning it would make it enumerable, which is also
38
+ // why this does not go through serializer.copy.
39
+ Object.defineProperty(projected, 'message', {
40
+ value: scrub(error.message),
41
+ enumerable: false,
42
+ writable: true,
43
+ configurable: true
44
+ });
45
+ const { code, statusCode } = readErrorCodes(error);
46
+ const fields = {
47
+ name: error.name,
48
+ code: type.isString(code) ? scrub(code) : code,
49
+ statusCode,
50
+ // The server's error sink sets `handled` once it has logged the error, and
51
+ // the client skips reporting a handled error back to the server. A rethrown
52
+ // `_error` without it would be logged a second time.
53
+ handled: error.handled
54
+ };
55
+ for (const [key, value] of Object.entries(fields)){
56
+ if (!type.isUndefined(value)) projected[key] = value;
57
+ }
58
+ const { cause } = error;
59
+ if (type.isError(cause)) {
60
+ if (!seen.has(cause)) projected.cause = projectNode(cause, scrub, seen);
61
+ } else if (error.name === 'UserError') {
62
+ // The author wrote a UserError's cause and metaData in config; any other
63
+ // class's non-Error cause is library data, such as a response body.
64
+ if (!type.isUndefined(cause)) projected.cause = scrubStrings(cause, scrub);
65
+ }
66
+ if (error.name === 'UserError' && !type.isUndefined(error.metaData)) {
67
+ projected.metaData = scrubStrings(error.metaData, scrub);
68
+ }
69
+ return projected;
70
+ }
71
+ // The value `_error` reads: the caught error rebuilt with only the fields a
72
+ // routine or catch list branches on. `received`, `configKey`, `location`,
73
+ // `source`, `config` and `stack` are left out - `received` can hold a token
74
+ // fetched at runtime that the scrub does not know. The result is an Error, so
75
+ // wherever config sends it on, the serializer's wire policy applies to it.
76
+ function projectCaughtError(error, { scrub = identity } = {}) {
77
+ if (!type.isError(error)) {
78
+ return projectCaughtError(new Error(String(error)), {
79
+ scrub
80
+ });
81
+ }
82
+ return projectNode(error, scrub, new Set());
83
+ }
84
+ export default projectCaughtError;
@@ -12,23 +12,10 @@
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
- */ import { ActionError, BlockError, BuildError, ConfigError, ConfigWarning, LowdefyInternalError, OperatorError, PluginError, RequestError, ServiceError, UserError } from '@lowdefy/errors';
15
+ */ import { lowdefyErrorTypes } from '@lowdefy/errors';
16
16
  import extractErrorProps from './extractErrorProps.js';
17
17
  import type from './type.js';
18
18
  import stableStringify from './stableStringify.js';
19
- const lowdefyErrorTypes = {
20
- ActionError,
21
- BlockError,
22
- BuildError,
23
- ConfigError,
24
- ConfigWarning,
25
- LowdefyInternalError,
26
- OperatorError,
27
- PluginError,
28
- RequestError,
29
- ServiceError,
30
- UserError
31
- };
32
19
  function propsToError(data) {
33
20
  const ErrorClass = lowdefyErrorTypes[data.name] || Error;
34
21
  const error = Object.create(ErrorClass.prototype);
@@ -41,7 +28,7 @@ function propsToError(data) {
41
28
  }
42
29
  return error;
43
30
  }
44
- const makeReplacer = ({ replacer, isoStringDates, skipMarkers, omitErrorProps } = {})=>(key, value)=>{
31
+ const makeReplacer = ({ replacer, isoStringDates, skipMarkers, projectError } = {})=>(key, value)=>{
45
32
  let dateReplacer = (date)=>({
46
33
  '~d': date.valueOf()
47
34
  });
@@ -57,7 +44,7 @@ const makeReplacer = ({ replacer, isoStringDates, skipMarkers, omitErrorProps }
57
44
  if (type.isError(newValue)) {
58
45
  return {
59
46
  '~e': extractErrorProps(newValue, {
60
- omit: omitErrorProps
47
+ project: projectError
61
48
  })
62
49
  };
63
50
  }
@@ -224,7 +211,7 @@ const serialize = (json, options = {})=>{
224
211
  return JSON.parse(JSON.stringify(json, makeReplacer({
225
212
  replacer: options.replacer,
226
213
  isoStringDates: options.isoStringDates,
227
- omitErrorProps: options.omitErrorProps
214
+ projectError: options.projectError
228
215
  })));
229
216
  };
230
217
  const serializeToString = (json, options = {})=>{
@@ -240,7 +227,7 @@ const serializeToString = (json, options = {})=>{
240
227
  replacer: makeReplacer({
241
228
  replacer: options.replacer,
242
229
  skipMarkers: options.skipMarkers,
243
- omitErrorProps: options.omitErrorProps
230
+ projectError: options.projectError
244
231
  }),
245
232
  space: options.space
246
233
  });
@@ -249,7 +236,7 @@ const serializeToString = (json, options = {})=>{
249
236
  replacer: options.replacer,
250
237
  isoStringDates: options.isoStringDates,
251
238
  skipMarkers: options.skipMarkers,
252
- omitErrorProps: options.omitErrorProps
239
+ projectError: options.projectError
253
240
  }), options.space);
254
241
  };
255
242
  const deserialize = (json, options = {})=>{
@@ -265,7 +252,7 @@ const copy = (json, options = {})=>{
265
252
  if (type.isDate(json)) return new Date(json.valueOf());
266
253
  return JSON.parse(JSON.stringify(json, makeReplacer({
267
254
  replacer: options.replacer,
268
- omitErrorProps: options.omitErrorProps
255
+ projectError: options.projectError
269
256
  })), makeReviver(options.reviver));
270
257
  };
271
258
  const serializer = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lowdefy/helpers",
3
- "version": "5.6.0",
3
+ "version": "6.1.0",
4
4
  "license": "Apache-2.0",
5
5
  "description": "",
6
6
  "homepage": "https://lowdefy.com",
@@ -31,13 +31,13 @@
31
31
  "dist/*"
32
32
  ],
33
33
  "dependencies": {
34
- "@lowdefy/errors": "5.6.0",
34
+ "@lowdefy/errors": "6.1.0",
35
35
  "intl-messageformat": "11.2.7"
36
36
  },
37
37
  "devDependencies": {
38
38
  "@jest/globals": "28.1.3",
39
- "@swc/cli": "0.8.0",
40
- "@swc/core": "1.15.18",
39
+ "@swc/cli": "0.8.1",
40
+ "@swc/core": "1.15.32",
41
41
  "@swc/jest": "0.2.39",
42
42
  "jest": "28.1.3",
43
43
  "jest-diff": "28.1.3"