@byline/core 6.6.0 → 6.7.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.
- package/dist/@types/db-types.d.ts +60 -0
- package/dist/auth/assert-locale-visibility.d.ts +27 -0
- package/dist/auth/assert-locale-visibility.js +43 -0
- package/dist/auth/assert-locale-visibility.test.node.js +46 -0
- package/dist/auth/index.d.ts +1 -0
- package/dist/auth/index.js +1 -0
- package/dist/config/resolve-item-view-columns.test.node.js +30 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/services/document-lifecycle/internals.js +3 -0
- package/dist/services/document-lifecycle.test.node.js +135 -0
- package/dist/services/populate.d.ts +9 -1
- package/dist/services/populate.js +4 -0
- package/dist/services/richtext-populate.d.ts +7 -1
- package/dist/services/richtext-populate.js +7 -4
- package/dist/storage/index.d.ts +1 -0
- package/dist/storage/index.js +1 -0
- package/dist/storage/locale-resolution.d.ts +76 -0
- package/dist/storage/locale-resolution.js +122 -0
- package/dist/storage/locale-resolution.test.node.d.ts +8 -0
- package/dist/storage/locale-resolution.test.node.js +245 -0
- package/package.json +2 -2
- package/dist/schemas/zod/review-comparison-probe.test.node.d.ts +0 -1
- package/dist/schemas/zod/review-comparison-probe.test.node.js +0 -26
- package/dist/services/restore-review-probe.test.node.js +0 -3397
- package/dist/services/review-probes.test.node.js +0 -349
- /package/dist/{services/restore-review-probe.test.node.d.ts → auth/assert-locale-visibility.test.node.d.ts} +0 -0
- /package/dist/{services/review-probes.test.node.d.ts → config/resolve-item-view-columns.test.node.d.ts} +0 -0
|
@@ -0,0 +1,76 @@
|
|
|
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 { LocaleVisibility, MissingLocalePolicy, ReadMode } from '../@types/index.js';
|
|
9
|
+
import type { FlattenedFieldValue } from './storage-row-types.js';
|
|
10
|
+
/**
|
|
11
|
+
* One version's entry in the `byline_document_version_locales` ledger: the
|
|
12
|
+
* concrete locales its content is complete in, or `localeAgnostic` when the
|
|
13
|
+
* version carries only the `'all'` sentinel (no localized content).
|
|
14
|
+
*/
|
|
15
|
+
export interface VersionLocaleLedger {
|
|
16
|
+
availableLocales: readonly string[];
|
|
17
|
+
localeAgnostic: boolean;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Derive a completeness ledger from loaded store rows, using the same rule the
|
|
21
|
+
* write path records: a locale is complete when it covers every localized field
|
|
22
|
+
* path the source locale has; a version with no localized rows is
|
|
23
|
+
* locale-agnostic. Used only for versions written before the ledger existed and
|
|
24
|
+
* not yet backfilled. It can only see the rows a read loaded, so a projected
|
|
25
|
+
* read of such a version may still misjudge completeness.
|
|
26
|
+
*/
|
|
27
|
+
export declare function deriveVersionLocaleLedger(rows: readonly FlattenedFieldValue[], sourceLocale: string): VersionLocaleLedger;
|
|
28
|
+
/**
|
|
29
|
+
* The per-document facts a locale-eligibility decision needs. `ledger` is the
|
|
30
|
+
* selected version's completeness ledger; `availableLocales` is the document's
|
|
31
|
+
* saved editorial checkbox set; `advertiseLocales` is the collection's opt-in
|
|
32
|
+
* to that control.
|
|
33
|
+
*/
|
|
34
|
+
export interface LocaleEligibility {
|
|
35
|
+
visibility: LocaleVisibility;
|
|
36
|
+
sourceLocale: string;
|
|
37
|
+
ledger: VersionLocaleLedger;
|
|
38
|
+
advertiseLocales: boolean;
|
|
39
|
+
availableLocales: readonly string[];
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* The locale visibility a read uses: an explicit `localeVisibility` wins;
|
|
43
|
+
* otherwise `status: 'any'` implies `'editorial'` and an omitted or
|
|
44
|
+
* `'published'` status implies `'public'`. Resolving visibility never grants
|
|
45
|
+
* access; callers still authorize `'editorial'` separately.
|
|
46
|
+
*/
|
|
47
|
+
export declare function resolveLocaleVisibility(status: ReadMode | undefined, explicit?: LocaleVisibility): LocaleVisibility;
|
|
48
|
+
/** The outcome of one read's language decision for one document version. */
|
|
49
|
+
export interface ReadLocaleDecision {
|
|
50
|
+
/** The locale to restore fields in; `undefined` for a multi-locale (`'all'`) read. */
|
|
51
|
+
restoreLocale: string | undefined;
|
|
52
|
+
/** The `resolvedLocale` read metadata: the locale the fields were selected in, or `null`. */
|
|
53
|
+
resolvedLocale: string | null;
|
|
54
|
+
/** `true` when localized values must be withheld and only non-localized values returned. */
|
|
55
|
+
withholdLocalized: boolean;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Decide, for one document version, which locale a read restores, what it
|
|
59
|
+
* reports as `resolvedLocale`, and whether localized values must be withheld.
|
|
60
|
+
* Shared by every storage adapter so the language decision is identical across
|
|
61
|
+
* engines and is made from the whole version, before any field projection.
|
|
62
|
+
*
|
|
63
|
+
* - `locale: 'all'` restores locale maps and reports `null`.
|
|
64
|
+
* - A locale-agnostic version restores the requested locale and reports `null`.
|
|
65
|
+
* - `'fallback'` restores and reports the first eligible chain entry.
|
|
66
|
+
* - `'omit'`, `'empty'` and an omitted policy are exact: an eligible locale is
|
|
67
|
+
* restored and reported. Under `'public'` visibility an ineligible locale
|
|
68
|
+
* withholds localized values and reports `null`; under `'editorial'` the
|
|
69
|
+
* exact locale is restored even when partial.
|
|
70
|
+
*/
|
|
71
|
+
export declare function resolveReadLocale(params: {
|
|
72
|
+
locale: string;
|
|
73
|
+
chain: readonly string[];
|
|
74
|
+
onMissingLocale: MissingLocalePolicy | undefined;
|
|
75
|
+
eligibility: LocaleEligibility;
|
|
76
|
+
}): ReadLocaleDecision;
|
|
@@ -0,0 +1,122 @@
|
|
|
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
|
+
/**
|
|
9
|
+
* Derive a completeness ledger from loaded store rows, using the same rule the
|
|
10
|
+
* write path records: a locale is complete when it covers every localized field
|
|
11
|
+
* path the source locale has; a version with no localized rows is
|
|
12
|
+
* locale-agnostic. Used only for versions written before the ledger existed and
|
|
13
|
+
* not yet backfilled. It can only see the rows a read loaded, so a projected
|
|
14
|
+
* read of such a version may still misjudge completeness.
|
|
15
|
+
*/
|
|
16
|
+
export function deriveVersionLocaleLedger(rows, sourceLocale) {
|
|
17
|
+
const pathsByLocale = new Map();
|
|
18
|
+
for (const row of rows) {
|
|
19
|
+
if (row.locale === 'all' || row.field_type === 'meta')
|
|
20
|
+
continue;
|
|
21
|
+
let paths = pathsByLocale.get(row.locale);
|
|
22
|
+
if (paths == null) {
|
|
23
|
+
paths = new Set();
|
|
24
|
+
pathsByLocale.set(row.locale, paths);
|
|
25
|
+
}
|
|
26
|
+
paths.add(row.field_path.join('.'));
|
|
27
|
+
}
|
|
28
|
+
if (pathsByLocale.size === 0)
|
|
29
|
+
return { availableLocales: [], localeAgnostic: true };
|
|
30
|
+
const canonical = pathsByLocale.get(sourceLocale) ?? new Set();
|
|
31
|
+
const availableLocales = [];
|
|
32
|
+
for (const [locale, paths] of pathsByLocale) {
|
|
33
|
+
let covers = true;
|
|
34
|
+
for (const path of canonical) {
|
|
35
|
+
if (!paths.has(path)) {
|
|
36
|
+
covers = false;
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (covers)
|
|
41
|
+
availableLocales.push(locale);
|
|
42
|
+
}
|
|
43
|
+
return { availableLocales, localeAgnostic: false };
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Whether `locale` may be delivered for one document version.
|
|
47
|
+
*
|
|
48
|
+
* - A locale-agnostic version renders identically everywhere: always eligible.
|
|
49
|
+
* - The source locale is always eligible, whatever its checkbox says.
|
|
50
|
+
* - Any other locale must be complete on the selected version.
|
|
51
|
+
* - Under `'public'` visibility in a collection with `advertiseLocales`, it
|
|
52
|
+
* must also be checked in the document's editorial set. An empty set
|
|
53
|
+
* authorizes no additional translation.
|
|
54
|
+
*/
|
|
55
|
+
function isLocaleEligible(locale, eligibility) {
|
|
56
|
+
const { visibility, sourceLocale, ledger, advertiseLocales, availableLocales } = eligibility;
|
|
57
|
+
if (ledger.localeAgnostic)
|
|
58
|
+
return true;
|
|
59
|
+
if (locale === sourceLocale)
|
|
60
|
+
return true;
|
|
61
|
+
if (!ledger.availableLocales.includes(locale))
|
|
62
|
+
return false;
|
|
63
|
+
if (visibility === 'editorial' || !advertiseLocales)
|
|
64
|
+
return true;
|
|
65
|
+
return availableLocales.includes(locale);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Walk `chain` (`[requested, …, floor]`) and return the first locale eligible
|
|
69
|
+
* under `eligibility`. Every candidate must pass, so a withheld intermediate
|
|
70
|
+
* locale can never be selected. The floor is terminal and returned when no
|
|
71
|
+
* earlier candidate qualifies.
|
|
72
|
+
*/
|
|
73
|
+
function resolveEligibleLocale(chain, eligibility) {
|
|
74
|
+
for (const candidate of chain) {
|
|
75
|
+
if (isLocaleEligible(candidate, eligibility))
|
|
76
|
+
return candidate;
|
|
77
|
+
}
|
|
78
|
+
// biome-ignore lint/style/noNonNullAssertion: a locale chain always has a floor
|
|
79
|
+
return chain[chain.length - 1];
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* The locale visibility a read uses: an explicit `localeVisibility` wins;
|
|
83
|
+
* otherwise `status: 'any'` implies `'editorial'` and an omitted or
|
|
84
|
+
* `'published'` status implies `'public'`. Resolving visibility never grants
|
|
85
|
+
* access; callers still authorize `'editorial'` separately.
|
|
86
|
+
*/
|
|
87
|
+
export function resolveLocaleVisibility(status, explicit) {
|
|
88
|
+
if (explicit != null)
|
|
89
|
+
return explicit;
|
|
90
|
+
return status === 'any' ? 'editorial' : 'public';
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Decide, for one document version, which locale a read restores, what it
|
|
94
|
+
* reports as `resolvedLocale`, and whether localized values must be withheld.
|
|
95
|
+
* Shared by every storage adapter so the language decision is identical across
|
|
96
|
+
* engines and is made from the whole version, before any field projection.
|
|
97
|
+
*
|
|
98
|
+
* - `locale: 'all'` restores locale maps and reports `null`.
|
|
99
|
+
* - A locale-agnostic version restores the requested locale and reports `null`.
|
|
100
|
+
* - `'fallback'` restores and reports the first eligible chain entry.
|
|
101
|
+
* - `'omit'`, `'empty'` and an omitted policy are exact: an eligible locale is
|
|
102
|
+
* restored and reported. Under `'public'` visibility an ineligible locale
|
|
103
|
+
* withholds localized values and reports `null`; under `'editorial'` the
|
|
104
|
+
* exact locale is restored even when partial.
|
|
105
|
+
*/
|
|
106
|
+
export function resolveReadLocale(params) {
|
|
107
|
+
const { locale, chain, onMissingLocale, eligibility } = params;
|
|
108
|
+
if (locale === 'all') {
|
|
109
|
+
return { restoreLocale: undefined, resolvedLocale: null, withholdLocalized: false };
|
|
110
|
+
}
|
|
111
|
+
if (eligibility.ledger.localeAgnostic) {
|
|
112
|
+
return { restoreLocale: locale, resolvedLocale: null, withholdLocalized: false };
|
|
113
|
+
}
|
|
114
|
+
if (onMissingLocale === 'fallback') {
|
|
115
|
+
const selected = resolveEligibleLocale(chain, eligibility);
|
|
116
|
+
return { restoreLocale: selected, resolvedLocale: selected, withholdLocalized: false };
|
|
117
|
+
}
|
|
118
|
+
if (eligibility.visibility === 'public' && !isLocaleEligible(locale, eligibility)) {
|
|
119
|
+
return { restoreLocale: locale, resolvedLocale: null, withholdLocalized: true };
|
|
120
|
+
}
|
|
121
|
+
return { restoreLocale: locale, resolvedLocale: locale, withholdLocalized: false };
|
|
122
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
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
|
+
export {};
|
|
@@ -0,0 +1,245 @@
|
|
|
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 { deriveVersionLocaleLedger, resolveLocaleVisibility, resolveReadLocale, } from './locale-resolution.js';
|
|
10
|
+
// The eligibility rule and the fallback walk are private to the module; these
|
|
11
|
+
// helpers observe them through `resolveReadLocale`, the only adapter-facing
|
|
12
|
+
// entry point. A locale is eligible exactly when a fallback read toward the
|
|
13
|
+
// source selects it, and the walk's result is the locale that read restores.
|
|
14
|
+
function isLocaleEligible(locale, eligibility) {
|
|
15
|
+
const chain = locale === eligibility.sourceLocale ? [locale] : [locale, eligibility.sourceLocale];
|
|
16
|
+
return (resolveReadLocale({ locale, chain, onMissingLocale: 'fallback', eligibility }).restoreLocale ===
|
|
17
|
+
locale);
|
|
18
|
+
}
|
|
19
|
+
function resolveEligibleLocale(chain, eligibility) {
|
|
20
|
+
const decision = resolveReadLocale({
|
|
21
|
+
locale: chain[0],
|
|
22
|
+
chain,
|
|
23
|
+
onMissingLocale: 'fallback',
|
|
24
|
+
eligibility,
|
|
25
|
+
});
|
|
26
|
+
return decision.restoreLocale;
|
|
27
|
+
}
|
|
28
|
+
describe('deriveVersionLocaleLedger', () => {
|
|
29
|
+
const row = (locale, path, field_type = 'text') => ({ locale, field_path: path.split('.'), field_type });
|
|
30
|
+
it('records the locales that cover every source-locale localized path', () => {
|
|
31
|
+
const ledger = deriveVersionLocaleLedger([
|
|
32
|
+
row('en', 'title'),
|
|
33
|
+
row('en', 'body'),
|
|
34
|
+
row('de', 'title'),
|
|
35
|
+
row('de', 'body'),
|
|
36
|
+
row('fr', 'title'),
|
|
37
|
+
row('all', 'sku'),
|
|
38
|
+
row('all', 'content.0', 'meta'),
|
|
39
|
+
], 'en');
|
|
40
|
+
expect(ledger).toEqual({ availableLocales: ['en', 'de'], localeAgnostic: false });
|
|
41
|
+
});
|
|
42
|
+
it('marks a version with no localized rows as locale-agnostic', () => {
|
|
43
|
+
expect(deriveVersionLocaleLedger([row('all', 'sku')], 'en')).toEqual({
|
|
44
|
+
availableLocales: [],
|
|
45
|
+
localeAgnostic: true,
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
describe('isLocaleEligible', () => {
|
|
50
|
+
const base = {
|
|
51
|
+
visibility: 'public',
|
|
52
|
+
sourceLocale: 'en',
|
|
53
|
+
ledger: { availableLocales: ['en', 'es', 'de'], localeAgnostic: false },
|
|
54
|
+
advertiseLocales: true,
|
|
55
|
+
availableLocales: ['de'],
|
|
56
|
+
};
|
|
57
|
+
it('public: a complete and checked translation is eligible', () => {
|
|
58
|
+
expect(isLocaleEligible('de', base)).toBe(true);
|
|
59
|
+
});
|
|
60
|
+
it('public: a complete but unchecked translation is not eligible', () => {
|
|
61
|
+
expect(isLocaleEligible('es', base)).toBe(false);
|
|
62
|
+
});
|
|
63
|
+
it('public: a checked but incomplete translation is not eligible', () => {
|
|
64
|
+
expect(isLocaleEligible('fr', { ...base, availableLocales: ['fr'] })).toBe(false);
|
|
65
|
+
});
|
|
66
|
+
it('public: the source is eligible even when its checkbox is off', () => {
|
|
67
|
+
expect(isLocaleEligible('en', { ...base, availableLocales: [] })).toBe(true);
|
|
68
|
+
});
|
|
69
|
+
it('public: an empty advertised set authorizes no additional translation', () => {
|
|
70
|
+
expect(isLocaleEligible('de', { ...base, availableLocales: [] })).toBe(false);
|
|
71
|
+
});
|
|
72
|
+
it('public: a collection without advertiseLocales is gated by completeness alone', () => {
|
|
73
|
+
const optOut = { ...base, advertiseLocales: false, availableLocales: [] };
|
|
74
|
+
expect(isLocaleEligible('es', optOut)).toBe(true);
|
|
75
|
+
expect(isLocaleEligible('fr', optOut)).toBe(false);
|
|
76
|
+
});
|
|
77
|
+
it('a locale-agnostic version is eligible in every locale', () => {
|
|
78
|
+
const agnostic = { ...base, ledger: { availableLocales: [], localeAgnostic: true } };
|
|
79
|
+
expect(isLocaleEligible('fr', agnostic)).toBe(true);
|
|
80
|
+
});
|
|
81
|
+
it('editorial: a complete but unchecked translation is eligible', () => {
|
|
82
|
+
expect(isLocaleEligible('es', { ...base, visibility: 'editorial' })).toBe(true);
|
|
83
|
+
});
|
|
84
|
+
it('editorial: completeness is still required', () => {
|
|
85
|
+
expect(isLocaleEligible('fr', { ...base, visibility: 'editorial' })).toBe(false);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
describe('resolveEligibleLocale', () => {
|
|
89
|
+
const base = {
|
|
90
|
+
visibility: 'public',
|
|
91
|
+
sourceLocale: 'en',
|
|
92
|
+
ledger: { availableLocales: ['en', 'es', 'es-MX'], localeAgnostic: false },
|
|
93
|
+
advertiseLocales: true,
|
|
94
|
+
availableLocales: [],
|
|
95
|
+
};
|
|
96
|
+
it('public: an unchecked translation falls back to the source', () => {
|
|
97
|
+
expect(resolveEligibleLocale(['es', 'en'], base)).toBe('en');
|
|
98
|
+
});
|
|
99
|
+
it('editorial: the same unchecked translation is selected', () => {
|
|
100
|
+
expect(resolveEligibleLocale(['es', 'en'], { ...base, visibility: 'editorial' })).toBe('es');
|
|
101
|
+
});
|
|
102
|
+
it('public: a withheld intermediate candidate is skipped', () => {
|
|
103
|
+
// Named fallback chains are not configurable yet; this pins that every
|
|
104
|
+
// candidate must satisfy the policy if they are added later.
|
|
105
|
+
const eligibility = { ...base, availableLocales: ['es'] };
|
|
106
|
+
expect(resolveEligibleLocale(['es-MX', 'es', 'en'], eligibility)).toBe('es');
|
|
107
|
+
expect(resolveEligibleLocale(['es-MX', 'es', 'en'], base)).toBe('en');
|
|
108
|
+
});
|
|
109
|
+
it('returns the floor when nothing earlier qualifies', () => {
|
|
110
|
+
expect(resolveEligibleLocale(['fr', 'en'], base)).toBe('en');
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
describe('resolveLocaleVisibility', () => {
|
|
114
|
+
it('defaults to public for an omitted or published status', () => {
|
|
115
|
+
expect(resolveLocaleVisibility(undefined)).toBe('public');
|
|
116
|
+
expect(resolveLocaleVisibility('published')).toBe('public');
|
|
117
|
+
});
|
|
118
|
+
it('defaults to editorial for status any', () => {
|
|
119
|
+
expect(resolveLocaleVisibility('any')).toBe('editorial');
|
|
120
|
+
});
|
|
121
|
+
it('an explicit value wins over the status default', () => {
|
|
122
|
+
expect(resolveLocaleVisibility('published', 'editorial')).toBe('editorial');
|
|
123
|
+
expect(resolveLocaleVisibility('any', 'public')).toBe('public');
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
describe('resolveReadLocale — the seven resolvedLocale result types', () => {
|
|
127
|
+
const eligibility = (overrides = {}) => ({
|
|
128
|
+
visibility: 'public',
|
|
129
|
+
sourceLocale: 'en',
|
|
130
|
+
// Spanish complete but unchecked; German partial (not in the ledger).
|
|
131
|
+
ledger: { availableLocales: ['en', 'es'], localeAgnostic: false },
|
|
132
|
+
advertiseLocales: true,
|
|
133
|
+
availableLocales: [],
|
|
134
|
+
...overrides,
|
|
135
|
+
});
|
|
136
|
+
const chain = (locale) => (locale === 'en' ? ['en'] : [locale, 'en']);
|
|
137
|
+
it('R1: fallback reports the selected permitted locale, including the source', () => {
|
|
138
|
+
expect(resolveReadLocale({
|
|
139
|
+
locale: 'es',
|
|
140
|
+
chain: chain('es'),
|
|
141
|
+
onMissingLocale: 'fallback',
|
|
142
|
+
eligibility: eligibility({ availableLocales: ['es'] }),
|
|
143
|
+
})).toEqual({ restoreLocale: 'es', resolvedLocale: 'es', withholdLocalized: false });
|
|
144
|
+
expect(resolveReadLocale({
|
|
145
|
+
locale: 'es',
|
|
146
|
+
chain: chain('es'),
|
|
147
|
+
onMissingLocale: 'fallback',
|
|
148
|
+
eligibility: eligibility(),
|
|
149
|
+
})).toEqual({ restoreLocale: 'en', resolvedLocale: 'en', withholdLocalized: false });
|
|
150
|
+
expect(resolveReadLocale({
|
|
151
|
+
locale: 'es',
|
|
152
|
+
chain: chain('es'),
|
|
153
|
+
onMissingLocale: 'fallback',
|
|
154
|
+
eligibility: eligibility({ visibility: 'editorial' }),
|
|
155
|
+
})).toEqual({ restoreLocale: 'es', resolvedLocale: 'es', withholdLocalized: false });
|
|
156
|
+
});
|
|
157
|
+
it('R2: omit reports the requested locale', () => {
|
|
158
|
+
expect(resolveReadLocale({
|
|
159
|
+
locale: 'es',
|
|
160
|
+
chain: chain('es'),
|
|
161
|
+
onMissingLocale: 'omit',
|
|
162
|
+
eligibility: eligibility({ availableLocales: ['es'] }),
|
|
163
|
+
})).toEqual({ restoreLocale: 'es', resolvedLocale: 'es', withholdLocalized: false });
|
|
164
|
+
});
|
|
165
|
+
it('R3: public empty with an eligible locale reports it, with no fallback', () => {
|
|
166
|
+
expect(resolveReadLocale({
|
|
167
|
+
locale: 'es',
|
|
168
|
+
chain: chain('es'),
|
|
169
|
+
onMissingLocale: 'empty',
|
|
170
|
+
eligibility: eligibility({ availableLocales: ['es'] }),
|
|
171
|
+
})).toEqual({ restoreLocale: 'es', resolvedLocale: 'es', withholdLocalized: false });
|
|
172
|
+
});
|
|
173
|
+
it('R4: public empty with an unavailable translation withholds values and reports null', () => {
|
|
174
|
+
// Complete but unchecked.
|
|
175
|
+
expect(resolveReadLocale({
|
|
176
|
+
locale: 'es',
|
|
177
|
+
chain: chain('es'),
|
|
178
|
+
onMissingLocale: 'empty',
|
|
179
|
+
eligibility: eligibility(),
|
|
180
|
+
})).toEqual({ restoreLocale: 'es', resolvedLocale: null, withholdLocalized: true });
|
|
181
|
+
// Checked but incomplete.
|
|
182
|
+
expect(resolveReadLocale({
|
|
183
|
+
locale: 'de',
|
|
184
|
+
chain: chain('de'),
|
|
185
|
+
onMissingLocale: 'empty',
|
|
186
|
+
eligibility: eligibility({ availableLocales: ['de'] }),
|
|
187
|
+
})).toEqual({ restoreLocale: 'de', resolvedLocale: null, withholdLocalized: true });
|
|
188
|
+
// An omitted policy is exact, like empty.
|
|
189
|
+
expect(resolveReadLocale({
|
|
190
|
+
locale: 'es',
|
|
191
|
+
chain: chain('es'),
|
|
192
|
+
onMissingLocale: undefined,
|
|
193
|
+
eligibility: eligibility(),
|
|
194
|
+
}).withholdLocalized).toBe(true);
|
|
195
|
+
});
|
|
196
|
+
it('R4: public empty still serves the source even when its checkbox is off', () => {
|
|
197
|
+
expect(resolveReadLocale({
|
|
198
|
+
locale: 'en',
|
|
199
|
+
chain: chain('en'),
|
|
200
|
+
onMissingLocale: 'empty',
|
|
201
|
+
eligibility: eligibility(),
|
|
202
|
+
})).toEqual({ restoreLocale: 'en', resolvedLocale: 'en', withholdLocalized: false });
|
|
203
|
+
});
|
|
204
|
+
it('R5: editorial empty reports the requested locale even when partial', () => {
|
|
205
|
+
expect(resolveReadLocale({
|
|
206
|
+
locale: 'de',
|
|
207
|
+
chain: chain('de'),
|
|
208
|
+
onMissingLocale: 'empty',
|
|
209
|
+
eligibility: eligibility({ visibility: 'editorial' }),
|
|
210
|
+
})).toEqual({ restoreLocale: 'de', resolvedLocale: 'de', withholdLocalized: false });
|
|
211
|
+
});
|
|
212
|
+
it('R6: a multi-locale read reports null and restores locale maps', () => {
|
|
213
|
+
expect(resolveReadLocale({
|
|
214
|
+
locale: 'all',
|
|
215
|
+
chain: ['all'],
|
|
216
|
+
onMissingLocale: 'empty',
|
|
217
|
+
eligibility: eligibility({ visibility: 'editorial' }),
|
|
218
|
+
})).toEqual({ restoreLocale: undefined, resolvedLocale: null, withholdLocalized: false });
|
|
219
|
+
});
|
|
220
|
+
it('R7: a locale-agnostic version reports null under every policy', () => {
|
|
221
|
+
const agnostic = eligibility({ ledger: { availableLocales: [], localeAgnostic: true } });
|
|
222
|
+
for (const onMissingLocale of ['fallback', 'omit', 'empty', undefined]) {
|
|
223
|
+
expect(resolveReadLocale({
|
|
224
|
+
locale: 'es',
|
|
225
|
+
chain: chain('es'),
|
|
226
|
+
onMissingLocale,
|
|
227
|
+
eligibility: agnostic,
|
|
228
|
+
})).toEqual({ restoreLocale: 'es', resolvedLocale: null, withholdLocalized: false });
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
it('a non-advertised collection withholds an incomplete translation publicly', () => {
|
|
232
|
+
expect(resolveReadLocale({
|
|
233
|
+
locale: 'de',
|
|
234
|
+
chain: chain('de'),
|
|
235
|
+
onMissingLocale: 'empty',
|
|
236
|
+
eligibility: eligibility({ advertiseLocales: false }),
|
|
237
|
+
}).withholdLocalized).toBe(true);
|
|
238
|
+
expect(resolveReadLocale({
|
|
239
|
+
locale: 'es',
|
|
240
|
+
chain: chain('es'),
|
|
241
|
+
onMissingLocale: 'empty',
|
|
242
|
+
eligibility: eligibility({ advertiseLocales: false }),
|
|
243
|
+
})).toEqual({ restoreLocale: 'es', resolvedLocale: 'es', withholdLocalized: false });
|
|
244
|
+
});
|
|
245
|
+
});
|
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": "6.
|
|
5
|
+
"version": "6.7.1",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": ">=20.9.0"
|
|
8
8
|
},
|
|
@@ -87,7 +87,7 @@
|
|
|
87
87
|
"sharp": "^0.35.4",
|
|
88
88
|
"uuid": "^14.0.2",
|
|
89
89
|
"zod": "^4.6.5",
|
|
90
|
-
"@byline/auth": "6.
|
|
90
|
+
"@byline/auth": "6.7.1"
|
|
91
91
|
},
|
|
92
92
|
"devDependencies": {
|
|
93
93
|
"@biomejs/biome": "2.5.14",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { defineCollection } from '../../@types/collection-types.js';
|
|
3
|
-
import { createCollectionSchemas } from './builder.js';
|
|
4
|
-
const collection = defineCollection({
|
|
5
|
-
path: 'review-probe', labels: { singular: 'Probe', plural: 'Probes' },
|
|
6
|
-
fields: [{ name: 'choice', type: 'select', options: [{ label: 'A', value: 'a' }] }],
|
|
7
|
-
});
|
|
8
|
-
const schemas = createCollectionSchemas(collection);
|
|
9
|
-
const doc = {
|
|
10
|
-
id: '550e8400-e29b-41d4-a716-446655440000', status: 'draft',
|
|
11
|
-
createdAt: '2026-09-16T12:00:00.000Z', updatedAt: '2026-09-16T12:00:00.000Z',
|
|
12
|
-
fields: { choice: 'a' },
|
|
13
|
-
};
|
|
14
|
-
const response = {
|
|
15
|
-
docs: [doc], meta: { page: 1, pageSize: 10, total: 1, totalPages: 1 },
|
|
16
|
-
included: { collection: { id: 'review-probe', path: collection.path, labels: collection.labels } },
|
|
17
|
-
};
|
|
18
|
-
describe('review comparison: invalid values reject a read response', () => {
|
|
19
|
-
it('accepts a valid response', () => expect(schemas.list.safeParse(response).success).toBe(true));
|
|
20
|
-
it('rejects the response if one document has an invalid select', () => {
|
|
21
|
-
expect(schemas.list.safeParse({ ...response, docs: [doc, { ...doc, fields: { choice: 'bogus' } }] }).success).toBe(false);
|
|
22
|
-
});
|
|
23
|
-
it('rejects the response if one document has an invalid status', () => {
|
|
24
|
-
expect(schemas.list.safeParse({ ...response, docs: [doc, { ...doc, status: 'bogus' }] }).success).toBe(false);
|
|
25
|
-
});
|
|
26
|
-
});
|