@endora-commerce/mod-languages 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 +50 -0
- package/dist/backend/entities/language.entity.d.ts +23 -0
- package/dist/backend/entities/language.entity.d.ts.map +1 -0
- package/dist/backend/entities/language.entity.js +78 -0
- package/dist/backend/entities/language.entity.js.map +1 -0
- package/dist/backend/index.d.ts +63 -0
- package/dist/backend/index.d.ts.map +1 -0
- package/dist/backend/index.js +114 -0
- package/dist/backend/index.js.map +1 -0
- package/dist/backend/routes.d.ts +28 -0
- package/dist/backend/routes.d.ts.map +1 -0
- package/dist/backend/routes.js +74 -0
- package/dist/backend/routes.js.map +1 -0
- package/dist/backend/services/language-ports.d.ts +33 -0
- package/dist/backend/services/language-ports.d.ts.map +1 -0
- package/dist/backend/services/language-ports.js +70 -0
- package/dist/backend/services/language-ports.js.map +1 -0
- package/dist/backend/services/language-reference-registry.d.ts +30 -0
- package/dist/backend/services/language-reference-registry.d.ts.map +1 -0
- package/dist/backend/services/language-reference-registry.js +54 -0
- package/dist/backend/services/language-reference-registry.js.map +1 -0
- package/dist/backend/services/language-seed-service.d.ts +24 -0
- package/dist/backend/services/language-seed-service.d.ts.map +1 -0
- package/dist/backend/services/language-seed-service.js +40 -0
- package/dist/backend/services/language-seed-service.js.map +1 -0
- package/dist/backend/services/language-service.d.ts +99 -0
- package/dist/backend/services/language-service.d.ts.map +1 -0
- package/dist/backend/services/language-service.js +299 -0
- package/dist/backend/services/language-service.js.map +1 -0
- package/dist/backend/services/locale-service.d.ts +35 -0
- package/dist/backend/services/locale-service.d.ts.map +1 -0
- package/dist/backend/services/locale-service.js +88 -0
- package/dist/backend/services/locale-service.js.map +1 -0
- package/dist/manifest.d.ts +169 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +40 -0
- package/dist/manifest.js.map +1 -0
- package/dist/migrations/20260425T161557_languages_currencies_init.d.ts +13 -0
- package/dist/migrations/20260425T161557_languages_currencies_init.d.ts.map +1 -0
- package/dist/migrations/20260425T161557_languages_currencies_init.js +60 -0
- package/dist/migrations/20260425T161557_languages_currencies_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 +27 -0
- package/dist/migrations/index.js.map +1 -0
- package/docs/languages.md +88 -0
- package/package.json +68 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-process registry of "who still points at this language code" descriptors
|
|
3
|
+
* (feature 077, D-87 drain).
|
|
4
|
+
*
|
|
5
|
+
* Registered under `languageReferenceRegistry` and contributed to from other modules'
|
|
6
|
+
* boot hooks. This module consults it before deleting a language, and
|
|
7
|
+
* `dictionaries` consults it to build the orphan report.
|
|
8
|
+
*
|
|
9
|
+
* **Enumeration policy: honoured while the contributing module is absent.**
|
|
10
|
+
* D-39's default is to skip, and honouring needs a written reason: this is
|
|
11
|
+
* referential integrity, not a surface. A switched-off module still owns the
|
|
12
|
+
* rows that carry the code, so skipping its descriptor would let an operator
|
|
13
|
+
* delete a language that comes back as a dangling reference the moment the module
|
|
14
|
+
* is switched on again — data loss caused by an action Constitution XVII
|
|
15
|
+
* promises is non-destructive and reversible.
|
|
16
|
+
*
|
|
17
|
+
* `ownerModuleId` is recorded on every descriptor even though this registry
|
|
18
|
+
* does not filter on it, because the alternative is a registry that could not
|
|
19
|
+
* express the decision either way, and because it attributes a 409 to a module.
|
|
20
|
+
*/
|
|
21
|
+
export class LanguageReferenceRegistry {
|
|
22
|
+
descriptors = [];
|
|
23
|
+
register(descriptor) {
|
|
24
|
+
if (!this.descriptors.includes(descriptor)) {
|
|
25
|
+
this.descriptors.push(descriptor);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/** The contributing module of every registered descriptor, in registration order. */
|
|
29
|
+
owners() {
|
|
30
|
+
return this.descriptors.map((d) => d.ownerModuleId);
|
|
31
|
+
}
|
|
32
|
+
async countReferences(code) {
|
|
33
|
+
const counts = await Promise.all(this.descriptors.map(async (d) => ({ descriptor: d, count: await d.countReferences(code) })));
|
|
34
|
+
return counts
|
|
35
|
+
.filter((row) => row.count > 0)
|
|
36
|
+
.map(({ descriptor, count }) => describe(descriptor, code, count));
|
|
37
|
+
}
|
|
38
|
+
async usedCodes() {
|
|
39
|
+
const perDescriptor = await Promise.all(this.descriptors.map(async (d) => ({ descriptor: d, rows: await d.usedCodes() })));
|
|
40
|
+
return perDescriptor.flatMap(({ descriptor, rows }) => rows.map((row) => describe(descriptor, row.code, row.count)));
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function describe(descriptor, code, count) {
|
|
44
|
+
return {
|
|
45
|
+
ownerModuleId: descriptor.ownerModuleId,
|
|
46
|
+
consumer: descriptor.consumer,
|
|
47
|
+
tableName: descriptor.tableName,
|
|
48
|
+
columnName: descriptor.columnName,
|
|
49
|
+
code,
|
|
50
|
+
count,
|
|
51
|
+
blocking: descriptor.blocking,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=language-reference-registry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"language-reference-registry.js","sourceRoot":"","sources":["../../../src/backend/services/language-reference-registry.ts"],"names":[],"mappings":"AAMA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,OAAO,yBAAyB;IACnB,WAAW,GAAoC,EAAE,CAAC;IAEnE,QAAQ,CAAC,UAAyC;QAChD,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC;YAC3C,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;QACpC,CAAC;IACH,CAAC;IAED,qFAAqF;IACrF,MAAM;QACJ,OAAO,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC;IACtD,CAAC;IAED,KAAK,CAAC,eAAe,CAAC,IAAY;QAChC,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,GAAG,CAC9B,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,KAAK,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC,eAAe,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAC7F,CAAC;QACF,OAAO,MAAM;aACV,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,KAAK,GAAG,CAAC,CAAC;aAC9B,GAAG,CAAC,CAAC,EAAE,UAAU,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC,QAAQ,CAAC,UAAU,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC;IACvE,CAAC;IAED,KAAK,CAAC,SAAS;QACb,MAAM,aAAa,GAAG,MAAM,OAAO,CAAC,GAAG,CACrC,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,KAAK,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC,CAClF,CAAC;QACF,OAAO,aAAa,CAAC,OAAO,CAAC,CAAC,EAAE,UAAU,EAAE,IAAI,EAAE,EAAE,EAAE,CACpD,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,QAAQ,CAAC,UAAU,EAAE,GAAG,CAAC,IAAI,EAAE,GAAG,CAAC,KAAK,CAAC,CAAC,CAC7D,CAAC;IACJ,CAAC;CACF;AAED,SAAS,QAAQ,CACf,UAAyC,EACzC,IAAY,EACZ,KAAa;IAEb,OAAO;QACL,aAAa,EAAE,UAAU,CAAC,aAAa;QACvC,QAAQ,EAAE,UAAU,CAAC,QAAQ;QAC7B,SAAS,EAAE,UAAU,CAAC,SAAS;QAC/B,UAAU,EAAE,UAAU,CAAC,UAAU;QACjC,IAAI;QACJ,KAAK;QACL,QAAQ,EAAE,UAAU,CAAC,QAAQ;KAC9B,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { LanguageSeedPort } from '@endora-commerce/contracts';
|
|
2
|
+
import type { EntityManager } from '@mikro-orm/postgresql';
|
|
3
|
+
/**
|
|
4
|
+
* The seeding face of `languages` (feature 077, D-87 drain).
|
|
5
|
+
*
|
|
6
|
+
* One job: fill the empty `native_label` that migration 038 left behind on the
|
|
7
|
+
* two languages migration 012 created. `dictionaries` knows the labels — it
|
|
8
|
+
* ships the reference catalogue — and this module owns the table, so before
|
|
9
|
+
* this port the reconciler wrote it with an `update "languages" …` naming
|
|
10
|
+
* another module's table in a string no import-level check can see.
|
|
11
|
+
*
|
|
12
|
+
* The `and "native_label" = ''` predicate is the idempotency: once an operator
|
|
13
|
+
* (or an earlier boot) has set a label, a re-run matches no row and reverts
|
|
14
|
+
* nothing.
|
|
15
|
+
*/
|
|
16
|
+
export declare class LanguageSeedService implements LanguageSeedPort {
|
|
17
|
+
private readonly emFactory;
|
|
18
|
+
constructor(emFactory: () => EntityManager);
|
|
19
|
+
backfillNativeLabels(rows: readonly {
|
|
20
|
+
code: string;
|
|
21
|
+
nativeLabel: string;
|
|
22
|
+
}[]): Promise<number>;
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=language-seed-service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"language-seed-service.d.ts","sourceRoot":"","sources":["../../../src/backend/services/language-seed-service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAC;AACnE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAE3D;;;;;;;;;;;;GAYG;AACH,qBAAa,mBAAoB,YAAW,gBAAgB;IAC9C,OAAO,CAAC,QAAQ,CAAC,SAAS;gBAAT,SAAS,EAAE,MAAM,aAAa;IAErD,oBAAoB,CACxB,IAAI,EAAE,SAAS;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE,EAAE,GACrD,OAAO,CAAC,MAAM,CAAC;CAqBnB"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The seeding face of `languages` (feature 077, D-87 drain).
|
|
3
|
+
*
|
|
4
|
+
* One job: fill the empty `native_label` that migration 038 left behind on the
|
|
5
|
+
* two languages migration 012 created. `dictionaries` knows the labels — it
|
|
6
|
+
* ships the reference catalogue — and this module owns the table, so before
|
|
7
|
+
* this port the reconciler wrote it with an `update "languages" …` naming
|
|
8
|
+
* another module's table in a string no import-level check can see.
|
|
9
|
+
*
|
|
10
|
+
* The `and "native_label" = ''` predicate is the idempotency: once an operator
|
|
11
|
+
* (or an earlier boot) has set a label, a re-run matches no row and reverts
|
|
12
|
+
* nothing.
|
|
13
|
+
*/
|
|
14
|
+
export class LanguageSeedService {
|
|
15
|
+
emFactory;
|
|
16
|
+
constructor(emFactory) {
|
|
17
|
+
this.emFactory = emFactory;
|
|
18
|
+
}
|
|
19
|
+
async backfillNativeLabels(rows) {
|
|
20
|
+
const em = this.emFactory();
|
|
21
|
+
let filled = 0;
|
|
22
|
+
for (const row of rows) {
|
|
23
|
+
const before = (await em.execute('select "native_label" from "languages" where "code" = ?', [
|
|
24
|
+
row.code,
|
|
25
|
+
]));
|
|
26
|
+
if (before.length === 0)
|
|
27
|
+
continue;
|
|
28
|
+
if (before[0]?.native_label !== '')
|
|
29
|
+
continue;
|
|
30
|
+
// command-coverage-ignore: idempotent lifecycle reconciler/seed — a system-
|
|
31
|
+
// owned backfill of a column the migration left empty, with no actor behind
|
|
32
|
+
// it. The `= ''` predicate means it can only ever fill a blank, never
|
|
33
|
+
// overwrite an operator's label, so there is nothing to audit or undo.
|
|
34
|
+
await em.execute(`update "languages" set "native_label" = ? where "code" = ? and "native_label" = ''`, [row.nativeLabel, row.code]);
|
|
35
|
+
filled += 1;
|
|
36
|
+
}
|
|
37
|
+
return filled;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=language-seed-service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"language-seed-service.js","sourceRoot":"","sources":["../../../src/backend/services/language-seed-service.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;GAYG;AACH,MAAM,OAAO,mBAAmB;IACD;IAA7B,YAA6B,SAA8B;QAA9B,cAAS,GAAT,SAAS,CAAqB;IAAG,CAAC;IAE/D,KAAK,CAAC,oBAAoB,CACxB,IAAsD;QAEtD,MAAM,EAAE,GAAG,IAAI,CAAC,SAAS,EAAE,CAAC;QAC5B,IAAI,MAAM,GAAG,CAAC,CAAC;QACf,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;YACvB,MAAM,MAAM,GAAG,CAAC,MAAM,EAAE,CAAC,OAAO,CAAC,yDAAyD,EAAE;gBAC1F,GAAG,CAAC,IAAI;aACT,CAAC,CAAoC,CAAC;YACvC,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;gBAAE,SAAS;YAClC,IAAI,MAAM,CAAC,CAAC,CAAC,EAAE,YAAY,KAAK,EAAE;gBAAE,SAAS;YAC7C,4EAA4E;YAC5E,4EAA4E;YAC5E,sEAAsE;YACtE,uEAAuE;YACvE,MAAM,EAAE,CAAC,OAAO,CACd,oFAAoF,EACpF,CAAC,GAAG,CAAC,WAAW,EAAE,GAAG,CAAC,IAAI,CAAC,CAC5B,CAAC;YACF,MAAM,IAAI,CAAC,CAAC;QACd,CAAC;QACD,OAAO,MAAM,CAAC;IAChB,CAAC;CACF"}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { EntityManager } from '@mikro-orm/postgresql';
|
|
2
|
+
import { type DictionaryReference, type DictionaryReferenceRegistryPort } from '@endora-commerce/contracts';
|
|
3
|
+
import { Language } from '../entities/language.entity.js';
|
|
4
|
+
import type { AuditPort } from '@endora-commerce/platform/kernel';
|
|
5
|
+
/**
|
|
6
|
+
* LanguageService — admin CRUD over the languages pool.
|
|
7
|
+
*
|
|
8
|
+
* Originally introduced by feature 004 (T238 / FR-105) for the minimal
|
|
9
|
+
* languages table. Extended by feature 017 to cover the new columns
|
|
10
|
+
* (`nativeLabel`, `isRtl`, `fallbackCode`) and the spec invariants:
|
|
11
|
+
*
|
|
12
|
+
* - exactly one default at a time (partial unique index, transactional
|
|
13
|
+
* promote+demote).
|
|
14
|
+
* - at-least-one-active (the only active row cannot be deactivated).
|
|
15
|
+
* - default-cannot-be-deactivated.
|
|
16
|
+
* - fallback-cycle guard (the `code → fallback_code → ...` chain must
|
|
17
|
+
* terminate without revisiting any node).
|
|
18
|
+
* - FK-protected hard-delete: refused while any consumer references the
|
|
19
|
+
* language (sales channels' default_language + languages JSONB,
|
|
20
|
+
* megamenu bindings, blog post / category languages, CMS page
|
|
21
|
+
* languages).
|
|
22
|
+
*/
|
|
23
|
+
export declare class LanguageService {
|
|
24
|
+
#private;
|
|
25
|
+
private readonly emFactory;
|
|
26
|
+
private readonly invalidateDictionaryCache?;
|
|
27
|
+
private readonly auditLog?;
|
|
28
|
+
/**
|
|
29
|
+
* Resolved per call rather than captured: the registry is a singleton this
|
|
30
|
+
* module owns, but the accessor keeps the constructor honest for the tests
|
|
31
|
+
* that build the service without one.
|
|
32
|
+
*/
|
|
33
|
+
private readonly referenceRegistry?;
|
|
34
|
+
constructor(emFactory: () => EntityManager, invalidateDictionaryCache?: (() => Promise<void>) | undefined, auditLog?: AuditPort | undefined,
|
|
35
|
+
/**
|
|
36
|
+
* Resolved per call rather than captured: the registry is a singleton this
|
|
37
|
+
* module owns, but the accessor keeps the constructor honest for the tests
|
|
38
|
+
* that build the service without one.
|
|
39
|
+
*/
|
|
40
|
+
referenceRegistry?: (() => DictionaryReferenceRegistryPort) | undefined);
|
|
41
|
+
list(): Promise<Language[]>;
|
|
42
|
+
listActive(): Promise<Language[]>;
|
|
43
|
+
getDefault(): Promise<Language | null>;
|
|
44
|
+
getByCode(code: string): Promise<Language | null>;
|
|
45
|
+
create(input: {
|
|
46
|
+
code: string;
|
|
47
|
+
label: string;
|
|
48
|
+
nativeLabel: string;
|
|
49
|
+
isRtl?: boolean;
|
|
50
|
+
fallbackCode?: string | null;
|
|
51
|
+
isActive?: boolean;
|
|
52
|
+
sortOrder?: number;
|
|
53
|
+
}): Promise<Language>;
|
|
54
|
+
/**
|
|
55
|
+
* Legacy upsert kept for the existing `/api/v1/admin/languages/:code`
|
|
56
|
+
* endpoint. Sets only the legacy fields. The dictionary admin surface
|
|
57
|
+
* uses `update()` for the extended fields.
|
|
58
|
+
*/
|
|
59
|
+
upsert(input: {
|
|
60
|
+
code: string;
|
|
61
|
+
label: string;
|
|
62
|
+
isActive?: boolean;
|
|
63
|
+
sortOrder?: number;
|
|
64
|
+
}): Promise<Language>;
|
|
65
|
+
/**
|
|
66
|
+
* Extended update used by the Dictionary admin surface (feature 017).
|
|
67
|
+
* Supports every column on the Language entity and enforces the spec
|
|
68
|
+
* invariants (default-cannot-be-deactivated, at-least-one-active,
|
|
69
|
+
* fallback-cycle).
|
|
70
|
+
*/
|
|
71
|
+
update(code: string, input: {
|
|
72
|
+
label?: string;
|
|
73
|
+
nativeLabel?: string;
|
|
74
|
+
isRtl?: boolean;
|
|
75
|
+
fallbackCode?: string | null;
|
|
76
|
+
isActive?: boolean;
|
|
77
|
+
sortOrder?: number;
|
|
78
|
+
}): Promise<Language>;
|
|
79
|
+
setDefault(code: string): Promise<Language>;
|
|
80
|
+
/**
|
|
81
|
+
* Every consumer reference to `code`, across the platform (feature 077, D-87).
|
|
82
|
+
*
|
|
83
|
+
* This used to be six hand-written `count(*)` statements naming
|
|
84
|
+
* `sales_channels`, `megamenu_bindings`, `blog_post_languages`,
|
|
85
|
+
* `blog_category_languages` and `cms_pages` — five tables this module does not
|
|
86
|
+
* own, in strings no import-level boundary check can see. Four of them are
|
|
87
|
+
* contributed descriptors now (`languageReferenceRegistry`); the fifth is the
|
|
88
|
+
* kernel's channel table, which this module may read through the ORM because
|
|
89
|
+
* the kernel is not a module (D-32).
|
|
90
|
+
*
|
|
91
|
+
* Channels are counted in memory rather than in SQL: a deployment has tens of
|
|
92
|
+
* them, both questions are about the same rows, and `languages` is a JSON
|
|
93
|
+
* array whose containment test was the reason the statement existed at all.
|
|
94
|
+
*/
|
|
95
|
+
countDependents(code: string): Promise<DictionaryReference[]>;
|
|
96
|
+
remove(code: string): Promise<void>;
|
|
97
|
+
private assertNoCycle;
|
|
98
|
+
}
|
|
99
|
+
//# sourceMappingURL=language-service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"language-service.d.ts","sourceRoot":"","sources":["../../../src/backend/services/language-service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,EAEL,KAAK,mBAAmB,EACxB,KAAK,+BAA+B,EACrC,MAAM,4BAA4B,CAAC;AAGpC,OAAO,EAAE,QAAQ,EAAE,MAAM,gCAAgC,CAAC;AAE1D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kCAAkC,CAAC;AAElE;;;;;;;;;;;;;;;;;GAiBG;AACH,qBAAa,eAAe;;IAExB,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,yBAAyB,CAAC;IAC3C,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC;IAC1B;;;;OAIG;IACH,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAC;gBARlB,SAAS,EAAE,MAAM,aAAa,EAC9B,yBAAyB,CAAC,GAAE,MAAM,OAAO,CAAC,IAAI,CAAC,aAAA,EAC/C,QAAQ,CAAC,EAAE,SAAS,YAAA;IACrC;;;;OAIG;IACc,iBAAiB,CAAC,GAAE,MAAM,+BAA+B,aAAA;IAqBtE,IAAI,IAAI,OAAO,CAAC,QAAQ,EAAE,CAAC;IAK3B,UAAU,IAAI,OAAO,CAAC,QAAQ,EAAE,CAAC;IAOjC,UAAU,IAAI,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC;IAKtC,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC;IAKjD,MAAM,CAAC,KAAK,EAAE;QAClB,IAAI,EAAE,MAAM,CAAC;QACb,KAAK,EAAE,MAAM,CAAC;QACd,WAAW,EAAE,MAAM,CAAC;QACpB,KAAK,CAAC,EAAE,OAAO,CAAC;QAChB,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAC7B,QAAQ,CAAC,EAAE,OAAO,CAAC;QACnB,SAAS,CAAC,EAAE,MAAM,CAAC;KACpB,GAAG,OAAO,CAAC,QAAQ,CAAC;IAoCrB;;;;OAIG;IACG,MAAM,CAAC,KAAK,EAAE;QAClB,IAAI,EAAE,MAAM,CAAC;QACb,KAAK,EAAE,MAAM,CAAC;QACd,QAAQ,CAAC,EAAE,OAAO,CAAC;QACnB,SAAS,CAAC,EAAE,MAAM,CAAC;KACpB,GAAG,OAAO,CAAC,QAAQ,CAAC;IA8CrB;;;;;OAKG;IACG,MAAM,CACV,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE;QACL,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,KAAK,CAAC,EAAE,OAAO,CAAC;QAChB,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAC7B,QAAQ,CAAC,EAAE,OAAO,CAAC;QACnB,SAAS,CAAC,EAAE,MAAM,CAAC;KACpB,GACA,OAAO,CAAC,QAAQ,CAAC;IAwDd,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC;IA4BjD;;;;;;;;;;;;;;OAcG;IACG,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,EAAE,CAAC;IAmC7D,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;YA+B3B,aAAa;CA0B5B"}
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import { ERROR_CODES, } from '@endora-commerce/contracts';
|
|
2
|
+
import { SalesChannel } from '@endora-commerce/platform/kernel';
|
|
3
|
+
import { HttpError } from '@endora-commerce/platform/http';
|
|
4
|
+
import { Language } from '../entities/language.entity.js';
|
|
5
|
+
import { recordAuditFromContext } from '@endora-commerce/platform/commands';
|
|
6
|
+
/**
|
|
7
|
+
* LanguageService — admin CRUD over the languages pool.
|
|
8
|
+
*
|
|
9
|
+
* Originally introduced by feature 004 (T238 / FR-105) for the minimal
|
|
10
|
+
* languages table. Extended by feature 017 to cover the new columns
|
|
11
|
+
* (`nativeLabel`, `isRtl`, `fallbackCode`) and the spec invariants:
|
|
12
|
+
*
|
|
13
|
+
* - exactly one default at a time (partial unique index, transactional
|
|
14
|
+
* promote+demote).
|
|
15
|
+
* - at-least-one-active (the only active row cannot be deactivated).
|
|
16
|
+
* - default-cannot-be-deactivated.
|
|
17
|
+
* - fallback-cycle guard (the `code → fallback_code → ...` chain must
|
|
18
|
+
* terminate without revisiting any node).
|
|
19
|
+
* - FK-protected hard-delete: refused while any consumer references the
|
|
20
|
+
* language (sales channels' default_language + languages JSONB,
|
|
21
|
+
* megamenu bindings, blog post / category languages, CMS page
|
|
22
|
+
* languages).
|
|
23
|
+
*/
|
|
24
|
+
export class LanguageService {
|
|
25
|
+
emFactory;
|
|
26
|
+
invalidateDictionaryCache;
|
|
27
|
+
auditLog;
|
|
28
|
+
referenceRegistry;
|
|
29
|
+
constructor(emFactory, invalidateDictionaryCache, auditLog,
|
|
30
|
+
/**
|
|
31
|
+
* Resolved per call rather than captured: the registry is a singleton this
|
|
32
|
+
* module owns, but the accessor keeps the constructor honest for the tests
|
|
33
|
+
* that build the service without one.
|
|
34
|
+
*/
|
|
35
|
+
referenceRegistry) {
|
|
36
|
+
this.emFactory = emFactory;
|
|
37
|
+
this.invalidateDictionaryCache = invalidateDictionaryCache;
|
|
38
|
+
this.auditLog = auditLog;
|
|
39
|
+
this.referenceRegistry = referenceRegistry;
|
|
40
|
+
}
|
|
41
|
+
#audit(em, action, objectId, stateBefore, stateAfter) {
|
|
42
|
+
if (this.auditLog) {
|
|
43
|
+
recordAuditFromContext(this.auditLog, em, {
|
|
44
|
+
action,
|
|
45
|
+
objectType: 'language',
|
|
46
|
+
objectId,
|
|
47
|
+
stateBefore,
|
|
48
|
+
stateAfter,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
async list() {
|
|
53
|
+
const em = this.emFactory();
|
|
54
|
+
return em.find(Language, {}, { orderBy: { sortOrder: 'asc', code: 'asc' } });
|
|
55
|
+
}
|
|
56
|
+
async listActive() {
|
|
57
|
+
const em = this.emFactory();
|
|
58
|
+
// `code` tiebreak keeps the order deterministic when several languages
|
|
59
|
+
// share a sortOrder (e.g. the seed default 0) — matches list() above.
|
|
60
|
+
return em.find(Language, { isActive: true }, { orderBy: { sortOrder: 'asc', code: 'asc' } });
|
|
61
|
+
}
|
|
62
|
+
async getDefault() {
|
|
63
|
+
const em = this.emFactory();
|
|
64
|
+
return em.findOne(Language, { isDefault: true });
|
|
65
|
+
}
|
|
66
|
+
async getByCode(code) {
|
|
67
|
+
const em = this.emFactory();
|
|
68
|
+
return em.findOne(Language, { code });
|
|
69
|
+
}
|
|
70
|
+
async create(input) {
|
|
71
|
+
const em = this.emFactory();
|
|
72
|
+
const existing = await em.findOne(Language, { code: input.code });
|
|
73
|
+
if (existing) {
|
|
74
|
+
throw new HttpError(409, ERROR_CODES.VALIDATION_FAILED, `Language ${input.code} already exists.`);
|
|
75
|
+
}
|
|
76
|
+
if (input.fallbackCode !== undefined && input.fallbackCode !== null) {
|
|
77
|
+
if (input.fallbackCode === input.code) {
|
|
78
|
+
throw new HttpError(409, ERROR_CODES.DICTIONARY_FALLBACK_CYCLE, 'A language cannot fall back to itself.');
|
|
79
|
+
}
|
|
80
|
+
await this.assertNoCycle(em, input.code, input.fallbackCode);
|
|
81
|
+
}
|
|
82
|
+
const row = em.create(Language, {
|
|
83
|
+
code: input.code,
|
|
84
|
+
label: input.label,
|
|
85
|
+
nativeLabel: input.nativeLabel,
|
|
86
|
+
...(input.isRtl !== undefined ? { isRtl: input.isRtl } : {}),
|
|
87
|
+
...(input.fallbackCode !== undefined ? { fallbackCode: input.fallbackCode } : {}),
|
|
88
|
+
...(input.isActive !== undefined ? { isActive: input.isActive } : {}),
|
|
89
|
+
...(input.sortOrder !== undefined ? { sortOrder: input.sortOrder } : {}),
|
|
90
|
+
});
|
|
91
|
+
em.persist(row);
|
|
92
|
+
this.#audit(em, 'language.create', row.code, null, { label: row.label });
|
|
93
|
+
await em.flush();
|
|
94
|
+
await this.invalidateDictionaryCache?.();
|
|
95
|
+
return row;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Legacy upsert kept for the existing `/api/v1/admin/languages/:code`
|
|
99
|
+
* endpoint. Sets only the legacy fields. The dictionary admin surface
|
|
100
|
+
* uses `update()` for the extended fields.
|
|
101
|
+
*/
|
|
102
|
+
async upsert(input) {
|
|
103
|
+
const em = this.emFactory();
|
|
104
|
+
const existing = await em.findOne(Language, { code: input.code });
|
|
105
|
+
if (existing) {
|
|
106
|
+
// Default-cannot-be-deactivated invariant.
|
|
107
|
+
if (input.isActive === false && existing.isDefault) {
|
|
108
|
+
throw new HttpError(409, ERROR_CODES.DICTIONARY_DEFAULT_CANNOT_BE_DEACTIVATED, 'Cannot deactivate the default language. Promote a different language first.');
|
|
109
|
+
}
|
|
110
|
+
if (input.isActive === false && existing.isActive) {
|
|
111
|
+
const otherActive = await em.count(Language, {
|
|
112
|
+
isActive: true,
|
|
113
|
+
code: { $ne: input.code },
|
|
114
|
+
});
|
|
115
|
+
if (otherActive === 0) {
|
|
116
|
+
throw new HttpError(409, ERROR_CODES.DICTIONARY_LAST_ACTIVE_ENTRY, 'At least one language must remain active.');
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
existing.label = input.label;
|
|
120
|
+
if (input.isActive !== undefined)
|
|
121
|
+
existing.isActive = input.isActive;
|
|
122
|
+
if (input.sortOrder !== undefined)
|
|
123
|
+
existing.sortOrder = input.sortOrder;
|
|
124
|
+
this.#audit(em, 'language.upsert', existing.code, null, { label: existing.label });
|
|
125
|
+
await em.flush();
|
|
126
|
+
await this.invalidateDictionaryCache?.();
|
|
127
|
+
return existing;
|
|
128
|
+
}
|
|
129
|
+
const row = em.create(Language, {
|
|
130
|
+
code: input.code,
|
|
131
|
+
label: input.label,
|
|
132
|
+
...(input.isActive !== undefined ? { isActive: input.isActive } : {}),
|
|
133
|
+
...(input.sortOrder !== undefined ? { sortOrder: input.sortOrder } : {}),
|
|
134
|
+
});
|
|
135
|
+
em.persist(row);
|
|
136
|
+
this.#audit(em, 'language.upsert', row.code, null, { label: row.label });
|
|
137
|
+
await em.flush();
|
|
138
|
+
await this.invalidateDictionaryCache?.();
|
|
139
|
+
return row;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Extended update used by the Dictionary admin surface (feature 017).
|
|
143
|
+
* Supports every column on the Language entity and enforces the spec
|
|
144
|
+
* invariants (default-cannot-be-deactivated, at-least-one-active,
|
|
145
|
+
* fallback-cycle).
|
|
146
|
+
*/
|
|
147
|
+
async update(code, input) {
|
|
148
|
+
const em = this.emFactory();
|
|
149
|
+
const existing = await em.findOne(Language, { code });
|
|
150
|
+
if (!existing) {
|
|
151
|
+
throw new HttpError(404, ERROR_CODES.DICTIONARY_ENTRY_NOT_FOUND, `Language ${code} not found.`);
|
|
152
|
+
}
|
|
153
|
+
if (input.isActive === false && existing.isDefault) {
|
|
154
|
+
throw new HttpError(409, ERROR_CODES.DICTIONARY_DEFAULT_CANNOT_BE_DEACTIVATED, 'Cannot deactivate the default language. Promote a different language first.');
|
|
155
|
+
}
|
|
156
|
+
if (input.isActive === false && existing.isActive) {
|
|
157
|
+
const otherActive = await em.count(Language, {
|
|
158
|
+
isActive: true,
|
|
159
|
+
code: { $ne: code },
|
|
160
|
+
});
|
|
161
|
+
if (otherActive === 0) {
|
|
162
|
+
throw new HttpError(409, ERROR_CODES.DICTIONARY_LAST_ACTIVE_ENTRY, 'At least one language must remain active.');
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if (input.fallbackCode !== undefined && input.fallbackCode !== null) {
|
|
166
|
+
if (input.fallbackCode === code) {
|
|
167
|
+
throw new HttpError(409, ERROR_CODES.DICTIONARY_FALLBACK_CYCLE, 'A language cannot fall back to itself.');
|
|
168
|
+
}
|
|
169
|
+
await this.assertNoCycle(em, code, input.fallbackCode);
|
|
170
|
+
}
|
|
171
|
+
if (input.label !== undefined)
|
|
172
|
+
existing.label = input.label;
|
|
173
|
+
if (input.nativeLabel !== undefined)
|
|
174
|
+
existing.nativeLabel = input.nativeLabel;
|
|
175
|
+
if (input.isRtl !== undefined)
|
|
176
|
+
existing.isRtl = input.isRtl;
|
|
177
|
+
if (input.fallbackCode !== undefined)
|
|
178
|
+
existing.fallbackCode = input.fallbackCode;
|
|
179
|
+
if (input.isActive !== undefined)
|
|
180
|
+
existing.isActive = input.isActive;
|
|
181
|
+
if (input.sortOrder !== undefined)
|
|
182
|
+
existing.sortOrder = input.sortOrder;
|
|
183
|
+
this.#audit(em, 'language.update', existing.code, null, { label: existing.label, isActive: existing.isActive });
|
|
184
|
+
await em.flush();
|
|
185
|
+
await this.invalidateDictionaryCache?.();
|
|
186
|
+
return existing;
|
|
187
|
+
}
|
|
188
|
+
async setDefault(code) {
|
|
189
|
+
const em = this.emFactory();
|
|
190
|
+
const target = await em.findOne(Language, { code });
|
|
191
|
+
if (!target) {
|
|
192
|
+
throw new HttpError(404, ERROR_CODES.NOT_FOUND, `Language ${code} not found.`);
|
|
193
|
+
}
|
|
194
|
+
if (!target.isActive) {
|
|
195
|
+
throw new HttpError(409, ERROR_CODES.VALIDATION_FAILED, 'Cannot mark an inactive language as default.');
|
|
196
|
+
}
|
|
197
|
+
if (target.isDefault)
|
|
198
|
+
return target;
|
|
199
|
+
// Demote-then-promote inside the EM's current tx scope (em.transactional
|
|
200
|
+
// would fork into a separate tx that commits independently — breaks the
|
|
201
|
+
// test fork+rollback pattern and the cross-module composition root's
|
|
202
|
+
// outer-tx semantics).
|
|
203
|
+
await em.nativeUpdate(Language, { isDefault: true }, { isDefault: false });
|
|
204
|
+
await em.nativeUpdate(Language, { code }, { isDefault: true });
|
|
205
|
+
target.isDefault = true;
|
|
206
|
+
this.#audit(em, 'language.set_default', code, null, { isDefault: true });
|
|
207
|
+
await em.flush();
|
|
208
|
+
await this.invalidateDictionaryCache?.();
|
|
209
|
+
return target;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Every consumer reference to `code`, across the platform (feature 077, D-87).
|
|
213
|
+
*
|
|
214
|
+
* This used to be six hand-written `count(*)` statements naming
|
|
215
|
+
* `sales_channels`, `megamenu_bindings`, `blog_post_languages`,
|
|
216
|
+
* `blog_category_languages` and `cms_pages` — five tables this module does not
|
|
217
|
+
* own, in strings no import-level boundary check can see. Four of them are
|
|
218
|
+
* contributed descriptors now (`languageReferenceRegistry`); the fifth is the
|
|
219
|
+
* kernel's channel table, which this module may read through the ORM because
|
|
220
|
+
* the kernel is not a module (D-32).
|
|
221
|
+
*
|
|
222
|
+
* Channels are counted in memory rather than in SQL: a deployment has tens of
|
|
223
|
+
* them, both questions are about the same rows, and `languages` is a JSON
|
|
224
|
+
* array whose containment test was the reason the statement existed at all.
|
|
225
|
+
*/
|
|
226
|
+
async countDependents(code) {
|
|
227
|
+
const channels = await this.emFactory().find(SalesChannel, {});
|
|
228
|
+
const references = [];
|
|
229
|
+
const asDefault = channels.filter((channel) => channel.defaultLanguage === code).length;
|
|
230
|
+
if (asDefault > 0) {
|
|
231
|
+
references.push({
|
|
232
|
+
ownerModuleId: 'sales_channels',
|
|
233
|
+
consumer: 'sales_channels',
|
|
234
|
+
tableName: 'sales_channels',
|
|
235
|
+
columnName: 'default_language',
|
|
236
|
+
code,
|
|
237
|
+
count: asDefault,
|
|
238
|
+
blocking: true,
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
const listed = channels.filter((channel) => channel.languages.includes(code)).length;
|
|
242
|
+
if (listed > 0) {
|
|
243
|
+
references.push({
|
|
244
|
+
ownerModuleId: 'sales_channels',
|
|
245
|
+
consumer: 'sales_channels',
|
|
246
|
+
tableName: 'sales_channels',
|
|
247
|
+
columnName: 'languages[]',
|
|
248
|
+
code,
|
|
249
|
+
count: listed,
|
|
250
|
+
blocking: true,
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
const registry = this.referenceRegistry?.();
|
|
254
|
+
if (registry)
|
|
255
|
+
references.push(...(await registry.countReferences(code)));
|
|
256
|
+
return references;
|
|
257
|
+
}
|
|
258
|
+
async remove(code) {
|
|
259
|
+
const em = this.emFactory();
|
|
260
|
+
const row = await em.findOne(Language, { code });
|
|
261
|
+
if (!row)
|
|
262
|
+
return;
|
|
263
|
+
if (row.isDefault) {
|
|
264
|
+
throw new HttpError(409, ERROR_CODES.DICTIONARY_DEFAULT_CANNOT_BE_DEACTIVATED, 'Cannot remove the default language.');
|
|
265
|
+
}
|
|
266
|
+
const dependents = await this.countDependents(code);
|
|
267
|
+
// Which references refuse the delete is the contributing module's call,
|
|
268
|
+
// carried on the descriptor: a column its own foreign key blanks on delete
|
|
269
|
+
// is reported but does not block.
|
|
270
|
+
const total = dependents
|
|
271
|
+
.filter((reference) => reference.blocking)
|
|
272
|
+
.reduce((sum, reference) => sum + reference.count, 0);
|
|
273
|
+
if (total > 0) {
|
|
274
|
+
throw new HttpError(409, ERROR_CODES.DICTIONARY_ENTRY_HAS_DEPENDENTS, `Language ${code} cannot be deleted because ${total} consumer reference(s) exist.`, [{ path: 'consumers', issue: JSON.stringify(dependents) }]);
|
|
275
|
+
}
|
|
276
|
+
this.#audit(em, 'language.delete', row.code, { label: row.label }, null);
|
|
277
|
+
await em.removeAndFlush(row);
|
|
278
|
+
await this.invalidateDictionaryCache?.();
|
|
279
|
+
}
|
|
280
|
+
async assertNoCycle(em, startCode, proposedFallback) {
|
|
281
|
+
// Walk the existing graph from `proposedFallback` and ensure we never
|
|
282
|
+
// arrive back at `startCode`. Bound the walk by the registry size so a
|
|
283
|
+
// pathological dataset cannot loop.
|
|
284
|
+
const visited = new Set([startCode]);
|
|
285
|
+
let cursor = proposedFallback;
|
|
286
|
+
let steps = 0;
|
|
287
|
+
const maxSteps = 32;
|
|
288
|
+
while (cursor && steps < maxSteps) {
|
|
289
|
+
if (visited.has(cursor)) {
|
|
290
|
+
throw new HttpError(409, ERROR_CODES.DICTIONARY_FALLBACK_CYCLE, `Setting fallback_code to ${proposedFallback} would create a cycle through ${cursor}.`);
|
|
291
|
+
}
|
|
292
|
+
visited.add(cursor);
|
|
293
|
+
const next = await em.findOne(Language, { code: cursor });
|
|
294
|
+
cursor = next?.fallbackCode ?? null;
|
|
295
|
+
steps += 1;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
//# sourceMappingURL=language-service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"language-service.js","sourceRoot":"","sources":["../../../src/backend/services/language-service.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { LanguageService } from './language-service.js';
|
|
2
|
+
/**
|
|
3
|
+
* LocaleService — the small chunk of behaviour FR-105 calls
|
|
4
|
+
* "translation-fallback middleware".
|
|
5
|
+
*
|
|
6
|
+
* Two responsibilities:
|
|
7
|
+
* 1. `pickLocalizedValue(record, requestedLocale, defaultLocale?)` — given a
|
|
8
|
+
* `Record<localeCode, string>` (the multilingual JSONB shape used by
|
|
9
|
+
* Product / Category / CMS), return the best available string. Order:
|
|
10
|
+
* requested → default → first present → empty string.
|
|
11
|
+
* 2. `resolveRequestLocale(acceptLanguageHeader)` — pick a supported
|
|
12
|
+
* locale for the current request, with the admin-configured default
|
|
13
|
+
* as the last-resort fallback.
|
|
14
|
+
*
|
|
15
|
+
* The default locale is read once per resolver and cached for the lifetime
|
|
16
|
+
* of the service instance to avoid hitting the DB on every request; it is
|
|
17
|
+
* refreshed on `invalidateDefault()` after admin mutations.
|
|
18
|
+
*/
|
|
19
|
+
export declare class LocaleService {
|
|
20
|
+
private readonly languageService;
|
|
21
|
+
private cachedDefaultCode;
|
|
22
|
+
private cachedDefaultLoadedAt;
|
|
23
|
+
private readonly cacheTtlMs;
|
|
24
|
+
constructor(languageService: LanguageService);
|
|
25
|
+
pickLocalizedValue(record: Record<string, string> | null | undefined, requestedLocale: string, defaultLocale?: string): string;
|
|
26
|
+
getDefaultLocale(): Promise<string>;
|
|
27
|
+
invalidateDefault(): void;
|
|
28
|
+
/**
|
|
29
|
+
* Resolve a single locale string from an Accept-Language header against
|
|
30
|
+
* the active language pool. Returns the configured default when nothing
|
|
31
|
+
* matches.
|
|
32
|
+
*/
|
|
33
|
+
resolveRequestLocale(acceptLanguageHeader: string | undefined, activeLocales: string[]): Promise<string>;
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=locale-service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"locale-service.d.ts","sourceRoot":"","sources":["../../../src/backend/services/locale-service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAE7D;;;;;;;;;;;;;;;;GAgBG;AACH,qBAAa,aAAa;IAKZ,OAAO,CAAC,QAAQ,CAAC,eAAe;IAJ5C,OAAO,CAAC,iBAAiB,CAAuB;IAChD,OAAO,CAAC,qBAAqB,CAAK;IAClC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAU;gBAER,eAAe,EAAE,eAAe;IAE7D,kBAAkB,CAChB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,GAAG,SAAS,EACjD,eAAe,EAAE,MAAM,EACvB,aAAa,CAAC,EAAE,MAAM,GACrB,MAAM;IAQH,gBAAgB,IAAI,OAAO,CAAC,MAAM,CAAC;IAWzC,iBAAiB,IAAI,IAAI;IAKzB;;;;OAIG;IACG,oBAAoB,CACxB,oBAAoB,EAAE,MAAM,GAAG,SAAS,EACxC,aAAa,EAAE,MAAM,EAAE,GACtB,OAAO,CAAC,MAAM,CAAC;CAiBnB"}
|