@lowdefy/helpers 6.0.0 → 7.0.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.
@@ -0,0 +1,88 @@
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
+ // A ServiceError's Retry-After (seconds, or an HTTP date), so a routine can wait as long
55
+ // as the service asked before retrying, such as an enrichment worker's backoff.
56
+ retryAfter: error.name === 'ServiceError' ? error.retryAfter ?? undefined : undefined
57
+ };
58
+ for (const [key, value] of Object.entries(fields)){
59
+ if (!type.isUndefined(value)) projected[key] = value;
60
+ }
61
+ const { cause } = error;
62
+ if (type.isError(cause)) {
63
+ if (!seen.has(cause)) projected.cause = projectNode(cause, scrub, seen);
64
+ } else if (error.name === 'UserError') {
65
+ // The author wrote a UserError's cause and metaData in config; any other
66
+ // class's non-Error cause is library data, such as a response body.
67
+ if (!type.isUndefined(cause)) projected.cause = scrubStrings(cause, scrub);
68
+ }
69
+ if (error.name === 'UserError' && !type.isUndefined(error.metaData)) {
70
+ projected.metaData = scrubStrings(error.metaData, scrub);
71
+ }
72
+ return projected;
73
+ }
74
+ // The value `_error` reads: the caught error rebuilt with only the fields a
75
+ // routine or catch list branches on (name, message, code, statusCode, handled, a
76
+ // ServiceError's retryAfter, and the cause). `received`, `configKey`, `location`,
77
+ // `source`, `config` and `stack` are left out - `received` can hold a token
78
+ // fetched at runtime that the scrub does not know. The result is an Error, so
79
+ // wherever config sends it on, the serializer's wire policy applies to it.
80
+ function projectCaughtError(error, { scrub = identity } = {}) {
81
+ if (!type.isError(error)) {
82
+ return projectCaughtError(new Error(String(error)), {
83
+ scrub
84
+ });
85
+ }
86
+ return projectNode(error, scrub, new Set());
87
+ }
88
+ export default projectCaughtError;
@@ -0,0 +1,42 @@
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
+ */ // Reads a quoted value from `start` (just past its opening quote). posthog-js escapes a quote
16
+ // in a value as \". Returns the value and the index past its closing quote, or the end of the
17
+ // chain when the quote is never closed.
18
+ function readChainValue(chain, start) {
19
+ let value = '';
20
+ let index = start;
21
+ while(index < chain.length){
22
+ const char = chain[index];
23
+ if (char === '\\' && chain[index + 1] === '"') {
24
+ value += '"';
25
+ index += 2;
26
+ continue;
27
+ }
28
+ if (char === '"') {
29
+ return {
30
+ value,
31
+ end: index + 1
32
+ };
33
+ }
34
+ value += char;
35
+ index += 1;
36
+ }
37
+ return {
38
+ value,
39
+ end: index
40
+ };
41
+ }
42
+ export default readChainValue;
@@ -0,0 +1,34 @@
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 stampHash from './stampHash.js';
16
+ // The stamp hash of a caller's roles in their active organization: the app
17
+ // roles (_user.roles, which page and endpoint auth gates match) and the
18
+ // organization role tier (_user.org_roles: owner, admin or member), since app
19
+ // config can gate blocks on either. Each list is sorted, so the same set always
20
+ // hashes the same, and the pair is JSON-encoded, so no separator inside a role
21
+ // can make two lists read as one. The server stamps it on every response
22
+ // (callerStamp) and the client compares it with the roles of the caller it
23
+ // renders, so both sides use this one function.
24
+ function rolesHash({ user }) {
25
+ return stampHash(JSON.stringify({
26
+ roles: [
27
+ ...user.roles ?? []
28
+ ].sort(),
29
+ org_roles: [
30
+ ...user.org_roles ?? []
31
+ ].sort()
32
+ }));
33
+ }
34
+ export default rolesHash;
@@ -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
  }
