@_linked/translation 0.5.0 → 0.6.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/lib/esm/authorization.d.ts +12 -2
- package/lib/esm/authorization.js +21 -3
- package/lib/esm/authorization.js.map +1 -1
- package/lib/esm/shapes/TranslationProvider.d.ts +17 -10
- package/lib/esm/shapes/TranslationProvider.js +278 -58
- package/lib/esm/shapes/TranslationProvider.js.map +1 -1
- package/package.json +2 -2
- package/src/authorization.ts +19 -3
- package/src/shapes/TranslationProvider.ts +166 -54
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ShapeProvider } from '@_linked/server-utils/utils/ShapeProvider';
|
|
2
|
+
import { callable, internal } from '@_linked/server-utils/utils/callable';
|
|
2
3
|
import type {
|
|
3
4
|
GlossaryTermRecord,
|
|
4
5
|
TranslationEntryRecord,
|
|
@@ -595,6 +596,8 @@ export async function advanceTranslationReleaseHotfix(data: {
|
|
|
595
596
|
expectedSequence: number;
|
|
596
597
|
hotfixSequence: number;
|
|
597
598
|
manifestHash: string;
|
|
599
|
+
/** When given, the release must belong to this app. */
|
|
600
|
+
appId?: string;
|
|
598
601
|
}): Promise<TranslationReleaseRecord> {
|
|
599
602
|
const id = requireText(data.id, 'id');
|
|
600
603
|
const current = await TranslationRelease.select((release) => [
|
|
@@ -618,6 +621,9 @@ export async function advanceTranslationReleaseHotfix(data: {
|
|
|
618
621
|
.catch(() => null);
|
|
619
622
|
const record = current ? toReleaseRecord(current) : null;
|
|
620
623
|
if (!record) throw new Error(`Translation release "${id}" was not found.`);
|
|
624
|
+
if (data.appId && !sameAppId(record.appId, data.appId)) {
|
|
625
|
+
throw new Error(`Translation release "${id}" was not found.`);
|
|
626
|
+
}
|
|
621
627
|
if (record.hotfixSequence !== data.expectedSequence) {
|
|
622
628
|
throw new Error(
|
|
623
629
|
`Translation release advanced from sequence ${data.expectedSequence} to ${record.hotfixSequence}.`,
|
|
@@ -634,6 +640,65 @@ export async function advanceTranslationReleaseHotfix(data: {
|
|
|
634
640
|
};
|
|
635
641
|
}
|
|
636
642
|
|
|
643
|
+
/**
|
|
644
|
+
* The app a release IRI belongs to, without a trailing `/`. Release IRIs are
|
|
645
|
+
* minted by `createTranslationRelease` as `<appId>/translation/release/<releaseId>`
|
|
646
|
+
* with one trailing `/` of the app id dropped, so an app id `…/app/` and `…/app`
|
|
647
|
+
* give the same release IRI. Compare with `sameAppId`.
|
|
648
|
+
*/
|
|
649
|
+
function releaseAppId(id: unknown): string {
|
|
650
|
+
const clean = typeof id === 'string' ? id.trim() : '';
|
|
651
|
+
const marker = '/translation/release/';
|
|
652
|
+
const at = clean.lastIndexOf(marker);
|
|
653
|
+
if (at <= 0 || at + marker.length >= clean.length) {
|
|
654
|
+
throw new Error('Not a translation release id.');
|
|
655
|
+
}
|
|
656
|
+
return clean.slice(0, at);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
/** Whether two app ids name the same app, as release IRIs see them. */
|
|
660
|
+
function sameAppId(left: string, right: string): boolean {
|
|
661
|
+
return left.replace(/\/$/, '') === right.replace(/\/$/, '');
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
/**
|
|
665
|
+
* The app id a release is stored under (`ofApplication`), when it is the app
|
|
666
|
+
* its IRI names. Falls back to the app id read from the IRI, so a missing
|
|
667
|
+
* release is still checked against that app.
|
|
668
|
+
*/
|
|
669
|
+
async function storedReleaseAppId(id: string, fromIri: string): Promise<string> {
|
|
670
|
+
const row: any = await TranslationRelease.select((release) => [
|
|
671
|
+
release.ofApplication,
|
|
672
|
+
])
|
|
673
|
+
.where((release) => release.equals({ id } as any))
|
|
674
|
+
.one()
|
|
675
|
+
.catch(() => null);
|
|
676
|
+
const stored = iri(row?.ofApplication);
|
|
677
|
+
return stored && sameAppId(stored, fromIri) ? stored : fromIri;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
/** WebID of the signed-in caller; never a client-supplied author. */
|
|
681
|
+
function actorWebIdOf(auth: any): string | undefined {
|
|
682
|
+
const webId =
|
|
683
|
+
auth?.userAccount?.accountOf?.id ??
|
|
684
|
+
auth?.webId ??
|
|
685
|
+
auth?.userAccount?.id ??
|
|
686
|
+
auth?.userAccount;
|
|
687
|
+
return typeof webId === 'string' && webId ? webId : undefined;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/**
|
|
691
|
+
* An error the LINKED server answers with `status` instead of 500. It is
|
|
692
|
+
* matched by shape (`name` + numeric `status`), as `ServerCallError.is` does,
|
|
693
|
+
* so no newer `@_linked/server-utils` is needed to throw one.
|
|
694
|
+
*/
|
|
695
|
+
function translationCallError(status: number, message: string): Error {
|
|
696
|
+
const error = new Error(message) as Error & { status: number };
|
|
697
|
+
error.name = 'ServerCallError';
|
|
698
|
+
error.status = status;
|
|
699
|
+
return error;
|
|
700
|
+
}
|
|
701
|
+
|
|
637
702
|
function requireText(value: string, label: string): string {
|
|
638
703
|
const clean = value?.trim();
|
|
639
704
|
if (!clean) throw new Error(`${label} is required.`);
|
|
@@ -1426,67 +1491,93 @@ export class TranslationProvider extends ShapeProvider {
|
|
|
1426
1491
|
|
|
1427
1492
|
/**
|
|
1428
1493
|
* Authoring gate. Reads `linkedAuth` off the request the LINKED server already
|
|
1429
|
-
* populates
|
|
1430
|
-
*
|
|
1431
|
-
*
|
|
1432
|
-
*
|
|
1433
|
-
*
|
|
1434
|
-
*
|
|
1435
|
-
*
|
|
1494
|
+
* populates, so this stays portable (no Create Now import, AD-G).
|
|
1495
|
+
*
|
|
1496
|
+
* Every method except `getMessages` resolves the caller once, synchronously,
|
|
1497
|
+
* before its first `await`, and uses that value for both the permission check
|
|
1498
|
+
* and the recorded author. Reading the request again after an `await` could
|
|
1499
|
+
* see a different call's request on this shared provider instance.
|
|
1500
|
+
*
|
|
1501
|
+
* Whether the caller may act on `appId` is decided by the host's resolver
|
|
1502
|
+
* (`configureTranslationAuthorization`); with none configured, every
|
|
1503
|
+
* authoring and read action is denied. `getMessages` stays public: it is the
|
|
1504
|
+
* app's runtime string fetch, not an authoring surface.
|
|
1505
|
+
*
|
|
1506
|
+
* `requireActor` and `requireAccess` are `@internal()`: helpers of the
|
|
1507
|
+
* methods below, never dispatched over HTTP.
|
|
1436
1508
|
*/
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1509
|
+
@internal()
|
|
1510
|
+
private requireActor(): string {
|
|
1511
|
+
const auth = (this as any).request?.linkedAuth;
|
|
1512
|
+
if (!auth?.userAccount) {
|
|
1513
|
+
throw translationCallError(
|
|
1514
|
+
401,
|
|
1515
|
+
'Authentication required to read or edit translations.',
|
|
1516
|
+
);
|
|
1441
1517
|
}
|
|
1518
|
+
const webId = actorWebIdOf(auth);
|
|
1519
|
+
if (!webId) {
|
|
1520
|
+
throw new Error('Translation permission required.');
|
|
1521
|
+
}
|
|
1522
|
+
return webId;
|
|
1442
1523
|
}
|
|
1443
1524
|
|
|
1525
|
+
@internal()
|
|
1444
1526
|
private async requireAccess(
|
|
1527
|
+
actorWebId: string,
|
|
1445
1528
|
data: { appId: string; language?: string },
|
|
1446
1529
|
action: TranslationAuthoringAction,
|
|
1447
1530
|
): Promise<void> {
|
|
1448
|
-
|
|
1449
|
-
|
|
1531
|
+
const appId = typeof data?.appId === 'string' ? data.appId.trim() : '';
|
|
1532
|
+
if (!appId) throw new Error('appId is required.');
|
|
1450
1533
|
if (
|
|
1451
|
-
!actorWebId ||
|
|
1452
1534
|
!(await canAuthorTranslation({
|
|
1453
|
-
appId
|
|
1535
|
+
appId,
|
|
1454
1536
|
actorWebId,
|
|
1455
1537
|
action,
|
|
1456
|
-
language: data
|
|
1538
|
+
language: data?.language,
|
|
1457
1539
|
}))
|
|
1458
1540
|
) {
|
|
1459
1541
|
throw new Error(`Translation ${action} permission required.`);
|
|
1460
1542
|
}
|
|
1461
1543
|
}
|
|
1462
1544
|
|
|
1545
|
+
@callable('user')
|
|
1463
1546
|
async listKeys(data: { appId: string }) {
|
|
1464
|
-
this.
|
|
1547
|
+
const actor = this.requireActor();
|
|
1548
|
+
await this.requireAccess(actor, { appId: data?.appId }, 'read');
|
|
1465
1549
|
return listTranslationKeys(data);
|
|
1466
1550
|
}
|
|
1467
1551
|
|
|
1552
|
+
@callable('user')
|
|
1468
1553
|
async listEntries(data: { appId: string }): Promise<TranslationEntryRecord[]> {
|
|
1469
|
-
this.
|
|
1554
|
+
const actor = this.requireActor();
|
|
1555
|
+
await this.requireAccess(actor, { appId: data?.appId }, 'read');
|
|
1470
1556
|
return listTranslationEntries(data);
|
|
1471
1557
|
}
|
|
1472
1558
|
|
|
1559
|
+
@callable('user')
|
|
1473
1560
|
async listKeyVersions(data: {
|
|
1474
1561
|
appId: string;
|
|
1475
1562
|
key?: string;
|
|
1476
1563
|
}): Promise<TranslationKeyVersionRecord[]> {
|
|
1477
|
-
this.
|
|
1564
|
+
const actor = this.requireActor();
|
|
1565
|
+
await this.requireAccess(actor, { appId: data?.appId }, 'read');
|
|
1478
1566
|
return listTranslationKeyVersions(data);
|
|
1479
1567
|
}
|
|
1480
1568
|
|
|
1569
|
+
@callable('user')
|
|
1481
1570
|
async listMemoryMatches(data: {
|
|
1482
1571
|
appId: string;
|
|
1483
1572
|
key: string;
|
|
1484
1573
|
language: string;
|
|
1485
1574
|
}): Promise<TranslationMemoryMatchRecord[]> {
|
|
1486
|
-
this.
|
|
1575
|
+
const actor = this.requireActor();
|
|
1576
|
+
await this.requireAccess(actor, { appId: data?.appId }, 'read');
|
|
1487
1577
|
return listTranslationMemoryMatches(data);
|
|
1488
1578
|
}
|
|
1489
1579
|
|
|
1580
|
+
@callable('user')
|
|
1490
1581
|
async proposeRevision(data: {
|
|
1491
1582
|
appId: string;
|
|
1492
1583
|
key: string;
|
|
@@ -1495,7 +1586,8 @@ export class TranslationProvider extends ShapeProvider {
|
|
|
1495
1586
|
basedOnText?: string;
|
|
1496
1587
|
note?: string;
|
|
1497
1588
|
}): Promise<{ id: string }> {
|
|
1498
|
-
|
|
1589
|
+
const actor = this.requireActor();
|
|
1590
|
+
await this.requireAccess(actor, data, 'propose');
|
|
1499
1591
|
const currentEntries = await listTranslationEntries({ appId: data.appId });
|
|
1500
1592
|
const basedOnText =
|
|
1501
1593
|
currentEntries.find(({ key }) => key === data.key)?.units[data.language]
|
|
@@ -1504,18 +1596,18 @@ export class TranslationProvider extends ShapeProvider {
|
|
|
1504
1596
|
...data,
|
|
1505
1597
|
basedOnText,
|
|
1506
1598
|
status: 'proposed',
|
|
1507
|
-
author:
|
|
1599
|
+
author: actor,
|
|
1508
1600
|
authorKind: 'human',
|
|
1509
1601
|
});
|
|
1510
1602
|
}
|
|
1511
1603
|
|
|
1604
|
+
@callable('user')
|
|
1512
1605
|
async listProposals(data: {
|
|
1513
1606
|
appId: string;
|
|
1514
1607
|
language?: string;
|
|
1515
1608
|
}): Promise<TranslationRevisionRecord[]> {
|
|
1516
|
-
this.
|
|
1517
|
-
|
|
1518
|
-
if (!actorWebId) throw new Error('Translation read permission required.');
|
|
1609
|
+
const actorWebId = this.requireActor();
|
|
1610
|
+
await this.requireAccess(actorWebId, { appId: data?.appId }, 'read');
|
|
1519
1611
|
const proposals = await listTranslationProposals(data);
|
|
1520
1612
|
const allowed = await Promise.all(
|
|
1521
1613
|
proposals.map((proposal) =>
|
|
@@ -1530,12 +1622,15 @@ export class TranslationProvider extends ShapeProvider {
|
|
|
1530
1622
|
return proposals.filter((_, index) => allowed[index]);
|
|
1531
1623
|
}
|
|
1532
1624
|
|
|
1625
|
+
@callable('user')
|
|
1533
1626
|
async decideProposal(data: {
|
|
1534
1627
|
appId: string;
|
|
1535
1628
|
revisionId: string;
|
|
1536
1629
|
decision: 'accept' | 'reject';
|
|
1537
1630
|
note?: string;
|
|
1538
1631
|
}): Promise<TranslationProposalDecision> {
|
|
1632
|
+
const actor = this.requireActor();
|
|
1633
|
+
await this.requireAccess(actor, { appId: data.appId }, 'review');
|
|
1539
1634
|
const proposal = await getTranslationRevision({
|
|
1540
1635
|
appId: data.appId,
|
|
1541
1636
|
revisionId: data.revisionId,
|
|
@@ -1544,85 +1639,93 @@ export class TranslationProvider extends ShapeProvider {
|
|
|
1544
1639
|
throw new Error('This translation proposal is no longer pending.');
|
|
1545
1640
|
}
|
|
1546
1641
|
await this.requireAccess(
|
|
1642
|
+
actor,
|
|
1547
1643
|
{ appId: data.appId, language: proposal.language },
|
|
1548
1644
|
'review',
|
|
1549
1645
|
);
|
|
1550
|
-
return decideTranslationProposal(data,
|
|
1646
|
+
return decideTranslationProposal(data, actor);
|
|
1551
1647
|
}
|
|
1552
1648
|
|
|
1649
|
+
@callable('user')
|
|
1553
1650
|
async listMemory(data: {
|
|
1554
1651
|
appId: string;
|
|
1555
1652
|
}): Promise<TranslationMemoryRecord[]> {
|
|
1556
|
-
this.
|
|
1653
|
+
const actor = this.requireActor();
|
|
1654
|
+
await this.requireAccess(actor, { appId: data?.appId }, 'read');
|
|
1557
1655
|
return listTranslationMemory(data);
|
|
1558
1656
|
}
|
|
1559
1657
|
|
|
1658
|
+
@callable('user')
|
|
1560
1659
|
async pretranslateMemory(data: {
|
|
1561
1660
|
appId: string;
|
|
1562
1661
|
language: string;
|
|
1563
1662
|
dryRun?: boolean;
|
|
1564
1663
|
}): Promise<TranslationMemoryPretranslateReport> {
|
|
1565
|
-
|
|
1566
|
-
|
|
1664
|
+
const actor = this.requireActor();
|
|
1665
|
+
await this.requireAccess(actor, data, 'run-mt');
|
|
1666
|
+
return pretranslateFromMemory(data, { author: actor });
|
|
1567
1667
|
}
|
|
1568
1668
|
|
|
1669
|
+
@callable('user')
|
|
1569
1670
|
async applyMemoryMatch(data: {
|
|
1570
1671
|
appId: string;
|
|
1571
1672
|
key: string;
|
|
1572
1673
|
language: string;
|
|
1573
1674
|
unitId: string;
|
|
1574
1675
|
}): Promise<TranslationUnitRecord> {
|
|
1575
|
-
|
|
1576
|
-
|
|
1676
|
+
const actor = this.requireActor();
|
|
1677
|
+
await this.requireAccess(actor, data, 'review');
|
|
1678
|
+
return applyTranslationMemoryMatch(data, { author: actor });
|
|
1577
1679
|
}
|
|
1578
1680
|
|
|
1681
|
+
@callable('user')
|
|
1579
1682
|
async listReleases(data: {
|
|
1580
1683
|
appId: string;
|
|
1581
1684
|
}): Promise<TranslationReleaseRecord[]> {
|
|
1582
|
-
this.
|
|
1685
|
+
const actor = this.requireActor();
|
|
1686
|
+
await this.requireAccess(actor, { appId: data?.appId }, 'read');
|
|
1583
1687
|
return listTranslationReleases(data);
|
|
1584
1688
|
}
|
|
1585
1689
|
|
|
1690
|
+
@callable('user')
|
|
1586
1691
|
async createRelease(
|
|
1587
1692
|
data: Omit<TranslationReleaseRecord, 'id' | 'createdAt'> & {
|
|
1588
1693
|
createdAt?: string;
|
|
1589
1694
|
},
|
|
1590
1695
|
): Promise<TranslationReleaseRecord> {
|
|
1591
|
-
this.
|
|
1696
|
+
const actor = this.requireActor();
|
|
1697
|
+
await this.requireAccess(actor, data, 'manage');
|
|
1592
1698
|
return createTranslationRelease(data);
|
|
1593
1699
|
}
|
|
1594
1700
|
|
|
1701
|
+
@callable('user')
|
|
1595
1702
|
async advanceReleaseHotfix(data: {
|
|
1596
1703
|
id: string;
|
|
1597
1704
|
expectedSequence: number;
|
|
1598
1705
|
hotfixSequence: number;
|
|
1599
1706
|
manifestHash: string;
|
|
1600
1707
|
}): Promise<TranslationReleaseRecord> {
|
|
1601
|
-
this.
|
|
1602
|
-
|
|
1708
|
+
const actor = this.requireActor();
|
|
1709
|
+
const id = typeof data?.id === 'string' ? data.id.trim() : '';
|
|
1710
|
+
const appId = await storedReleaseAppId(id, releaseAppId(id));
|
|
1711
|
+
await this.requireAccess(actor, { appId }, 'manage');
|
|
1712
|
+
return advanceTranslationReleaseHotfix({ ...data, appId });
|
|
1603
1713
|
}
|
|
1604
1714
|
|
|
1715
|
+
@callable('user')
|
|
1605
1716
|
async upsertKey(data: TranslationKeyInput): Promise<{ id: string }> {
|
|
1606
|
-
this.
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
/** WebID of the signed-in caller — never trust a client-supplied author. */
|
|
1611
|
-
private authorIri(): string | undefined {
|
|
1612
|
-
const auth = (this as any).request?.linkedAuth;
|
|
1613
|
-
const webId =
|
|
1614
|
-
auth?.userAccount?.accountOf?.id ??
|
|
1615
|
-
auth?.webId ??
|
|
1616
|
-
auth?.userAccount?.id ??
|
|
1617
|
-
auth?.userAccount;
|
|
1618
|
-
return typeof webId === 'string' && webId ? webId : undefined;
|
|
1717
|
+
const actor = this.requireActor();
|
|
1718
|
+
await this.requireAccess(actor, data, 'manage');
|
|
1719
|
+
return upsertTranslationKey(data, { createdBy: actor });
|
|
1619
1720
|
}
|
|
1620
1721
|
|
|
1722
|
+
@callable('user')
|
|
1621
1723
|
async upsertUnit(data: TranslationUnitInput): Promise<{ id: string }> {
|
|
1622
|
-
|
|
1724
|
+
const actor = this.requireActor();
|
|
1725
|
+
await this.requireAccess(actor, data, 'review');
|
|
1623
1726
|
return upsertTranslationUnit(
|
|
1624
1727
|
{ ...data, updatedBy: undefined },
|
|
1625
|
-
{ author:
|
|
1728
|
+
{ author: actor, authorKind: 'human' },
|
|
1626
1729
|
);
|
|
1627
1730
|
}
|
|
1628
1731
|
|
|
@@ -1632,11 +1735,14 @@ export class TranslationProvider extends ShapeProvider {
|
|
|
1632
1735
|
* anchors the IRIs. Uniqueness = (term, language) — upsert is
|
|
1633
1736
|
* delete-then-create on that identity.
|
|
1634
1737
|
*/
|
|
1738
|
+
@callable('user')
|
|
1635
1739
|
async listGlossary(data: { appId: string }): Promise<GlossaryTermRecord[]> {
|
|
1636
|
-
this.
|
|
1740
|
+
const actor = this.requireActor();
|
|
1741
|
+
await this.requireAccess(actor, { appId: data?.appId }, 'read');
|
|
1637
1742
|
return listGlossaryTerms(data);
|
|
1638
1743
|
}
|
|
1639
1744
|
|
|
1745
|
+
@callable('user')
|
|
1640
1746
|
async upsertGlossaryTerm(data: {
|
|
1641
1747
|
appId: string;
|
|
1642
1748
|
term: string;
|
|
@@ -1647,7 +1753,8 @@ export class TranslationProvider extends ShapeProvider {
|
|
|
1647
1753
|
caseSensitive?: boolean;
|
|
1648
1754
|
description?: string;
|
|
1649
1755
|
}): Promise<{ id: string }> {
|
|
1650
|
-
|
|
1756
|
+
const actor = this.requireActor();
|
|
1757
|
+
await this.requireAccess(actor, data, 'manage');
|
|
1651
1758
|
const appId = requireText(data.appId, 'appId');
|
|
1652
1759
|
const term = requireText(data.term, 'term');
|
|
1653
1760
|
const language = data.language?.trim() || '';
|
|
@@ -1678,8 +1785,10 @@ export class TranslationProvider extends ShapeProvider {
|
|
|
1678
1785
|
return { id };
|
|
1679
1786
|
}
|
|
1680
1787
|
|
|
1788
|
+
@callable('user')
|
|
1681
1789
|
async deleteGlossaryTerm(data: { appId: string; id: string }): Promise<{ deleted: boolean }> {
|
|
1682
|
-
|
|
1790
|
+
const actor = this.requireActor();
|
|
1791
|
+
await this.requireAccess(actor, data, 'manage');
|
|
1683
1792
|
const appId = requireText(data.appId, 'appId');
|
|
1684
1793
|
const id = requireText(data.id, 'id');
|
|
1685
1794
|
// Only glossary nodes under this app's namespace may be deleted here.
|
|
@@ -1691,12 +1800,14 @@ export class TranslationProvider extends ShapeProvider {
|
|
|
1691
1800
|
}
|
|
1692
1801
|
|
|
1693
1802
|
/** History for one (key, language) cell — newest first (Plan 017 AD-P). */
|
|
1803
|
+
@callable('user')
|
|
1694
1804
|
async listRevisions(data: {
|
|
1695
1805
|
appId: string;
|
|
1696
1806
|
key: string;
|
|
1697
1807
|
language: string;
|
|
1698
1808
|
}): Promise<TranslationRevisionRecord[]> {
|
|
1699
|
-
this.
|
|
1809
|
+
const actor = this.requireActor();
|
|
1810
|
+
await this.requireAccess(actor, { appId: data?.appId }, 'read');
|
|
1700
1811
|
const appId = requireText(data.appId, 'appId');
|
|
1701
1812
|
const key = requireText(data.key, 'key');
|
|
1702
1813
|
const language = requireText(data.language, 'language');
|
|
@@ -1721,6 +1832,7 @@ export class TranslationProvider extends ShapeProvider {
|
|
|
1721
1832
|
return toRevisionRecords(rows as any);
|
|
1722
1833
|
}
|
|
1723
1834
|
|
|
1835
|
+
@callable('public')
|
|
1724
1836
|
async getMessages(data: {
|
|
1725
1837
|
appId: string;
|
|
1726
1838
|
language: string;
|