@endora-commerce/mod-dictionaries 0.100.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +60 -0
- package/dist/admin/api/client.d.ts +76 -0
- package/dist/admin/api/client.d.ts.map +1 -0
- package/dist/admin/api/client.js +87 -0
- package/dist/admin/api/client.js.map +1 -0
- package/dist/admin/components/EntryStatusBadges.d.ts +9 -0
- package/dist/admin/components/EntryStatusBadges.d.ts.map +1 -0
- package/dist/admin/components/EntryStatusBadges.js +8 -0
- package/dist/admin/components/EntryStatusBadges.js.map +1 -0
- package/dist/admin/components/LanguagePicker.d.ts +8 -0
- package/dist/admin/components/LanguagePicker.d.ts.map +1 -0
- package/dist/admin/components/LanguagePicker.js +30 -0
- package/dist/admin/components/LanguagePicker.js.map +1 -0
- package/dist/admin/components/TranslationsDrawer.d.ts +10 -0
- package/dist/admin/components/TranslationsDrawer.d.ts.map +1 -0
- package/dist/admin/components/TranslationsDrawer.js +90 -0
- package/dist/admin/components/TranslationsDrawer.js.map +1 -0
- package/dist/admin/index.d.ts +48 -0
- package/dist/admin/index.d.ts.map +1 -0
- package/dist/admin/index.js +82 -0
- package/dist/admin/index.js.map +1 -0
- package/dist/admin/pages/AuditPage.d.ts +9 -0
- package/dist/admin/pages/AuditPage.d.ts.map +1 -0
- package/dist/admin/pages/AuditPage.js +80 -0
- package/dist/admin/pages/AuditPage.js.map +1 -0
- package/dist/admin/pages/DictionaryPage.d.ts +9 -0
- package/dist/admin/pages/DictionaryPage.d.ts.map +1 -0
- package/dist/admin/pages/DictionaryPage.js +38 -0
- package/dist/admin/pages/DictionaryPage.js.map +1 -0
- package/dist/admin/tabs/CountriesTab.d.ts +3 -0
- package/dist/admin/tabs/CountriesTab.d.ts.map +1 -0
- package/dist/admin/tabs/CountriesTab.js +233 -0
- package/dist/admin/tabs/CountriesTab.js.map +1 -0
- package/dist/admin/tabs/CurrenciesTab.d.ts +3 -0
- package/dist/admin/tabs/CurrenciesTab.d.ts.map +1 -0
- package/dist/admin/tabs/CurrenciesTab.js +209 -0
- package/dist/admin/tabs/CurrenciesTab.js.map +1 -0
- package/dist/admin/tabs/LanguagesTab.d.ts +3 -0
- package/dist/admin/tabs/LanguagesTab.d.ts.map +1 -0
- package/dist/admin/tabs/LanguagesTab.js +268 -0
- package/dist/admin/tabs/LanguagesTab.js.map +1 -0
- package/dist/backend/entities/country.entity.d.ts +35 -0
- package/dist/backend/entities/country.entity.d.ts.map +1 -0
- package/dist/backend/entities/country.entity.js +107 -0
- package/dist/backend/entities/country.entity.js.map +1 -0
- package/dist/backend/entities/dictionary-translation.entity.d.ts +25 -0
- package/dist/backend/entities/dictionary-translation.entity.d.ts.map +1 -0
- package/dist/backend/entities/dictionary-translation.entity.js +65 -0
- package/dist/backend/entities/dictionary-translation.entity.js.map +1 -0
- package/dist/backend/entities/language-country.entity.d.ts +17 -0
- package/dist/backend/entities/language-country.entity.d.ts.map +1 -0
- package/dist/backend/entities/language-country.entity.js +49 -0
- package/dist/backend/entities/language-country.entity.js.map +1 -0
- package/dist/backend/index.d.ts +85 -0
- package/dist/backend/index.d.ts.map +1 -0
- package/dist/backend/index.js +155 -0
- package/dist/backend/index.js.map +1 -0
- package/dist/backend/routes.admin.d.ts +39 -0
- package/dist/backend/routes.admin.d.ts.map +1 -0
- package/dist/backend/routes.admin.js +317 -0
- package/dist/backend/routes.admin.js.map +1 -0
- package/dist/backend/routes.storefront.d.ts +7 -0
- package/dist/backend/routes.storefront.d.ts.map +1 -0
- package/dist/backend/routes.storefront.js +34 -0
- package/dist/backend/routes.storefront.js.map +1 -0
- package/dist/backend/seed/countries.d.ts +28 -0
- package/dist/backend/seed/countries.d.ts.map +1 -0
- package/dist/backend/seed/countries.js +121 -0
- package/dist/backend/seed/countries.js.map +1 -0
- package/dist/backend/seed/currencies.d.ts +9 -0
- package/dist/backend/seed/currencies.d.ts.map +1 -0
- package/dist/backend/seed/currencies.js +76 -0
- package/dist/backend/seed/currencies.js.map +1 -0
- package/dist/backend/seed/language-countries.d.ts +7 -0
- package/dist/backend/seed/language-countries.d.ts.map +1 -0
- package/dist/backend/seed/language-countries.js +19 -0
- package/dist/backend/seed/language-countries.js.map +1 -0
- package/dist/backend/seed/translations.pl-PL.d.ts +10 -0
- package/dist/backend/seed/translations.pl-PL.d.ts.map +1 -0
- package/dist/backend/seed/translations.pl-PL.js +74 -0
- package/dist/backend/seed/translations.pl-PL.js.map +1 -0
- package/dist/backend/services/country-currency-reference.d.ts +13 -0
- package/dist/backend/services/country-currency-reference.d.ts.map +1 -0
- package/dist/backend/services/country-currency-reference.js +25 -0
- package/dist/backend/services/country-currency-reference.js.map +1 -0
- package/dist/backend/services/country-reference-registry.d.ts +30 -0
- package/dist/backend/services/country-reference-registry.d.ts.map +1 -0
- package/dist/backend/services/country-reference-registry.js +54 -0
- package/dist/backend/services/country-reference-registry.js.map +1 -0
- package/dist/backend/services/country-service.d.ts +69 -0
- package/dist/backend/services/country-service.d.ts.map +1 -0
- package/dist/backend/services/country-service.js +193 -0
- package/dist/backend/services/country-service.js.map +1 -0
- package/dist/backend/services/dictionary-cache.d.ts +27 -0
- package/dist/backend/services/dictionary-cache.d.ts.map +1 -0
- package/dist/backend/services/dictionary-cache.js +57 -0
- package/dist/backend/services/dictionary-cache.js.map +1 -0
- package/dist/backend/services/dictionary-read-service.d.ts +36 -0
- package/dist/backend/services/dictionary-read-service.d.ts.map +1 -0
- package/dist/backend/services/dictionary-read-service.js +232 -0
- package/dist/backend/services/dictionary-read-service.js.map +1 -0
- package/dist/backend/services/dictionary-validator.d.ts +26 -0
- package/dist/backend/services/dictionary-validator.d.ts.map +1 -0
- package/dist/backend/services/dictionary-validator.js +72 -0
- package/dist/backend/services/dictionary-validator.js.map +1 -0
- package/dist/backend/services/dispatch-validator-mode.d.ts +19 -0
- package/dist/backend/services/dispatch-validator-mode.d.ts.map +1 -0
- package/dist/backend/services/dispatch-validator-mode.js +19 -0
- package/dist/backend/services/dispatch-validator-mode.js.map +1 -0
- package/dist/backend/services/label-resolver.d.ts +57 -0
- package/dist/backend/services/label-resolver.d.ts.map +1 -0
- package/dist/backend/services/label-resolver.js +116 -0
- package/dist/backend/services/label-resolver.js.map +1 -0
- package/dist/backend/services/language-country-service.d.ts +26 -0
- package/dist/backend/services/language-country-service.d.ts.map +1 -0
- package/dist/backend/services/language-country-service.js +108 -0
- package/dist/backend/services/language-country-service.js.map +1 -0
- package/dist/backend/services/seed-reconciler.d.ts +22 -0
- package/dist/backend/services/seed-reconciler.d.ts.map +1 -0
- package/dist/backend/services/seed-reconciler.js +149 -0
- package/dist/backend/services/seed-reconciler.js.map +1 -0
- package/dist/backend/services/translation-service.d.ts +29 -0
- package/dist/backend/services/translation-service.d.ts.map +1 -0
- package/dist/backend/services/translation-service.js +99 -0
- package/dist/backend/services/translation-service.js.map +1 -0
- package/dist/manifest.d.ts +174 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +178 -0
- package/dist/manifest.js.map +1 -0
- package/dist/migrations/20260506T112634_dictionaries_dictionary_init.d.ts +33 -0
- package/dist/migrations/20260506T112634_dictionaries_dictionary_init.d.ts.map +1 -0
- package/dist/migrations/20260506T112634_dictionaries_dictionary_init.js +157 -0
- package/dist/migrations/20260506T112634_dictionaries_dictionary_init.js.map +1 -0
- package/dist/migrations/index.d.ts +27 -0
- package/dist/migrations/index.d.ts.map +1 -0
- package/dist/migrations/index.js +29 -0
- package/dist/migrations/index.js.map +1 -0
- package/docs/dictionaries/index.md +89 -0
- package/i18n/en.json +88 -0
- package/i18n/pl.json +88 -0
- package/package.json +99 -0
- package/tailwind.css +14 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { DictionaryReferenceError, } from '@endora-commerce/contracts';
|
|
2
|
+
import { Country } from '../entities/country.entity.js';
|
|
3
|
+
const TTL_MS = 60_000;
|
|
4
|
+
export class DictionaryValidator {
|
|
5
|
+
emFactory;
|
|
6
|
+
currencies;
|
|
7
|
+
languages;
|
|
8
|
+
cache = new Map();
|
|
9
|
+
/**
|
|
10
|
+
* Feature 075, Phase C — `country` is this module's own table and stays an
|
|
11
|
+
* `em.findOne`; `currency` and `language` are not, and go through their
|
|
12
|
+
* owners' read ports. The reads used to be `em.findOne(Currency, …)` /
|
|
13
|
+
* `em.findOne(Language, …)` against tables deactivation does not drop, so a
|
|
14
|
+
* write validated against a switched-off `currencies` was accepted as valid.
|
|
15
|
+
* Fail-closed is the right answer for a validator: an unvalidated code
|
|
16
|
+
* written into an order or a tax rule is worse than a refused write, and both
|
|
17
|
+
* owners are binding `dependencies` of this manifest.
|
|
18
|
+
*/
|
|
19
|
+
constructor(emFactory, currencies, languages) {
|
|
20
|
+
this.emFactory = emFactory;
|
|
21
|
+
this.currencies = currencies;
|
|
22
|
+
this.languages = languages;
|
|
23
|
+
}
|
|
24
|
+
async validateCountryCode(code, mode) {
|
|
25
|
+
await this.validate('country', code.toUpperCase(), mode);
|
|
26
|
+
}
|
|
27
|
+
async validateCurrencyCode(code, mode) {
|
|
28
|
+
await this.validate('currency', code.toUpperCase(), mode);
|
|
29
|
+
}
|
|
30
|
+
async validateLanguageCode(code, mode) {
|
|
31
|
+
await this.validate('language', code, mode);
|
|
32
|
+
}
|
|
33
|
+
invalidate() {
|
|
34
|
+
this.cache.clear();
|
|
35
|
+
}
|
|
36
|
+
async validate(entryType, entryCode, mode) {
|
|
37
|
+
const state = await this.getState(entryType, entryCode);
|
|
38
|
+
if (!state.exists) {
|
|
39
|
+
throw new DictionaryReferenceError({
|
|
40
|
+
code: 'DICTIONARY_ENTRY_NOT_FOUND',
|
|
41
|
+
entryType,
|
|
42
|
+
entryCode,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
if (mode === 'create-or-change' && !state.isActive) {
|
|
46
|
+
throw new DictionaryReferenceError({
|
|
47
|
+
code: 'DICTIONARY_ENTRY_INACTIVE',
|
|
48
|
+
entryType,
|
|
49
|
+
entryCode,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
async getState(entryType, entryCode) {
|
|
54
|
+
const key = `${entryType}:${entryCode}`;
|
|
55
|
+
const cached = this.cache.get(key);
|
|
56
|
+
if (cached && cached.expiresAt > Date.now())
|
|
57
|
+
return cached;
|
|
58
|
+
const row = entryType === 'country'
|
|
59
|
+
? await this.emFactory().findOne(Country, { code: entryCode })
|
|
60
|
+
: entryType === 'currency'
|
|
61
|
+
? await this.currencies.findByCode(entryCode)
|
|
62
|
+
: await this.languages.findByCode(entryCode);
|
|
63
|
+
const next = {
|
|
64
|
+
exists: Boolean(row),
|
|
65
|
+
isActive: row?.isActive ?? false,
|
|
66
|
+
expiresAt: Date.now() + TTL_MS,
|
|
67
|
+
};
|
|
68
|
+
this.cache.set(key, next);
|
|
69
|
+
return next;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=dictionary-validator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dictionary-validator.js","sourceRoot":"","sources":["../../../src/backend/services/dictionary-validator.ts"],"names":[],"mappings":"AACA,OAAO,EACL,wBAAwB,GAMzB,MAAM,4BAA4B,CAAC;AACpC,OAAO,EAAE,OAAO,EAAE,MAAM,+BAA+B,CAAC;AAQxD,MAAM,MAAM,GAAG,MAAM,CAAC;AAEtB,MAAM,OAAO,mBAAmB;IAcX;IACA;IACA;IAfF,KAAK,GAAG,IAAI,GAAG,EAAsB,CAAC;IAEvD;;;;;;;;;OASG;IACH,YACmB,SAA8B,EAC9B,UAA4B,EAC5B,SAA2B;QAF3B,cAAS,GAAT,SAAS,CAAqB;QAC9B,eAAU,GAAV,UAAU,CAAkB;QAC5B,cAAS,GAAT,SAAS,CAAkB;IAC3C,CAAC;IAEJ,KAAK,CAAC,mBAAmB,CAAC,IAAY,EAAE,IAAyB;QAC/D,MAAM,IAAI,CAAC,QAAQ,CAAC,SAAS,EAAE,IAAI,CAAC,WAAW,EAAE,EAAE,IAAI,CAAC,CAAC;IAC3D,CAAC;IAED,KAAK,CAAC,oBAAoB,CAAC,IAAY,EAAE,IAAyB;QAChE,MAAM,IAAI,CAAC,QAAQ,CAAC,UAAU,EAAE,IAAI,CAAC,WAAW,EAAE,EAAE,IAAI,CAAC,CAAC;IAC5D,CAAC;IAED,KAAK,CAAC,oBAAoB,CAAC,IAAY,EAAE,IAAyB;QAChE,MAAM,IAAI,CAAC,QAAQ,CAAC,UAAU,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;IAC9C,CAAC;IAED,UAAU;QACR,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC;IACrB,CAAC;IAEO,KAAK,CAAC,QAAQ,CACpB,SAA8B,EAC9B,SAAiB,EACjB,IAAyB;QAEzB,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,QAAQ,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;QACxD,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC;YAClB,MAAM,IAAI,wBAAwB,CAAC;gBACjC,IAAI,EAAE,4BAA4B;gBAClC,SAAS;gBACT,SAAS;aACV,CAAC,CAAC;QACL,CAAC;QACD,IAAI,IAAI,KAAK,kBAAkB,IAAI,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC;YACnD,MAAM,IAAI,wBAAwB,CAAC;gBACjC,IAAI,EAAE,2BAA2B;gBACjC,SAAS;gBACT,SAAS;aACV,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAEO,KAAK,CAAC,QAAQ,CAAC,SAA8B,EAAE,SAAiB;QACtE,MAAM,GAAG,GAAG,GAAG,SAAS,IAAI,SAAS,EAAE,CAAC;QACxC,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACnC,IAAI,MAAM,IAAI,MAAM,CAAC,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE;YAAE,OAAO,MAAM,CAAC;QAE3D,MAAM,GAAG,GACP,SAAS,KAAK,SAAS;YACrB,CAAC,CAAC,MAAM,IAAI,CAAC,SAAS,EAAE,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;YAC9D,CAAC,CAAC,SAAS,KAAK,UAAU;gBACxB,CAAC,CAAC,MAAM,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC,SAAS,CAAC;gBAC7C,CAAC,CAAC,MAAM,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC;QACnD,MAAM,IAAI,GAAe;YACvB,MAAM,EAAE,OAAO,CAAC,GAAG,CAAC;YACpB,QAAQ,EAAE,GAAG,EAAE,QAAQ,IAAI,KAAK;YAChC,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,MAAM;SAC/B,CAAC;QACF,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;QAC1B,OAAO,IAAI,CAAC;IACd,CAAC;CACF"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `dispatchValidatorMode` moved to `@endora-commerce/contracts` in feature 075's Phase P.
|
|
3
|
+
*
|
|
4
|
+
* It is published as a **function, not a port**, which contradicts
|
|
5
|
+
* `contracts/port-publication.md` §1.5 — that section names this file as the
|
|
6
|
+
* worked example of a helper that "reads state the module owns", and it reads
|
|
7
|
+
* nothing. Three comparisons over its two arguments: switching `dictionaries`
|
|
8
|
+
* off cannot change the answer, so a gated port would answer 503 to a question
|
|
9
|
+
* about two strings the caller already holds (FR-013).
|
|
10
|
+
*
|
|
11
|
+
* The five callers reach `dictionaryValidator` immediately afterwards, and
|
|
12
|
+
* that one *is* a port and *does* read this module's tables. Splitting the
|
|
13
|
+
* pure decision from the stateful validation is what lets the second fail
|
|
14
|
+
* closed without the first inventing a mode.
|
|
15
|
+
*
|
|
16
|
+
* Re-exported here for the length of Phase P, which cuts no consumer.
|
|
17
|
+
*/
|
|
18
|
+
export { dispatchValidatorMode } from '@endora-commerce/contracts';
|
|
19
|
+
//# sourceMappingURL=dispatch-validator-mode.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dispatch-validator-mode.d.ts","sourceRoot":"","sources":["../../../src/backend/services/dispatch-validator-mode.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAC"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `dispatchValidatorMode` moved to `@endora-commerce/contracts` in feature 075's Phase P.
|
|
3
|
+
*
|
|
4
|
+
* It is published as a **function, not a port**, which contradicts
|
|
5
|
+
* `contracts/port-publication.md` §1.5 — that section names this file as the
|
|
6
|
+
* worked example of a helper that "reads state the module owns", and it reads
|
|
7
|
+
* nothing. Three comparisons over its two arguments: switching `dictionaries`
|
|
8
|
+
* off cannot change the answer, so a gated port would answer 503 to a question
|
|
9
|
+
* about two strings the caller already holds (FR-013).
|
|
10
|
+
*
|
|
11
|
+
* The five callers reach `dictionaryValidator` immediately afterwards, and
|
|
12
|
+
* that one *is* a port and *does* read this module's tables. Splitting the
|
|
13
|
+
* pure decision from the stateful validation is what lets the second fail
|
|
14
|
+
* closed without the first inventing a mode.
|
|
15
|
+
*
|
|
16
|
+
* Re-exported here for the length of Phase P, which cuts no consumer.
|
|
17
|
+
*/
|
|
18
|
+
export { dispatchValidatorMode } from '@endora-commerce/contracts';
|
|
19
|
+
//# sourceMappingURL=dispatch-validator-mode.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dispatch-validator-mode.js","sourceRoot":"","sources":["../../../src/backend/services/dispatch-validator-mode.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAC"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { EntityManager } from '@mikro-orm/postgresql';
|
|
2
|
+
import { type CurrencyReadPort, type DictionaryEntryType, type LanguageReadPort } from '@endora-commerce/contracts';
|
|
3
|
+
export interface ResolveLabelArgs {
|
|
4
|
+
entryType: DictionaryEntryType;
|
|
5
|
+
entryCode: string;
|
|
6
|
+
locale?: string;
|
|
7
|
+
}
|
|
8
|
+
/** One entry to label, with the canonical label its own row already carries. */
|
|
9
|
+
export interface LabelBatchEntry {
|
|
10
|
+
entryType: DictionaryEntryType;
|
|
11
|
+
entryCode: string;
|
|
12
|
+
canonicalLabel: string;
|
|
13
|
+
}
|
|
14
|
+
/** Key into the map {@link LabelResolver.resolveLabels} returns. */
|
|
15
|
+
export declare function labelKey(entryType: DictionaryEntryType, entryCode: string): string;
|
|
16
|
+
export declare class LabelResolver {
|
|
17
|
+
private readonly emFactory;
|
|
18
|
+
private readonly currencies;
|
|
19
|
+
private readonly languages;
|
|
20
|
+
/**
|
|
21
|
+
* Feature 075, Phase C — the fallback chain is walked in `languages`' table
|
|
22
|
+
* and a canonical currency or language label is read from its owner's, so
|
|
23
|
+
* both go through the published read ports. Issue #142's property is
|
|
24
|
+
* unchanged: `list()` is one statement, exactly as the `em.find(Language,
|
|
25
|
+
* {})` it replaces was, so the cold registry build stays at seven.
|
|
26
|
+
*/
|
|
27
|
+
constructor(emFactory: () => EntityManager, currencies: CurrencyReadPort, languages: LanguageReadPort);
|
|
28
|
+
/**
|
|
29
|
+
* Labels for a whole batch, in a **constant** number of statements: one for
|
|
30
|
+
* the language table the fallback chain is walked in, one for every
|
|
31
|
+
* translation any entry in the batch could use.
|
|
32
|
+
*
|
|
33
|
+
* Issue #142 — the registry called {@link resolveLabel} once per entry, and
|
|
34
|
+
* each call re-read the canonical row, then the translation, then the
|
|
35
|
+
* fallback language, one statement at a time on one connection. Over the 72
|
|
36
|
+
* entries a seeded registry answers that was ~170 sequential round trips, and
|
|
37
|
+
* it grew with the dictionary. The fallback chain in particular depends only
|
|
38
|
+
* on the locale, so it was the same walk repeated once per entry.
|
|
39
|
+
*
|
|
40
|
+
* Callers already hold the rows, so the canonical label is passed in rather
|
|
41
|
+
* than read back; an entry with no translation in the chain keeps it.
|
|
42
|
+
*/
|
|
43
|
+
resolveLabels(entries: readonly LabelBatchEntry[], locale?: string): Promise<Map<string, string>>;
|
|
44
|
+
resolveLabel(args: ResolveLabelArgs): Promise<string>;
|
|
45
|
+
/** The label the entry's own row carries, from whichever module owns it. */
|
|
46
|
+
private canonicalLabel;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The locale, then each `fallbackCode` after it — the same walk the per-entry
|
|
50
|
+
* resolver used to make one statement at a time. A locale no `Language` row
|
|
51
|
+
* knows ends the chain, and a cycle ends it too.
|
|
52
|
+
*/
|
|
53
|
+
export declare function buildFallbackChain(locale: string, languages: readonly {
|
|
54
|
+
code: string;
|
|
55
|
+
fallbackCode?: string | null;
|
|
56
|
+
}[]): string[];
|
|
57
|
+
//# sourceMappingURL=label-resolver.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"label-resolver.d.ts","sourceRoot":"","sources":["../../../src/backend/services/label-resolver.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,EAEL,KAAK,gBAAgB,EACrB,KAAK,mBAAmB,EACxB,KAAK,gBAAgB,EACtB,MAAM,4BAA4B,CAAC;AAQpC,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,mBAAmB,CAAC;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,gFAAgF;AAChF,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,mBAAmB,CAAC;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;CACxB;AAED,oEAAoE;AACpE,wBAAgB,QAAQ,CAAC,SAAS,EAAE,mBAAmB,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAElF;AAED,qBAAa,aAAa;IAStB,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,UAAU;IAC3B,OAAO,CAAC,QAAQ,CAAC,SAAS;IAV5B;;;;;;OAMG;gBAEgB,SAAS,EAAE,MAAM,aAAa,EAC9B,UAAU,EAAE,gBAAgB,EAC5B,SAAS,EAAE,gBAAgB;IAG9C;;;;;;;;;;;;;;OAcG;IACG,aAAa,CACjB,OAAO,EAAE,SAAS,eAAe,EAAE,EACnC,MAAM,CAAC,EAAE,MAAM,GACd,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAiCzB,YAAY,CAAC,IAAI,EAAE,gBAAgB,GAAG,OAAO,CAAC,MAAM,CAAC;IAY3D,4EAA4E;YAC9D,cAAc;CAY7B;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,SAAS;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,EAAE,GACnE,MAAM,EAAE,CAWV"}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { ERROR_CODES, } from '@endora-commerce/contracts';
|
|
2
|
+
import { HttpError } from '@endora-commerce/platform/http';
|
|
3
|
+
import { Country } from '../entities/country.entity.js';
|
|
4
|
+
import { DictionaryTranslation } from '../entities/dictionary-translation.entity.js';
|
|
5
|
+
/** Locale fallback chains never walk deeper than this, cycles included. */
|
|
6
|
+
const MAX_FALLBACK_DEPTH = 10;
|
|
7
|
+
/** Key into the map {@link LabelResolver.resolveLabels} returns. */
|
|
8
|
+
export function labelKey(entryType, entryCode) {
|
|
9
|
+
return `${entryType}:${entryCode}`;
|
|
10
|
+
}
|
|
11
|
+
export class LabelResolver {
|
|
12
|
+
emFactory;
|
|
13
|
+
currencies;
|
|
14
|
+
languages;
|
|
15
|
+
/**
|
|
16
|
+
* Feature 075, Phase C — the fallback chain is walked in `languages`' table
|
|
17
|
+
* and a canonical currency or language label is read from its owner's, so
|
|
18
|
+
* both go through the published read ports. Issue #142's property is
|
|
19
|
+
* unchanged: `list()` is one statement, exactly as the `em.find(Language,
|
|
20
|
+
* {})` it replaces was, so the cold registry build stays at seven.
|
|
21
|
+
*/
|
|
22
|
+
constructor(emFactory, currencies, languages) {
|
|
23
|
+
this.emFactory = emFactory;
|
|
24
|
+
this.currencies = currencies;
|
|
25
|
+
this.languages = languages;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Labels for a whole batch, in a **constant** number of statements: one for
|
|
29
|
+
* the language table the fallback chain is walked in, one for every
|
|
30
|
+
* translation any entry in the batch could use.
|
|
31
|
+
*
|
|
32
|
+
* Issue #142 — the registry called {@link resolveLabel} once per entry, and
|
|
33
|
+
* each call re-read the canonical row, then the translation, then the
|
|
34
|
+
* fallback language, one statement at a time on one connection. Over the 72
|
|
35
|
+
* entries a seeded registry answers that was ~170 sequential round trips, and
|
|
36
|
+
* it grew with the dictionary. The fallback chain in particular depends only
|
|
37
|
+
* on the locale, so it was the same walk repeated once per entry.
|
|
38
|
+
*
|
|
39
|
+
* Callers already hold the rows, so the canonical label is passed in rather
|
|
40
|
+
* than read back; an entry with no translation in the chain keeps it.
|
|
41
|
+
*/
|
|
42
|
+
async resolveLabels(entries, locale) {
|
|
43
|
+
const labels = new Map();
|
|
44
|
+
for (const entry of entries) {
|
|
45
|
+
labels.set(labelKey(entry.entryType, entry.entryCode), entry.canonicalLabel);
|
|
46
|
+
}
|
|
47
|
+
if (!locale || entries.length === 0)
|
|
48
|
+
return labels;
|
|
49
|
+
const em = this.emFactory();
|
|
50
|
+
const chain = buildFallbackChain(locale, await this.languages.list());
|
|
51
|
+
const rows = await em.find(DictionaryTranslation, {
|
|
52
|
+
languageCode: { $in: chain },
|
|
53
|
+
entryType: { $in: [...new Set(entries.map((entry) => entry.entryType))] },
|
|
54
|
+
entryCode: { $in: [...new Set(entries.map((entry) => entry.entryCode))] },
|
|
55
|
+
});
|
|
56
|
+
// Earlier in the chain wins, which is what walking it in order used to mean.
|
|
57
|
+
const rank = new Map(chain.map((code, index) => [code, index]));
|
|
58
|
+
const bestRank = new Map();
|
|
59
|
+
for (const row of rows) {
|
|
60
|
+
const key = labelKey(row.entryType, row.entryCode);
|
|
61
|
+
// The `$in` filters are a cross product, so a row can match a code of one
|
|
62
|
+
// entry type under another's. Only keys the batch asked for are labelled.
|
|
63
|
+
if (!labels.has(key))
|
|
64
|
+
continue;
|
|
65
|
+
const candidate = rank.get(row.languageCode);
|
|
66
|
+
if (candidate === undefined)
|
|
67
|
+
continue;
|
|
68
|
+
const current = bestRank.get(key);
|
|
69
|
+
if (current !== undefined && current <= candidate)
|
|
70
|
+
continue;
|
|
71
|
+
bestRank.set(key, candidate);
|
|
72
|
+
labels.set(key, row.label);
|
|
73
|
+
}
|
|
74
|
+
return labels;
|
|
75
|
+
}
|
|
76
|
+
async resolveLabel(args) {
|
|
77
|
+
const canonical = await this.canonicalLabel(args.entryType, args.entryCode);
|
|
78
|
+
if (!canonical)
|
|
79
|
+
throw notFound(args.entryType, args.entryCode);
|
|
80
|
+
if (!args.locale)
|
|
81
|
+
return canonical;
|
|
82
|
+
const labels = await this.resolveLabels([{ entryType: args.entryType, entryCode: args.entryCode, canonicalLabel: canonical }], args.locale);
|
|
83
|
+
return labels.get(labelKey(args.entryType, args.entryCode)) ?? canonical;
|
|
84
|
+
}
|
|
85
|
+
/** The label the entry's own row carries, from whichever module owns it. */
|
|
86
|
+
async canonicalLabel(entryType, entryCode) {
|
|
87
|
+
if (entryType === 'country') {
|
|
88
|
+
return (await this.emFactory().findOne(Country, { code: entryCode }))?.label ?? null;
|
|
89
|
+
}
|
|
90
|
+
if (entryType === 'currency') {
|
|
91
|
+
return (await this.currencies.findByCode(entryCode))?.label ?? null;
|
|
92
|
+
}
|
|
93
|
+
return (await this.languages.findByCode(entryCode))?.label ?? null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* The locale, then each `fallbackCode` after it — the same walk the per-entry
|
|
98
|
+
* resolver used to make one statement at a time. A locale no `Language` row
|
|
99
|
+
* knows ends the chain, and a cycle ends it too.
|
|
100
|
+
*/
|
|
101
|
+
export function buildFallbackChain(locale, languages) {
|
|
102
|
+
const fallbackByCode = new Map(languages.map((row) => [row.code, row.fallbackCode ?? null]));
|
|
103
|
+
const chain = [];
|
|
104
|
+
const visited = new Set();
|
|
105
|
+
let current = locale;
|
|
106
|
+
for (let depth = 0; current && depth < MAX_FALLBACK_DEPTH && !visited.has(current); depth += 1) {
|
|
107
|
+
visited.add(current);
|
|
108
|
+
chain.push(current);
|
|
109
|
+
current = fallbackByCode.get(current) ?? null;
|
|
110
|
+
}
|
|
111
|
+
return chain;
|
|
112
|
+
}
|
|
113
|
+
function notFound(entryType, entryCode) {
|
|
114
|
+
return new HttpError(404, ERROR_CODES.DICTIONARY_ENTRY_NOT_FOUND, `Dictionary ${entryType} ${entryCode} not found.`);
|
|
115
|
+
}
|
|
116
|
+
//# sourceMappingURL=label-resolver.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"label-resolver.js","sourceRoot":"","sources":["../../../src/backend/services/label-resolver.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { EntityManager } from '@mikro-orm/postgresql';
|
|
2
|
+
import { type LanguageReadPort } from '@endora-commerce/contracts';
|
|
3
|
+
import { LanguageCountry } from '../entities/language-country.entity.js';
|
|
4
|
+
import type { AuditPort } from '@endora-commerce/platform/kernel';
|
|
5
|
+
export declare class LanguageCountryService {
|
|
6
|
+
#private;
|
|
7
|
+
private readonly emFactory;
|
|
8
|
+
private readonly languages;
|
|
9
|
+
private readonly invalidateDictionaryCache?;
|
|
10
|
+
private readonly auditLog?;
|
|
11
|
+
/**
|
|
12
|
+
* Feature 075, Phase C — the link row is this module's, the country is this
|
|
13
|
+
* module's, the language is `languages`'. Only the last one crosses a
|
|
14
|
+
* boundary and it does so through the published read port.
|
|
15
|
+
*/
|
|
16
|
+
constructor(emFactory: () => EntityManager, languages: LanguageReadPort, invalidateDictionaryCache?: (() => Promise<void>) | undefined, auditLog?: AuditPort | undefined);
|
|
17
|
+
listForLanguage(languageCode: string): Promise<LanguageCountry[]>;
|
|
18
|
+
listForCountry(countryCode: string): Promise<LanguageCountry[]>;
|
|
19
|
+
upsert(input: {
|
|
20
|
+
languageCode: string;
|
|
21
|
+
countryCode: string;
|
|
22
|
+
isPrimary?: boolean;
|
|
23
|
+
}): Promise<LanguageCountry>;
|
|
24
|
+
remove(languageCode: string, countryCode: string): Promise<void>;
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=language-country-service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"language-country-service.d.ts","sourceRoot":"","sources":["../../../src/backend/services/language-country-service.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,EAAe,KAAK,gBAAgB,EAAE,MAAM,4BAA4B,CAAC;AAEhF,OAAO,EAAE,eAAe,EAAE,MAAM,wCAAwC,CAAC;AAGzE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kCAAkC,CAAC;AAElE,qBAAa,sBAAsB;;IAO/B,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,yBAAyB,CAAC;IAC3C,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC;IAT5B;;;;OAIG;gBAEgB,SAAS,EAAE,MAAM,aAAa,EAC9B,SAAS,EAAE,gBAAgB,EAC3B,yBAAyB,CAAC,GAAE,MAAM,OAAO,CAAC,IAAI,CAAC,aAAA,EAC/C,QAAQ,CAAC,EAAE,SAAS,YAAA;IAoBjC,eAAe,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC;IAKjE,cAAc,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC;IAK/D,MAAM,CAAC,KAAK,EAAE;QAClB,YAAY,EAAE,MAAM,CAAC;QACrB,WAAW,EAAE,MAAM,CAAC;QACpB,SAAS,CAAC,EAAE,OAAO,CAAC;KACrB,GAAG,OAAO,CAAC,eAAe,CAAC;IA+DtB,MAAM,CAAC,YAAY,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAQvE"}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// LanguageCountryService — feature 017 / T028.
|
|
2
|
+
//
|
|
3
|
+
// Manages the M2M between Languages and Countries plus the at-most-one-
|
|
4
|
+
// primary-language-per-Country invariant. The invariant is hard-enforced
|
|
5
|
+
// at the database via a partial unique index
|
|
6
|
+
// `uniq_language_countries_primary_per_country` (see migration
|
|
7
|
+
// `038_dictionary_init.ts`); this service implements the transactional
|
|
8
|
+
// promote+demote pattern so set-primary operations never violate the
|
|
9
|
+
// index mid-flight.
|
|
10
|
+
import { ERROR_CODES } from '@endora-commerce/contracts';
|
|
11
|
+
import { HttpError } from '@endora-commerce/platform/http';
|
|
12
|
+
import { LanguageCountry } from '../entities/language-country.entity.js';
|
|
13
|
+
import { Country } from '../entities/country.entity.js';
|
|
14
|
+
import { recordAuditFromContext } from '@endora-commerce/platform/commands';
|
|
15
|
+
export class LanguageCountryService {
|
|
16
|
+
emFactory;
|
|
17
|
+
languages;
|
|
18
|
+
invalidateDictionaryCache;
|
|
19
|
+
auditLog;
|
|
20
|
+
/**
|
|
21
|
+
* Feature 075, Phase C — the link row is this module's, the country is this
|
|
22
|
+
* module's, the language is `languages`'. Only the last one crosses a
|
|
23
|
+
* boundary and it does so through the published read port.
|
|
24
|
+
*/
|
|
25
|
+
constructor(emFactory, languages, invalidateDictionaryCache, auditLog) {
|
|
26
|
+
this.emFactory = emFactory;
|
|
27
|
+
this.languages = languages;
|
|
28
|
+
this.invalidateDictionaryCache = invalidateDictionaryCache;
|
|
29
|
+
this.auditLog = auditLog;
|
|
30
|
+
}
|
|
31
|
+
#audit(em, action, objectId, stateAfter) {
|
|
32
|
+
if (this.auditLog) {
|
|
33
|
+
recordAuditFromContext(this.auditLog, em, {
|
|
34
|
+
action,
|
|
35
|
+
objectType: 'language_country',
|
|
36
|
+
objectId,
|
|
37
|
+
stateBefore: null,
|
|
38
|
+
stateAfter,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
async listForLanguage(languageCode) {
|
|
43
|
+
const em = this.emFactory();
|
|
44
|
+
return em.find(LanguageCountry, { languageCode });
|
|
45
|
+
}
|
|
46
|
+
async listForCountry(countryCode) {
|
|
47
|
+
const em = this.emFactory();
|
|
48
|
+
return em.find(LanguageCountry, { countryCode });
|
|
49
|
+
}
|
|
50
|
+
async upsert(input) {
|
|
51
|
+
const em = this.emFactory();
|
|
52
|
+
// Validate parent rows exist — the FK enforces this too, but a clear
|
|
53
|
+
// error message at the service layer is more operator-friendly.
|
|
54
|
+
const [lang, country] = await Promise.all([
|
|
55
|
+
this.languages.findByCode(input.languageCode),
|
|
56
|
+
em.findOne(Country, { code: input.countryCode }),
|
|
57
|
+
]);
|
|
58
|
+
if (!lang) {
|
|
59
|
+
throw new HttpError(404, ERROR_CODES.DICTIONARY_ENTRY_NOT_FOUND, `Language ${input.languageCode} not found.`);
|
|
60
|
+
}
|
|
61
|
+
if (!country) {
|
|
62
|
+
throw new HttpError(404, ERROR_CODES.DICTIONARY_ENTRY_NOT_FOUND, `Country ${input.countryCode} not found.`);
|
|
63
|
+
}
|
|
64
|
+
const wantsPrimary = input.isPrimary === true;
|
|
65
|
+
// Promote-with-demote inside the EM's current tx scope (no inner
|
|
66
|
+
// transactional — see CountryService.setDefault for rationale).
|
|
67
|
+
if (wantsPrimary) {
|
|
68
|
+
await em.nativeUpdate(LanguageCountry, {
|
|
69
|
+
countryCode: input.countryCode,
|
|
70
|
+
isPrimary: true,
|
|
71
|
+
languageCode: { $ne: input.languageCode },
|
|
72
|
+
}, { isPrimary: false });
|
|
73
|
+
}
|
|
74
|
+
const existing = await em.findOne(LanguageCountry, {
|
|
75
|
+
languageCode: input.languageCode,
|
|
76
|
+
countryCode: input.countryCode,
|
|
77
|
+
});
|
|
78
|
+
const objId = `${input.languageCode}:${input.countryCode}`;
|
|
79
|
+
if (existing) {
|
|
80
|
+
if (input.isPrimary !== undefined)
|
|
81
|
+
existing.isPrimary = wantsPrimary;
|
|
82
|
+
this.#audit(em, 'language_country.upsert', objId, { isPrimary: existing.isPrimary });
|
|
83
|
+
await em.flush();
|
|
84
|
+
await this.invalidateDictionaryCache?.();
|
|
85
|
+
return existing;
|
|
86
|
+
}
|
|
87
|
+
const row = em.create(LanguageCountry, {
|
|
88
|
+
languageCode: input.languageCode,
|
|
89
|
+
countryCode: input.countryCode,
|
|
90
|
+
...(input.isPrimary !== undefined ? { isPrimary: wantsPrimary } : {}),
|
|
91
|
+
});
|
|
92
|
+
em.persist(row);
|
|
93
|
+
this.#audit(em, 'language_country.upsert', objId, { isPrimary: row.isPrimary });
|
|
94
|
+
await em.flush();
|
|
95
|
+
await this.invalidateDictionaryCache?.();
|
|
96
|
+
return row;
|
|
97
|
+
}
|
|
98
|
+
async remove(languageCode, countryCode) {
|
|
99
|
+
const em = this.emFactory();
|
|
100
|
+
const row = await em.findOne(LanguageCountry, { languageCode, countryCode });
|
|
101
|
+
if (!row)
|
|
102
|
+
return;
|
|
103
|
+
this.#audit(em, 'language_country.delete', `${languageCode}:${countryCode}`, null);
|
|
104
|
+
await em.removeAndFlush(row);
|
|
105
|
+
await this.invalidateDictionaryCache?.();
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
//# sourceMappingURL=language-country-service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"language-country-service.js","sourceRoot":"","sources":["../../../src/backend/services/language-country-service.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { EntityManager } from '@mikro-orm/postgresql';
|
|
2
|
+
import type { CurrencyReadPort, CurrencySeedPort, LanguageReadPort, LanguageSeedPort } from '@endora-commerce/contracts';
|
|
3
|
+
export interface SeedReconcilerSummary {
|
|
4
|
+
countriesInserted: number;
|
|
5
|
+
currenciesInserted: number;
|
|
6
|
+
languagesBackfilled: number;
|
|
7
|
+
translationsInserted: number;
|
|
8
|
+
languageCountriesInserted: number;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* The two tables this reconciler seeds that it does not own, and the read side
|
|
12
|
+
* of both. Every one of them is a published port of the owning module — the
|
|
13
|
+
* seam D-87's drain put where four raw statements used to be.
|
|
14
|
+
*/
|
|
15
|
+
export interface SeedReconcilerPorts {
|
|
16
|
+
readonly currencySeed: CurrencySeedPort;
|
|
17
|
+
readonly currencyRead: CurrencyReadPort;
|
|
18
|
+
readonly languageSeed: LanguageSeedPort;
|
|
19
|
+
readonly languageRead: LanguageReadPort;
|
|
20
|
+
}
|
|
21
|
+
export declare function runDictionarySeedReconciler(emFactory: () => EntityManager, ports: SeedReconcilerPorts): Promise<SeedReconcilerSummary>;
|
|
22
|
+
//# sourceMappingURL=seed-reconciler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"seed-reconciler.d.ts","sourceRoot":"","sources":["../../../src/backend/services/seed-reconciler.ts"],"names":[],"mappings":"AA6BA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,KAAK,EACV,gBAAgB,EAChB,gBAAgB,EAChB,gBAAgB,EAChB,gBAAgB,EACjB,MAAM,4BAA4B,CAAC;AAMpC,MAAM,WAAW,qBAAqB;IACpC,iBAAiB,EAAE,MAAM,CAAC;IAC1B,kBAAkB,EAAE,MAAM,CAAC;IAC3B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,oBAAoB,EAAE,MAAM,CAAC;IAC7B,yBAAyB,EAAE,MAAM,CAAC;CACnC;AAeD;;;;GAIG;AACH,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,YAAY,EAAE,gBAAgB,CAAC;IACxC,QAAQ,CAAC,YAAY,EAAE,gBAAgB,CAAC;IACxC,QAAQ,CAAC,YAAY,EAAE,gBAAgB,CAAC;IACxC,QAAQ,CAAC,YAAY,EAAE,gBAAgB,CAAC;CACzC;AAED,wBAAsB,2BAA2B,CAC/C,SAAS,EAAE,MAAM,aAAa,EAC9B,KAAK,EAAE,mBAAmB,GACzB,OAAO,CAAC,qBAAqB,CAAC,CAmGhC"}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// Boot-time seed reconciler for the Dictionary module
|
|
2
|
+
// (feature 017 / R6 / T011).
|
|
3
|
+
//
|
|
4
|
+
// Responsibilities (idempotent — safe to run on every backend boot):
|
|
5
|
+
//
|
|
6
|
+
// 1. Insert any currency from CURRENCY_SEED whose `code` is missing —
|
|
7
|
+
// through `currencySeedPort`, because the table belongs to `currencies`
|
|
8
|
+
// (feature 077, D-87: this step used to be a raw `insert into
|
|
9
|
+
// "currencies"`, a write into another module's table that named no import
|
|
10
|
+
// specifier and so crossed the boundary invisibly).
|
|
11
|
+
// Currencies that already exist are NEVER updated by the reconciler;
|
|
12
|
+
// the migration's defaults stand and operators may edit any field
|
|
13
|
+
// via Admin UI without fear of being overwritten on the next boot.
|
|
14
|
+
// 2. Insert any country from COUNTRY_SEED whose `code` is missing.
|
|
15
|
+
// 3. Backfill `languages.native_label` ONLY when it is still the empty
|
|
16
|
+
// string left by migration 038 — through `languageSeedPort`, for the same
|
|
17
|
+
// reason step 1 goes through `currencySeedPort`. Once an operator (or this
|
|
18
|
+
// reconciler) sets it to a non-empty value, future boots leave it alone.
|
|
19
|
+
// 4. Insert seeded translations for every (entry_type, entry_code,
|
|
20
|
+
// language_code) row that doesn't already exist. Existing rows are
|
|
21
|
+
// preserved verbatim (operators may edit; the reconciler will not
|
|
22
|
+
// revert their changes).
|
|
23
|
+
// 5. Insert seeded language↔country associations for every
|
|
24
|
+
// (language_code, country_code) row that doesn't already exist.
|
|
25
|
+
//
|
|
26
|
+
// All inserts are gated by an existence check that runs in the same
|
|
27
|
+
// EntityManager — there is no race condition because the reconciler is
|
|
28
|
+
// expected to run during plugin registration, before HTTP traffic.
|
|
29
|
+
import { COUNTRY_SEED } from '../seed/countries.js';
|
|
30
|
+
import { CURRENCY_SEED } from '../seed/currencies.js';
|
|
31
|
+
import { POLISH_TRANSLATION_SEED } from '../seed/translations.pl-PL.js';
|
|
32
|
+
import { LANGUAGE_COUNTRY_SEED } from '../seed/language-countries.js';
|
|
33
|
+
export async function runDictionarySeedReconciler(emFactory, ports) {
|
|
34
|
+
const em = emFactory();
|
|
35
|
+
const conn = em.getConnection();
|
|
36
|
+
const summary = {
|
|
37
|
+
countriesInserted: 0,
|
|
38
|
+
currenciesInserted: 0,
|
|
39
|
+
languagesBackfilled: 0,
|
|
40
|
+
translationsInserted: 0,
|
|
41
|
+
languageCountriesInserted: 0,
|
|
42
|
+
};
|
|
43
|
+
// 1) Currencies — insert missing rows, then read the table back. The order
|
|
44
|
+
// matters and is why the two are not one call: step 2 only sets a country's
|
|
45
|
+
// `default_currency_code` when the currency is really there.
|
|
46
|
+
summary.currenciesInserted = await ports.currencySeed.ensureSeeded(CURRENCY_SEED);
|
|
47
|
+
const present = await loadPresentCodes(conn, ports);
|
|
48
|
+
// 2) Countries — insert missing rows.
|
|
49
|
+
for (const c of COUNTRY_SEED) {
|
|
50
|
+
if (present.countries.has(c.code))
|
|
51
|
+
continue;
|
|
52
|
+
// Defensive: only set defaultCurrencyCode if the currency exists.
|
|
53
|
+
const defaultCurrency = c.defaultCurrencyCode && present.currencies.has(c.defaultCurrencyCode)
|
|
54
|
+
? c.defaultCurrencyCode
|
|
55
|
+
: null;
|
|
56
|
+
await conn.execute(`insert into "countries"
|
|
57
|
+
("code","alpha3_code","numeric_code","label","region","subregion",
|
|
58
|
+
"dial_code","is_eu_member","default_currency_code",
|
|
59
|
+
"is_active","is_default","sort_order","created_at","updated_at")
|
|
60
|
+
values (?,?,?,?,?,?,?,?,?,?,?,?,now(),now())`, [
|
|
61
|
+
c.code,
|
|
62
|
+
c.alpha3Code,
|
|
63
|
+
c.numericCode,
|
|
64
|
+
c.label,
|
|
65
|
+
c.region,
|
|
66
|
+
c.subregion ?? null,
|
|
67
|
+
c.dialCode ?? null,
|
|
68
|
+
c.isEuMember ?? false,
|
|
69
|
+
defaultCurrency,
|
|
70
|
+
c.isActive ?? true,
|
|
71
|
+
c.isDefault ?? false,
|
|
72
|
+
c.sortOrder ?? 0,
|
|
73
|
+
]);
|
|
74
|
+
present.countries.add(c.code);
|
|
75
|
+
summary.countriesInserted += 1;
|
|
76
|
+
}
|
|
77
|
+
// 3) Languages — backfill `native_label` ONLY when still the empty
|
|
78
|
+
// string left by the migration. The seed only knows the two languages
|
|
79
|
+
// already created by migration 012.
|
|
80
|
+
summary.languagesBackfilled = await ports.languageSeed.backfillNativeLabels([
|
|
81
|
+
{ code: 'en-US', nativeLabel: 'English (US)' },
|
|
82
|
+
{ code: 'pl-PL', nativeLabel: 'Polski' },
|
|
83
|
+
]);
|
|
84
|
+
// 4) Translations + 5) language↔country — load the existing keysets,
|
|
85
|
+
// then insert only what's missing.
|
|
86
|
+
const presentRels = await loadPresentTranslations(conn);
|
|
87
|
+
for (const t of POLISH_TRANSLATION_SEED) {
|
|
88
|
+
const parentSet = t.entryType === 'country'
|
|
89
|
+
? present.countries
|
|
90
|
+
: t.entryType === 'currency'
|
|
91
|
+
? present.currencies
|
|
92
|
+
: present.languages;
|
|
93
|
+
if (!parentSet.has(t.entryCode))
|
|
94
|
+
continue;
|
|
95
|
+
if (!present.languages.has(t.languageCode))
|
|
96
|
+
continue;
|
|
97
|
+
const key = `${t.entryType}|${t.entryCode}|${t.languageCode}`;
|
|
98
|
+
if (presentRels.translations.has(key))
|
|
99
|
+
continue;
|
|
100
|
+
await conn.execute(`insert into "dictionary_translations"
|
|
101
|
+
("entry_type","entry_code","language_code","label","created_at","updated_at")
|
|
102
|
+
values (?,?,?,?,now(),now())`, [t.entryType, t.entryCode, t.languageCode, t.label]);
|
|
103
|
+
presentRels.translations.add(key);
|
|
104
|
+
summary.translationsInserted += 1;
|
|
105
|
+
}
|
|
106
|
+
for (const lc of LANGUAGE_COUNTRY_SEED) {
|
|
107
|
+
if (!present.languages.has(lc.languageCode))
|
|
108
|
+
continue;
|
|
109
|
+
if (!present.countries.has(lc.countryCode))
|
|
110
|
+
continue;
|
|
111
|
+
const key = `${lc.languageCode}|${lc.countryCode}`;
|
|
112
|
+
if (presentRels.languageCountries.has(key))
|
|
113
|
+
continue;
|
|
114
|
+
await conn.execute(`insert into "language_countries"
|
|
115
|
+
("language_code","country_code","is_primary","created_at")
|
|
116
|
+
values (?,?,?,now())`, [lc.languageCode, lc.countryCode, lc.isPrimary]);
|
|
117
|
+
presentRels.languageCountries.add(key);
|
|
118
|
+
summary.languageCountriesInserted += 1;
|
|
119
|
+
}
|
|
120
|
+
return summary;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* `countries` is this module's own table and is read in SQL; the other two are
|
|
124
|
+
* read through their owners' published read ports, which is what retires the
|
|
125
|
+
* `select "code" from "currencies"` / `"languages"` pair D-87 seeded.
|
|
126
|
+
*/
|
|
127
|
+
async function loadPresentCodes(conn, ports) {
|
|
128
|
+
const [countries, currencies, languages] = await Promise.all([
|
|
129
|
+
conn.execute(`select "code" from "countries"`),
|
|
130
|
+
ports.currencyRead.list(),
|
|
131
|
+
ports.languageRead.list(),
|
|
132
|
+
]);
|
|
133
|
+
return {
|
|
134
|
+
countries: new Set(countries.map((r) => r.code)),
|
|
135
|
+
currencies: new Set(currencies.map((r) => r.code)),
|
|
136
|
+
languages: new Set(languages.map((r) => r.code)),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
async function loadPresentTranslations(conn) {
|
|
140
|
+
const [translations, languageCountries] = await Promise.all([
|
|
141
|
+
conn.execute(`select "entry_type","entry_code","language_code" from "dictionary_translations"`),
|
|
142
|
+
conn.execute(`select "language_code","country_code" from "language_countries"`),
|
|
143
|
+
]);
|
|
144
|
+
return {
|
|
145
|
+
translations: new Set(translations.map((r) => `${r.entry_type}|${r.entry_code}|${r.language_code}`)),
|
|
146
|
+
languageCountries: new Set(languageCountries.map((r) => `${r.language_code}|${r.country_code}`)),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
//# sourceMappingURL=seed-reconciler.js.map
|