@@ -219,12 +206,13 @@ const serialize = (json, options = {})=>{
219
206
  '~d': json.valueOf()
220
207
  };
221
208
  }
222
- // skipMarkers is deliberately not threaded here - serialize has never applied
223
- // it, and its callers depend on markers surviving.
209
+ // Markers survive unless the caller passes skipMarkers: config sent to the
210
+ // client needs them to resolve error locations, data does not.
224
211
  return JSON.parse(JSON.stringify(json, makeReplacer({
225
212
  replacer: options.replacer,
226
213
  isoStringDates: options.isoStringDates,
227
- omitErrorProps: options.omitErrorProps
214
+ skipMarkers: options.skipMarkers,
215
+ projectError: options.projectError
228
216
  })));
229
217
  };
230
218
  const serializeToString = (json, options = {})=>{
@@ -240,7 +228,7 @@ const serializeToString = (json, options = {})=>{
240
228
  replacer: makeReplacer({
241
229
  replacer: options.replacer,
242
230
  skipMarkers: options.skipMarkers,
243
- omitErrorProps: options.omitErrorProps
231
+ projectError: options.projectError
244
232
  }),
245
233
  space: options.space
246
234
  });
@@ -249,7 +237,7 @@ const serializeToString = (json, options = {})=>{
249
237
  replacer: options.replacer,
250
238
  isoStringDates: options.isoStringDates,
251
239
  skipMarkers: options.skipMarkers,
252
- omitErrorProps: options.omitErrorProps
240
+ projectError: options.projectError
253
241
  }), options.space);
254
242
  };
