@byline/core 4.3.0 → 4.4.1

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,339 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+ import { describe, expect, it } from 'vitest';
9
+ import { validateAdminConfigs } from '../config/validate-admin-configs.js';
10
+ import { formatDeclarationPath, formatInstancePath, parseDeclarationPath, parseInstancePath, resolveDeclarationPath, toDeclarationSegments, walkFieldDeclarations, } from './index.js';
11
+ // The same structural shape the characterization suite uses: array→group
12
+ // nesting, a blocks field, an array inside a block, and two block types that
13
+ // each declare `alt` — the collision an unqualified path cannot resolve.
14
+ const fields = [
15
+ { name: 'title', label: 'Title', type: 'text' },
16
+ {
17
+ name: 'files',
18
+ label: 'Files',
19
+ type: 'array',
20
+ fields: [
21
+ {
22
+ name: 'filesGroup',
23
+ type: 'group',
24
+ fields: [{ name: 'caption', label: 'Caption', type: 'text' }],
25
+ },
26
+ ],
27
+ },
28
+ {
29
+ name: 'content',
30
+ label: 'Content',
31
+ type: 'blocks',
32
+ blocks: [
33
+ {
34
+ blockType: 'photoBlock',
35
+ fields: [
36
+ {
37
+ name: 'gallery',
38
+ label: 'Gallery',
39
+ type: 'array',
40
+ fields: [{ name: 'alt', label: 'Alt', type: 'text' }],
41
+ },
42
+ ],
43
+ },
44
+ { blockType: 'videoBlock', fields: [{ name: 'alt', label: 'Alt', type: 'text' }] },
45
+ ],
46
+ },
47
+ ];
48
+ describe('parseDeclarationPath', () => {
49
+ it('splits a dotted path into field segments', () => {
50
+ expect(parseDeclarationPath('files.filesGroup.caption')).toEqual({
51
+ ok: true,
52
+ segments: [
53
+ { kind: 'field', name: 'files' },
54
+ { kind: 'field', name: 'filesGroup' },
55
+ { kind: 'field', name: 'caption' },
56
+ ],
57
+ });
58
+ });
59
+ it('cannot classify a block type on its own — that needs the schema', () => {
60
+ // `photoBlock` comes back as a field segment. Only resolution against a
61
+ // field set can tell it apart from a field of the same name.
62
+ const parsed = parseDeclarationPath('content.photoBlock.gallery.alt');
63
+ expect(parsed.ok && parsed.segments.every((s) => s.kind === 'field')).toBe(true);
64
+ });
65
+ it('rejects item indices rather than silently dropping them', () => {
66
+ expect(parseDeclarationPath('files[0].filesGroup.caption')).toEqual({
67
+ ok: false,
68
+ reason: 'index',
69
+ });
70
+ });
71
+ it('distinguishes a stray bracket from a wrong-dialect index', () => {
72
+ // `index` steers the author from instance notation to declaration
73
+ // notation; that advice is misleading for what is only a typo.
74
+ expect(parseDeclarationPath('files].caption')).toEqual({ ok: false, reason: 'malformed' });
75
+ });
76
+ it('rejects empty paths and empty segments', () => {
77
+ expect(parseDeclarationPath('')).toEqual({ ok: false, reason: 'empty' });
78
+ expect(parseDeclarationPath(' ')).toEqual({ ok: false, reason: 'empty' });
79
+ expect(parseDeclarationPath('a..b')).toEqual({ ok: false, reason: 'emptySegment' });
80
+ expect(parseDeclarationPath('.a')).toEqual({ ok: false, reason: 'emptySegment' });
81
+ expect(parseDeclarationPath('a.')).toEqual({ ok: false, reason: 'emptySegment' });
82
+ });
83
+ });
84
+ describe('parseInstancePath', () => {
85
+ it('reads positional item selectors', () => {
86
+ expect(parseInstancePath('content[0].gallery[1].alt')).toEqual({
87
+ ok: true,
88
+ segments: [
89
+ { kind: 'field', name: 'content' },
90
+ { kind: 'index', index: 0 },
91
+ { kind: 'field', name: 'gallery' },
92
+ { kind: 'index', index: 1 },
93
+ { kind: 'field', name: 'alt' },
94
+ ],
95
+ });
96
+ });
97
+ it('reads stable-id item selectors', () => {
98
+ expect(parseInstancePath('content[id=abc].alt')).toEqual({
99
+ ok: true,
100
+ segments: [
101
+ { kind: 'field', name: 'content' },
102
+ { kind: 'id', id: 'abc' },
103
+ { kind: 'field', name: 'alt' },
104
+ ],
105
+ });
106
+ });
107
+ it('rejects malformed bracket syntax instead of truncating', () => {
108
+ for (const bad of ['a[', 'a[]', 'a[x]', 'a]b']) {
109
+ expect(parseInstancePath(bad).ok).toBe(false);
110
+ }
111
+ });
112
+ it('rejects an id containing a closing bracket rather than truncating it', () => {
113
+ // Ids are UUIDv7 today, so this is unreachable in practice. Pinned because
114
+ // the id token is scanned to the first `]`: the guarantee worth holding is
115
+ // that an id which somehow contains one is rejected outright, never
116
+ // silently shortened into a different — and possibly valid — id.
117
+ expect(parseInstancePath('gallery[id=a]b].alt').ok).toBe(false);
118
+ });
119
+ });
120
+ describe('formatting', () => {
121
+ it('round-trips a declaration path', () => {
122
+ const parsed = parseDeclarationPath('files.filesGroup.caption');
123
+ expect(parsed.ok && formatDeclarationPath(parsed.segments)).toBe('files.filesGroup.caption');
124
+ });
125
+ it('round-trips an instance path', () => {
126
+ const parsed = parseInstancePath('content[0].gallery[1].alt');
127
+ expect(parsed.ok && formatInstancePath(parsed.segments)).toBe('content[0].gallery[1].alt');
128
+ });
129
+ it('renders a block type in declaration form and omits it in instance form', () => {
130
+ const segments = [
131
+ { kind: 'field', name: 'content' },
132
+ { kind: 'blockType', blockType: 'photoBlock' },
133
+ { kind: 'field', name: 'alt' },
134
+ ];
135
+ expect(formatDeclarationPath(segments)).toBe('content.photoBlock.alt');
136
+ expect(formatInstancePath(segments)).toBe('content.alt');
137
+ });
138
+ });
139
+ describe('toDeclarationSegments', () => {
140
+ it('drops item selectors and keeps the block type', () => {
141
+ const parsed = parseInstancePath('content[0].gallery[1].alt');
142
+ expect(parsed.ok && formatDeclarationPath(toDeclarationSegments(parsed.segments))).toBe('content.gallery.alt');
143
+ });
144
+ it('preserves a field whose name is numeric', () => {
145
+ // The regex-over-text approach each call site hand-rolls today deletes
146
+ // this segment. Working over typed segments cannot: `0` here is a field.
147
+ const parsed = parseInstancePath('weird.0.value');
148
+ expect(parsed.ok && formatDeclarationPath(toDeclarationSegments(parsed.segments))).toBe('weird.0.value');
149
+ });
150
+ });
151
+ describe('walkFieldDeclarations', () => {
152
+ const collect = () => {
153
+ const out = [];
154
+ walkFieldDeclarations(fields, (_field, segments) => {
155
+ out.push(formatDeclarationPath(segments));
156
+ });
157
+ return out;
158
+ };
159
+ it('qualifies paths through a blocks field with the block type', () => {
160
+ expect(collect()).toEqual(expect.arrayContaining([
161
+ 'title',
162
+ 'files.filesGroup.caption',
163
+ 'content.photoBlock.gallery.alt',
164
+ 'content.videoBlock.alt',
165
+ ]));
166
+ });
167
+ it('keeps same-named fields in different blocks distinct', () => {
168
+ // The defect this walk exists to prevent: without the block type both of
169
+ // these collapse to `content.alt` and become unidentifiable.
170
+ const alts = collect().filter((p) => p.endsWith('.alt'));
171
+ expect(new Set(alts).size).toBe(alts.length);
172
+ });
173
+ it('visits structure fields themselves, not only leaves', () => {
174
+ expect(collect()).toEqual(expect.arrayContaining(['files', 'files.filesGroup', 'content']));
175
+ });
176
+ it('reports every block through onBlock, addressed by its own path', () => {
177
+ const blocks = [];
178
+ walkFieldDeclarations(fields, () => { }, {
179
+ onBlock: (block, segments) => {
180
+ blocks.push(`${formatDeclarationPath(segments)} (${block.blockType})`);
181
+ },
182
+ });
183
+ expect(blocks).toEqual(['content.photoBlock (photoBlock)', 'content.videoBlock (videoBlock)']);
184
+ });
185
+ it('reports blocks nested inside other blocks', () => {
186
+ // `validateBlockAdminConfigs` collects every declaration site of a block
187
+ // type, and a block may be declared inside another block. The walk has to
188
+ // recurse through block fields, not just collection fields.
189
+ const nested = [
190
+ {
191
+ name: 'content',
192
+ label: 'C',
193
+ type: 'blocks',
194
+ blocks: [
195
+ {
196
+ blockType: 'outerBlock',
197
+ fields: [
198
+ {
199
+ name: 'inner',
200
+ label: 'I',
201
+ type: 'blocks',
202
+ blocks: [{ blockType: 'deepBlock', fields: [{ name: 'deep', type: 'text' }] }],
203
+ },
204
+ ],
205
+ },
206
+ ],
207
+ },
208
+ ];
209
+ const seen = [];
210
+ walkFieldDeclarations(nested, () => { }, {
211
+ onBlock: (_block, segments) => seen.push(formatDeclarationPath(segments)),
212
+ });
213
+ expect(seen).toEqual(['content.outerBlock', 'content.outerBlock.inner.deepBlock']);
214
+ });
215
+ it('reports a block declaring no fields, which the field visitor cannot see', () => {
216
+ // Validation that must inspect every block — the dot-free check on block
217
+ // types, for one — would silently stop covering empty blocks if blocks
218
+ // were inferred from the segments of the fields inside them.
219
+ const withEmpty = [
220
+ {
221
+ name: 'content',
222
+ label: 'C',
223
+ type: 'blocks',
224
+ blocks: [{ blockType: 'emptyBlock', fields: [] }],
225
+ },
226
+ ];
227
+ const visited = [];
228
+ const blocks = [];
229
+ walkFieldDeclarations(withEmpty, (_field, segments) => visited.push(formatDeclarationPath(segments)), { onBlock: (block) => blocks.push(block.blockType) });
230
+ expect(visited).toEqual(['content']);
231
+ expect(blocks).toEqual(['emptyBlock']);
232
+ });
233
+ it('reports each block before the fields inside it', () => {
234
+ const events = [];
235
+ walkFieldDeclarations(fields, (_field, segments) => events.push(`field:${formatDeclarationPath(segments)}`), {
236
+ onBlock: (_block, segments) => events.push(`block:${formatDeclarationPath(segments)}`),
237
+ });
238
+ expect(events.indexOf('block:content.photoBlock')).toBeLessThan(events.indexOf('field:content.photoBlock.gallery'));
239
+ });
240
+ });
241
+ describe('resolveDeclarationPath', () => {
242
+ it('resolves a path through array and group structure', () => {
243
+ const result = resolveDeclarationPath(fields, 'files.filesGroup.caption');
244
+ expect(result.status).toBe('ok');
245
+ expect(result.status === 'ok' && result.field.name).toBe('caption');
246
+ });
247
+ it('reclassifies a block-type segment the parser could not identify', () => {
248
+ const result = resolveDeclarationPath(fields, 'content.photoBlock.gallery.alt');
249
+ expect(result.status).toBe('ok');
250
+ expect(result.status === 'ok' && result.segments[1]).toEqual({
251
+ kind: 'blockType',
252
+ blockType: 'photoBlock',
253
+ });
254
+ });
255
+ it('distinguishes same-named fields in different blocks', () => {
256
+ const photo = resolveDeclarationPath(fields, 'content.photoBlock.gallery.alt');
257
+ const video = resolveDeclarationPath(fields, 'content.videoBlock.alt');
258
+ expect(photo.status).toBe('ok');
259
+ expect(video.status).toBe('ok');
260
+ });
261
+ it('reports `blocks` rather than `unresolved` when traversal is barred', () => {
262
+ const result = resolveDeclarationPath(fields, 'content.photoBlock.gallery.alt', {
263
+ blocks: 'forbidden',
264
+ });
265
+ expect(result).toEqual({ status: 'blocks', at: 0 });
266
+ });
267
+ it('reports `blocks` even for an unqualified path into a block', () => {
268
+ expect(resolveDeclarationPath(fields, 'content.alt', { blocks: 'forbidden' }).status).toBe('blocks');
269
+ });
270
+ it('resolves the blocks field itself under `forbidden`', () => {
271
+ // `forbidden` bars *traversal* into a block, not addressing the blocks
272
+ // field. A blocks field carries a label like any other, so an admin
273
+ // `fields{}` key naming it is a legitimate override target — what the
274
+ // narrowing rules out is reaching past it to the fields inside.
275
+ const result = resolveDeclarationPath(fields, 'content', { blocks: 'forbidden' });
276
+ expect(result.status).toBe('ok');
277
+ expect(result.status === 'ok' && result.field.type).toBe('blocks');
278
+ });
279
+ it('rejects an unknown block type', () => {
280
+ expect(resolveDeclarationPath(fields, 'content.audioBlock.alt').status).toBe('unresolved');
281
+ });
282
+ it('rejects a path that stops on the block type', () => {
283
+ // Addresses a block, not a field declaration.
284
+ expect(resolveDeclarationPath(fields, 'content.photoBlock').status).toBe('unresolved');
285
+ });
286
+ it('rejects a path walking through a value field', () => {
287
+ expect(resolveDeclarationPath(fields, 'title.anything').status).toBe('unresolved');
288
+ });
289
+ it('rejects instance segments passed where a declaration is required', () => {
290
+ expect(resolveDeclarationPath(fields, 'files[0].filesGroup.caption').status).toBe('unresolved');
291
+ });
292
+ it('resolves a structure field addressed on its own', () => {
293
+ expect(resolveDeclarationPath(fields, 'files').status).toBe('ok');
294
+ });
295
+ });
296
+ // ---------------------------------------------------------------------------
297
+ // Equivalence with the validator this module will replace.
298
+ //
299
+ // Phase 2c swaps `validate-admin-configs`' private `resolveSchemaPath` for
300
+ // `resolveDeclarationPath({ blocks: 'forbidden' })`. That is only safe if the
301
+ // two agree on every key today, so assert it directly rather than assuming.
302
+ // ---------------------------------------------------------------------------
303
+ describe('resolveDeclarationPath — agreement with the admin fields{} validator', () => {
304
+ const collection = {
305
+ path: 'pages',
306
+ labels: { singular: 'Page', plural: 'Pages' },
307
+ useAsTitle: 'title',
308
+ fields,
309
+ };
310
+ const validatorAccepts = (key) => {
311
+ try {
312
+ validateAdminConfigs([{ slug: 'pages', fields: { [key]: {} } }], [collection]);
313
+ return true;
314
+ }
315
+ catch {
316
+ return false;
317
+ }
318
+ };
319
+ const resolverAccepts = (key) => resolveDeclarationPath(fields, key, { blocks: 'forbidden' }).status === 'ok';
320
+ const keys = [
321
+ 'title',
322
+ 'files',
323
+ 'files.filesGroup',
324
+ 'files.filesGroup.caption',
325
+ 'files.filesGroup.missing',
326
+ 'files[0].filesGroup.caption',
327
+ 'title.anything',
328
+ 'content',
329
+ 'content.alt',
330
+ 'content.photoBlock.gallery.alt',
331
+ 'missing',
332
+ '',
333
+ ];
334
+ for (const key of keys) {
335
+ it(`agrees on ${key === '' ? '<empty>' : key}`, () => {
336
+ expect(resolverAccepts(key)).toBe(validatorAccepts(key));
337
+ });
338
+ }
339
+ });
@@ -0,0 +1,47 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+ import type { Block, Field } from '../@types/index.js';
9
+ import type { PathResolution, PathSegment, ResolveOptions } from './path-types.js';
10
+ export interface WalkOptions {
11
+ /**
12
+ * Called for every block declaration encountered, immediately before its
13
+ * fields are visited, with the segments addressing the block itself
14
+ * (`content.photoBlock`).
15
+ *
16
+ * Blocks are declaration sites in their own right, and a block with no
17
+ * fields is invisible to `visit` — so validation that must see every block
18
+ * regardless of its contents needs this rather than inferring blocks from
19
+ * the segments of the fields inside them.
20
+ */
21
+ readonly onBlock?: (block: Block, segments: readonly PathSegment[]) => void;
22
+ }
23
+ /**
24
+ * Visit every field declaration in a field set, depth first, handing each one
25
+ * the segments that address it.
26
+ *
27
+ * Structure fields are visited themselves and then descended into. A `blocks`
28
+ * field contributes two segments per hop — its own name and the block type —
29
+ * which is what keeps two blocks declaring the same field name apart.
30
+ *
31
+ * This is the single canonical walk. Producers of declaration paths (the
32
+ * upload hook registry, boot-validation error messages) should use it rather
33
+ * than re-implementing the descent, which is how they drifted apart.
34
+ */
35
+ export declare function walkFieldDeclarations(fields: readonly Field[], visit: (field: Field, segments: readonly PathSegment[]) => void, options?: WalkOptions): void;
36
+ /**
37
+ * Resolve a declaration path against a field set.
38
+ *
39
+ * Accepts either a path string or pre-parsed segments. Segments arriving as
40
+ * `kind: 'field'` where the schema expects a block type are reclassified in
41
+ * the returned `segments`, so callers get a correctly typed path back even
42
+ * though the parser could not have known.
43
+ *
44
+ * Item selectors are rejected: a declaration path addresses a declaration, so
45
+ * an index is a category error rather than something to ignore.
46
+ */
47
+ export declare function resolveDeclarationPath(fields: readonly Field[], path: string | readonly PathSegment[], options?: ResolveOptions): PathResolution;
@@ -0,0 +1,120 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+ import { parseDeclarationPath } from './parse-path.js';
9
+ /**
10
+ * Visit every field declaration in a field set, depth first, handing each one
11
+ * the segments that address it.
12
+ *
13
+ * Structure fields are visited themselves and then descended into. A `blocks`
14
+ * field contributes two segments per hop — its own name and the block type —
15
+ * which is what keeps two blocks declaring the same field name apart.
16
+ *
17
+ * This is the single canonical walk. Producers of declaration paths (the
18
+ * upload hook registry, boot-validation error messages) should use it rather
19
+ * than re-implementing the descent, which is how they drifted apart.
20
+ */
21
+ export function walkFieldDeclarations(fields, visit, options = {}) {
22
+ const walk = (current, prefix) => {
23
+ for (const field of current) {
24
+ const segments = [...prefix, { kind: 'field', name: field.name }];
25
+ visit(field, segments);
26
+ if (field.type === 'group' || field.type === 'array') {
27
+ walk(field.fields, segments);
28
+ }
29
+ else if (field.type === 'blocks') {
30
+ for (const block of field.blocks) {
31
+ const blockSegments = [
32
+ ...segments,
33
+ { kind: 'blockType', blockType: block.blockType },
34
+ ];
35
+ options.onBlock?.(block, blockSegments);
36
+ walk(block.fields, blockSegments);
37
+ }
38
+ }
39
+ }
40
+ };
41
+ walk(fields, []);
42
+ }
43
+ /**
44
+ * Resolve a declaration path against a field set.
45
+ *
46
+ * Accepts either a path string or pre-parsed segments. Segments arriving as
47
+ * `kind: 'field'` where the schema expects a block type are reclassified in
48
+ * the returned `segments`, so callers get a correctly typed path back even
49
+ * though the parser could not have known.
50
+ *
51
+ * Item selectors are rejected: a declaration path addresses a declaration, so
52
+ * an index is a category error rather than something to ignore.
53
+ */
54
+ export function resolveDeclarationPath(fields, path, options = {}) {
55
+ const allowBlocks = (options.blocks ?? 'qualified') === 'qualified';
56
+ let input;
57
+ if (typeof path === 'string') {
58
+ const parsed = parseDeclarationPath(path);
59
+ if (!parsed.ok)
60
+ return { status: 'unresolved', at: 0 };
61
+ input = parsed.segments;
62
+ }
63
+ else {
64
+ input = path;
65
+ }
66
+ if (input.length === 0)
67
+ return { status: 'unresolved', at: 0 };
68
+ const resolved = [];
69
+ let current = fields;
70
+ let i = 0;
71
+ while (i < input.length) {
72
+ const segment = input[i];
73
+ // Declaration paths carry names only. Anything else is a caller passing
74
+ // instance segments where a declaration was required.
75
+ if (segment == null || (segment.kind !== 'field' && segment.kind !== 'blockType')) {
76
+ return { status: 'unresolved', at: i };
77
+ }
78
+ const name = segment.kind === 'field' ? segment.name : segment.blockType;
79
+ const field = current.find((candidate) => candidate.name === name);
80
+ if (field == null)
81
+ return { status: 'unresolved', at: i };
82
+ resolved.push({ kind: 'field', name: field.name });
83
+ // Deliberately ahead of the `blocks: 'forbidden'` check below: that policy
84
+ // bars *traversal* into a block, so a path ending on the blocks field
85
+ // resolves normally. The field carries a label like any other and is a
86
+ // legitimate admin override target.
87
+ const isLast = i === input.length - 1;
88
+ if (isLast)
89
+ return { status: 'ok', field, segments: resolved };
90
+ if (field.type === 'group' || field.type === 'array') {
91
+ current = field.fields;
92
+ i += 1;
93
+ continue;
94
+ }
95
+ if (field.type === 'blocks') {
96
+ if (!allowBlocks)
97
+ return { status: 'blocks', at: i };
98
+ // The next segment names the block type — the discriminator that makes
99
+ // the rest of the path unambiguous.
100
+ const next = input[i + 1];
101
+ if (next == null || (next.kind !== 'field' && next.kind !== 'blockType')) {
102
+ return { status: 'unresolved', at: i + 1 };
103
+ }
104
+ const blockType = next.kind === 'field' ? next.name : next.blockType;
105
+ const block = field.blocks.find((candidate) => candidate.blockType === blockType);
106
+ if (block == null)
107
+ return { status: 'unresolved', at: i + 1 };
108
+ resolved.push({ kind: 'blockType', blockType: block.blockType });
109
+ // A path ending on the block type addresses the block, not a field.
110
+ if (i + 1 === input.length - 1)
111
+ return { status: 'unresolved', at: i + 1 };
112
+ current = block.fields;
113
+ i += 2;
114
+ continue;
115
+ }
116
+ // A value field with path left to walk.
117
+ return { status: 'unresolved', at: i };
118
+ }
119
+ return { status: 'unresolved', at: input.length - 1 };
120
+ }
@@ -46,9 +46,15 @@ function* walkCounterSites(fields, data, pathPrefix = '') {
46
46
  }