255
243
  const deserialize = (json, options = {})=>{
@@ -265,7 +253,7 @@ const copy = (json, options = {})=>{
265
253
  if (type.isDate(json)) return new Date(json.valueOf());
266
254
  return JSON.parse(JSON.stringify(json, makeReplacer({
267
255
  replacer: options.replacer,
268
- omitErrorProps: options.omitErrorProps
256
+ projectError: options.projectError
269
257
  })), makeReviver(options.reviver));
270
258
  };
271
259
  const serializer = {
@@ -0,0 +1,170 @@
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
+ */ // SHA-256 round constants: the first 32 bits of the fractional parts of the
16
+ // cube roots of the first 64 primes.
17
+ const K = [
18
+ 0x428a2f98,
19
+ 0x71374491,
20
+ 0xb5c0fbcf,
21
+ 0xe9b5dba5,
22
+ 0x3956c25b,
23
+ 0x59f111f1,
24
+ 0x923f82a4,
25
+ 0xab1c5ed5,
26
+ 0xd807aa98,
27
+ 0x12835b01,
28
+ 0x243185be,
29
+ 0x550c7dc3,
30
+ 0x72be5d74,
31
+ 0x80deb1fe,
32
+ 0x9bdc06a7,
33
+ 0xc19bf174,
34
+ 0xe49b69c1,
35
+ 0xefbe4786,
36
+ 0x0fc19dc6,
37
+ 0x240ca1cc,
38
+ 0x2de92c6f,
39
+ 0x4a7484aa,
40
+ 0x5cb0a9dc,
41
+ 0x76f988da,
42
+ 0x983e5152,
43
+ 0xa831c66d,
44
+ 0xb00327c8,
45
+ 0xbf597fc7,
46
+ 0xc6e00bf3,
47
+ 0xd5a79147,
48
+ 0x06ca6351,
49
+ 0x14292967,
50
+ 0x27b70a85,
51
+ 0x2e1b2138,
52
+ 0x4d2c6dfc,
53
+ 0x53380d13,
54
+ 0x650a7354,
55
+ 0x766a0abb,
56
+ 0x81c2c92e,
57
+ 0x92722c85,
58
+ 0xa2bfe8a1,
59
+ 0xa81a664b,
60
+ 0xc24b8b70,
61
+ 0xc76c51a3,
62
+ 0xd192e819,
63
+ 0xd6990624,
64
+ 0xf40e3585,
65
+ 0x106aa070,
66
+ 0x19a4c116,
67
+ 0x1e376c08,
68
+ 0x2748774c,
69
+ 0x34b0bcb5,
70
+ 0x391c0cb3,
71
+ 0x4ed8aa4a,
72
+ 0x5b9cca4f,
73
+ 0x682e6ff3,
74
+ 0x748f82ee,
75
+ 0x78a5636f,
76
+ 0x84c87814,
77
+ 0x8cc70208,
78
+ 0x90befffa,
79
+ 0xa4506ceb,
80
+ 0xbef9a3f7,
81
+ 0xc67178f2
82
+ ];
83
+ // SHA-256 initial hash values: the first 32 bits of the fractional parts of the
84
+ // square roots of the first 8 primes.
85
+ const H0 = [
86
+ 0x6a09e667,
87
+ 0xbb67ae85,
88
+ 0x3c6ef372,
89
+ 0xa54ff53a,
90
+ 0x510e527f,
91
+ 0x9b05688c,
92
+ 0x1f83d9ab,
93
+ 0x5be0cd19
94
+ ];
95
+ function rotateRight(value, bits) {
96
+ return value >>> bits | value << 32 - bits;
97
+ }
98
+ function utf8Bytes(text) {
99
+ const bytes = [];
100
+ for (const character of text){
101
+ const code = character.codePointAt(0);
102
+ if (code < 0x80) {
103
+ bytes.push(code);
104
+ } else if (code < 0x800) {
105
+ bytes.push(0xc0 | code >> 6, 0x80 | code & 0x3f);
106
+ } else if (code < 0x10000) {
107
+ bytes.push(0xe0 | code >> 12, 0x80 | code >> 6 & 0x3f, 0x80 | code & 0x3f);
108
+ } else {
109
+ bytes.push(0xf0 | code >> 18, 0x80 | code >> 12 & 0x3f, 0x80 | code >> 6 & 0x3f, 0x80 | code & 0x3f);
110
+ }
111
+ }
112
+ return bytes;
113
+ }
114
+ // SHA-256 (FIPS 180-4) of a string's UTF-8 bytes, as lowercase hex.
115
+ function sha256Hex(text) {
116
+ const bytes = utf8Bytes(text);
117
+ const bitLength = bytes.length * 8;
118
+ bytes.push(0x80);
119
+ while(bytes.length % 64 !== 56){
120
+ bytes.push(0);
121
+ }
122
+ // The message length in bits as a 64-bit big-endian integer. Stamp inputs
123
+ // (ids, role and organization lists) are far below 2^32 bits, so the high
124
+ // word is zero.
125
+ bytes.push(0, 0, 0, 0);
126
+ bytes.push(bitLength >>> 24 & 0xff, bitLength >>> 16 & 0xff, bitLength >>> 8 & 0xff);
127
+ bytes.push(bitLength & 0xff);
128
+ const hash = [
129
+ ...H0
130
+ ];
131
+ const w = new Array(64);
132
+ for(let offset = 0; offset < bytes.length; offset += 64){
133
+ for(let i = 0; i < 16; i += 1){
134
+ const j = offset + i * 4;
135
+ w[i] = (bytes[j] << 24 | bytes[j + 1] << 16 | bytes[j + 2] << 8 | bytes[j + 3]) >>> 0;
136
+ }
137
+ for(let i = 16; i < 64; i += 1){
138
+ const s0 = rotateRight(w[i - 15], 7) ^ rotateRight(w[i - 15], 18) ^ w[i - 15] >>> 3;
139
+ const s1 = rotateRight(w[i - 2], 17) ^ rotateRight(w[i - 2], 19) ^ w[i - 2] >>> 10;
140
+ w[i] = w[i - 16] + s0 + w[i - 7] + s1 >>> 0;
141
+ }
142
+ let [a, b, c, d, e, f, g, h] = hash;
143
+ for(let i = 0; i < 64; i += 1){
144
+ const S1 = rotateRight(e, 6) ^ rotateRight(e, 11) ^ rotateRight(e, 25);
145
+ const choice = e & f ^ ~e & g;
146
+ const temp1 = h + S1 + choice + K[i] + w[i] >>> 0;
147
+ const S0 = rotateRight(a, 2) ^ rotateRight(a, 13) ^ rotateRight(a, 22);
148
+ const majority = a & b ^ a & c ^ b & c;
149
+ const temp2 = S0 + majority >>> 0;
150
+ h = g;
151
+ g = f;
152
+ f = e;
153
+ e = d + temp1 >>> 0;
154
+ d = c;
155
+ c = b;
156
+ b = a;
157
+ a = temp1 + temp2 >>> 0;
158
+ }
159
+ hash[0] = hash[0] + a >>> 0;
160
+ hash[1] = hash[1] + b >>> 0;
161
+ hash[2] = hash[2] + c >>> 0;
162
+ hash[3] = hash[3] + d >>> 0;
163
+ hash[4] = hash[4] + e >>> 0;
164
+ hash[5] = hash[5] + f >>> 0;
165
+ hash[6] = hash[6] + g >>> 0;
166
+ hash[7] = hash[7] + h >>> 0;
167
+ }
168
+ return hash.map((word)=>word.toString(16).padStart(8, '0')).join('');
169
+ }
170
+ export default sha256Hex;
@@ -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 sha256Hex from './sha256Hex.js';
16
+ // A short fingerprint for one part of a response's caller stamp (see
17
+ // callerStamp): the first 16 hex characters of SHA-256 of the text. It is an
18
+ // identity comparison, not a secret: SHA-256 is implemented in sha256Hex rather
19
+ // than taken from WebCrypto, which browsers expose only on https and localhost
20
+ // pages, so the server and every page compute the same value synchronously.
21
+ function stampHash(text) {
22
+ return sha256Hex(text).slice(0, 16);
23
+ }
24
+ export default stampHash;
@@ -0,0 +1,103 @@
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 journeyTargetSelectors from './journeyTargetSelectors.js';
16
+ import parseElementsChain from './parseElementsChain.js';
17
+ import parseRowIndex from './parseRowIndex.js';
18
+ import type from './type.js';
19
+ const { blockWrapperPrefix, cellAttribute, interactiveRoles, rowAttribute } = journeyTargetSelectors;
20
+ function blockIdOf(entry) {
21
+ const id = entry.attributes.attr__id;
22
+ if (!type.isString(id) || !id.startsWith(blockWrapperPrefix)) {
23
+ return null;
24
+ }
25
+ return id.slice(blockWrapperPrefix.length);
26
+ }
27
+ function normaliseText(value) {
28
+ if (!type.isString(value)) {
29
+ return null;
30
+ }
31
+ const text = value.replace(/\s+/g, ' ').trim();
32
+ return text === '' ? null : text;
33
+ }
34
+ function isOption(entry) {
35
+ return entry.classes.includes('ant-select-item-option') || entry.attributes.attr__role === 'option';
36
+ }
37
+ // The chain form of the interactive-control selector. A radio or checkbox input inside a label
38
+ // is reached through the label, as the selector does.
39
+ function isInteractive(entry, position, scope) {
40
+ const { attributes, tag } = entry;
41
+ if (interactiveRoles.includes(attributes.attr__role)) {
42
+ return true;
43
+ }
44
+ if (tag === 'input') {
45
+ if (attributes.attr__type === 'hidden') {
46
+ return false;
47
+ }
48
+ const isToggle = attributes.attr__type === 'radio' || attributes.attr__type === 'checkbox';
49
+ return !(isToggle && scope.slice(position + 1).some((outer)=>outer.tag === 'label'));
50
+ }
51
+ if (tag === 'a') {
52
+ return type.isString(attributes.href) || type.isString(attributes.attr__href);
53
+ }
54
+ return [
55
+ 'button',
56
+ 'textarea',
57
+ 'select',
58
+ 'label'
59
+ ].includes(tag);
60
+ }
61
+ // The text of the control the click reached (an interactive control or a dropdown option): its
62
+ // own text, else (a label or option whose text sits in a child) the first text between the
63
+ // clicked element and it. posthog-js records the clicked element's direct text and the text of
64
+ // buttons, links and other form elements. A click that reached no control has no text, as in
65
+ // describeElement: its block alone targets it.
66
+ function textOf(scope) {
67
+ const controlIndex = scope.findIndex((entry, position)=>isInteractive(entry, position, scope) || isOption(entry));
68
+ if (controlIndex === -1) {
69
+ return null;
70
+ }
71
+ for (const entry of [
72
+ scope[controlIndex],
73
+ ...scope.slice(0, controlIndex)
74
+ ]){
75
+ const text = normaliseText(entry.attributes.text);
76
+ if (text !== null) {
77
+ return text;
78
+ }
79
+ }
80
+ return null;
81
+ }
82
+ // The journey target a posthog-js `$elements_chain` describes: the block wrappers the click
83
+ // passed through (innermost first), the grid row and column, the control's text, and whether it
84
+ // was a dropdown option. Pure, so the production pull runs it in Node on captured events. Total:
85
+ // any string gives a target and never throws.
86
+ function targetFromElementsChain(chain) {
87
+ const entries = type.isString(chain) ? parseElementsChain(chain) : [];
88
+ const blockIds = entries.map(blockIdOf).filter((blockId)=>blockId !== null);
89
+ const blockIndex = entries.findIndex((entry)=>blockIdOf(entry) !== null);
90
+ const inner = blockIndex === -1 ? entries : entries.slice(0, blockIndex);
91
+ const scope = blockIndex === -1 ? entries : entries.slice(0, blockIndex + 1);
92
+ const rowEntry = inner.find((entry)=>`attr__${rowAttribute}` in entry.attributes);
93
+ const cellEntry = inner.find((entry)=>`attr__${cellAttribute}` in entry.attributes);
94
+ return {
95
+ block_id: blockIds[0] ?? null,
96
+ row: rowEntry ? parseRowIndex(rowEntry.attributes[`attr__${rowAttribute}`]) : null,
97
+ column: cellEntry ? cellEntry.attributes[`attr__${cellAttribute}`] : null,
98
+ text: textOf(scope),
99
+ option: inner.some(isOption),
100
+ block_ids: blockIds
101
+ };
102
+ }
103
+ export default targetFromElementsChain;
@@ -0,0 +1,23 @@
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 isTraceId from './isTraceId.js';
16
+ // The date directory a trace id's file lives in: `yyyy-mm-dd`.
17
+ function traceIdDate(id) {
18
+ if (!isTraceId(id)) {
19
+ throw new Error(`Invalid trace id. Received ${JSON.stringify(id)}.`);
20
+ }
21
+ return `${id.slice(0, 4)}-${id.slice(4, 6)}-${id.slice(6, 8)}`;
22
+ }
23
+ export default traceIdDate;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lowdefy/helpers",
3
- "version": "6.0.0",
3
+ "version": "7.0.0",
4
4
  "license": "Apache-2.0",
5
5
  "description": "",
6
6
  "homepage": "https://lowdefy.com",
@@ -31,7 +31,8 @@
31
31
  "dist/*"
32
32
  ],
33
33
  "dependencies": {
34
- "@lowdefy/errors": "6.0.0",
34
+ "@lowdefy/errors": "7.0.0",
35
+ "change-case": "5.4.0",
35
36
  "intl-messageformat": "11.2.7"
36
37
  },
37
38
  "devDependencies": {
@@ -40,7 +41,8 @@
40
41
  "@swc/core": "1.15.32",
41
42
  "@swc/jest": "0.2.39",
42
43
  "jest": "28.1.3",
43
- "jest-diff": "28.1.3"
44
+ "jest-diff": "28.1.3",
45
+ "jest-environment-jsdom": "28.1.3"
44
46
  },
45
47
  "publishConfig": {
46
48
  "access": "public"