47
47
  }
48
48
  /**
49
- * Resolve a counter value already present in `previousData` at the
50
- * same dotted path as the site. Returns `undefined` if the value is
51
- * missing or not a finite number.
49
+ * Resolve a counter value already present in `previousData` at the same
50
+ * dotted path as the site. Returns `undefined` if the value is missing or
51
+ * not a finite number.
52
+ *
53
+ * `path` is a plain object accessor, **not** a field path in the sense of
54
+ * `@byline/core` `paths/` — it walks data, never the schema, and stops at
55
+ * any array because counters may not be declared inside `array` / `blocks`
56
+ * (see `walkCounterSites`). It therefore needs no item selectors and no
57
+ * block-type segments, which is why it does not use the shared grammar.
52
58
  */
53
59
  function readPreviousValue(previousData, path) {
54
60
  const segments = path.split('.');
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@byline/core",
3
3
  "private": false,
4
4
  "license": "MPL-2.0",
5
- "version": "4.3.0",
5
+ "version": "4.4.1",
6
6
  "engines": {
7
7
  "node": ">=20.9.0"
8
8
  },
@@ -81,7 +81,7 @@
81
81
  "pino": "^10.3.1",
82
82
  "sharp": "^0.35.3",
83
83
  "zod": "^4.4.3",
84
- "@byline/auth": "4.3.0"
84
+ "@byline/auth": "4.4.1"
85
85
  },
86
86
  "devDependencies": {
87
87
  "@biomejs/biome": "2.5.4",