@tandem-language-exchange/content-store 1.3.2 → 1.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-Y6HC4NYU.js → chunk-OCAIIQZW.js} +2 -2
- package/dist/chunk-OCAIIQZW.js.map +1 -0
- package/dist/{chunk-SF7FCBR2.js → chunk-OWL72OTS.js} +3 -3
- package/dist/chunk-OWL72OTS.js.map +1 -0
- package/dist/chunk-POJRKC4G.js +1015 -0
- package/dist/chunk-POJRKC4G.js.map +1 -0
- package/dist/{chunk-D2F7FQEM.js → chunk-QQDU3TVQ.js} +3 -3
- package/dist/{chunk-MOGVAQ2N.js → chunk-SDEERVPV.js} +2 -2
- package/dist/{chunk-YWUFALDR.js → chunk-UQX4THTY.js} +75 -5
- package/dist/chunk-UQX4THTY.js.map +1 -0
- package/dist/{chunk-LZHYKLAU.js → chunk-VBJ6LVMY.js} +2 -4
- package/dist/chunk-VBJ6LVMY.js.map +1 -0
- package/dist/client/fetch-content-bundles.js +5 -5
- package/dist/client/fetch-merged-translation-bundles.js +5 -5
- package/dist/client/fetch-translation-bundles.js +5 -5
- package/dist/client/list-projects.js +1 -1
- package/dist/client/list-resources.js +1 -1
- package/dist/client/query-cms.js +3 -3
- package/dist/node.d.ts +96 -2
- package/dist/node.js +217 -5
- package/dist/node.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-LZHYKLAU.js.map +0 -1
- package/dist/chunk-NQHWG4XM.js +0 -472
- package/dist/chunk-NQHWG4XM.js.map +0 -1
- package/dist/chunk-SF7FCBR2.js.map +0 -1
- package/dist/chunk-Y6HC4NYU.js.map +0 -1
- package/dist/chunk-YWUFALDR.js.map +0 -1
- /package/dist/{chunk-D2F7FQEM.js.map → chunk-QQDU3TVQ.js.map} +0 -0
- /package/dist/{chunk-MOGVAQ2N.js.map → chunk-SDEERVPV.js.map} +0 -0
package/dist/node.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { C as CMSProvider, T as TranslationBundleInfo, F as FetchCmsBundlesOptions, a as FetchTranslationBundlesOptions, S as SDKConfig, b as FetchMergedTranslationBundlesOptions, Q as QueryOptions } from './index-DqDNlXSE.js';
|
|
2
2
|
export { B as BundleItem, c as CmsBundleInfo, d as ContentStore, e as S3Config, f as S3RetryConfig, g as TranslationFilterConfig, h as fetchCmsBundles, i as fetchMergedTranslationBundles, j as fetchTranslationBundles, k as getDefaultS3RetryConfig, q as queryCmsBundle } from './index-DqDNlXSE.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -11,6 +11,65 @@ export { B as BundleItem, c as CmsBundleInfo, d as ContentStore, e as S3Config,
|
|
|
11
11
|
*/
|
|
12
12
|
declare function trimDepth(value: unknown, remaining: number): unknown;
|
|
13
13
|
|
|
14
|
+
type ContentRefreshScope = 'cms' | 'translations' | 'all';
|
|
15
|
+
interface ContentRefreshRequest {
|
|
16
|
+
scope?: ContentRefreshScope;
|
|
17
|
+
cms?: CMSProvider;
|
|
18
|
+
content_types?: string[];
|
|
19
|
+
projects?: string[];
|
|
20
|
+
locales?: string[];
|
|
21
|
+
}
|
|
22
|
+
/** Defaults applied when the request omits fields (set by the host application). */
|
|
23
|
+
interface ContentRefreshDefaults {
|
|
24
|
+
scope?: ContentRefreshScope;
|
|
25
|
+
cms?: CMSProvider;
|
|
26
|
+
contentTypes?: string[];
|
|
27
|
+
translationProjects?: string[];
|
|
28
|
+
locales?: string[];
|
|
29
|
+
}
|
|
30
|
+
interface ContentRefreshError {
|
|
31
|
+
step: string;
|
|
32
|
+
message: string;
|
|
33
|
+
}
|
|
34
|
+
interface ContentRefreshResult {
|
|
35
|
+
ok: boolean;
|
|
36
|
+
scope: ContentRefreshScope;
|
|
37
|
+
cmsFiles?: Record<string, string>;
|
|
38
|
+
translationFiles?: TranslationBundleInfo;
|
|
39
|
+
errors: ContentRefreshError[];
|
|
40
|
+
durationMs: number;
|
|
41
|
+
}
|
|
42
|
+
interface ContentRefreshFetchers {
|
|
43
|
+
fetchCmsBundles: (options: FetchCmsBundlesOptions) => Promise<Record<string, string>>;
|
|
44
|
+
fetchTranslationBundles: (options: FetchTranslationBundlesOptions) => Promise<TranslationBundleInfo>;
|
|
45
|
+
}
|
|
46
|
+
/** Base64 of `username:password` (value only — prefix with `Basic ` in the header). */
|
|
47
|
+
declare function encodeBasicAuthCredentials(username: string, password: string): string;
|
|
48
|
+
/**
|
|
49
|
+
* Validates `Authorization: Basic <base64>` against the expected credentials
|
|
50
|
+
* (base64 of `user:pass`, same as staging site basic auth).
|
|
51
|
+
*/
|
|
52
|
+
declare function assertContentRefreshBasicAuth(authorizationHeader: string | undefined, expectedCredentialsBase64: string): void;
|
|
53
|
+
declare class ContentRefreshAuthError extends Error {
|
|
54
|
+
readonly statusCode: number;
|
|
55
|
+
constructor(message: string, statusCode: number);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Pull the latest bundles from S3 into the host app's configured output directory.
|
|
59
|
+
*/
|
|
60
|
+
declare function executeContentRefresh(fetchers: ContentRefreshFetchers, request: ContentRefreshRequest, defaults?: ContentRefreshDefaults): Promise<ContentRefreshResult>;
|
|
61
|
+
interface PostContentRefreshResponse {
|
|
62
|
+
target: string;
|
|
63
|
+
ok: boolean;
|
|
64
|
+
status: number;
|
|
65
|
+
body?: unknown;
|
|
66
|
+
error?: string;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Ask a remote application (web-site, web-app, …) to refresh its local content cache.
|
|
70
|
+
*/
|
|
71
|
+
declare function postContentRefresh(target: string, url: string, basicAuth: string, request: ContentRefreshRequest): Promise<PostContentRefreshResponse>;
|
|
72
|
+
|
|
14
73
|
declare class ContentStoreSDK {
|
|
15
74
|
private store;
|
|
16
75
|
private outputDir;
|
|
@@ -39,6 +98,41 @@ declare class ContentStoreSDK {
|
|
|
39
98
|
* Queries a previously fetched bundle from the local filesystem.
|
|
40
99
|
*/
|
|
41
100
|
queryCmsBundle(cms: CMSProvider, contentType: string, options?: QueryOptions): Promise<unknown[]>;
|
|
101
|
+
/**
|
|
102
|
+
* Download the latest bundles from S3 (same as the hosted refresh HTTP endpoint).
|
|
103
|
+
*/
|
|
104
|
+
refreshContent(request?: ContentRefreshRequest, defaults?: ContentRefreshDefaults): Promise<ContentRefreshResult>;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Minimal request shape for Next.js Pages API routes (`pages/api/*`). */
|
|
108
|
+
interface NextPagesContentRefreshRequest {
|
|
109
|
+
method?: string;
|
|
110
|
+
headers: {
|
|
111
|
+
authorization?: string | string[];
|
|
112
|
+
};
|
|
113
|
+
body?: ContentRefreshRequest;
|
|
42
114
|
}
|
|
115
|
+
/** Minimal response shape for Next.js Pages API routes. */
|
|
116
|
+
interface NextPagesContentRefreshResponse {
|
|
117
|
+
status(code: number): {
|
|
118
|
+
json(body: unknown): void;
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
interface NextContentRefreshHandlerConfig {
|
|
122
|
+
sdk: ContentStoreSDK;
|
|
123
|
+
/** Base64 of `username:password` (same value as `CONTENT_REFRESH_BASIC_AUTH`). */
|
|
124
|
+
basicAuth: string;
|
|
125
|
+
defaults?: ContentRefreshDefaults;
|
|
126
|
+
}
|
|
127
|
+
/** @deprecated Use {@link NextContentRefreshHandlerConfig}. */
|
|
128
|
+
type NextPagesContentRefreshHandlerConfig = NextContentRefreshHandlerConfig;
|
|
129
|
+
/**
|
|
130
|
+
* Handler for a Next.js Pages API route (e.g. pages/api/internal/content-refresh.ts).
|
|
131
|
+
*/
|
|
132
|
+
declare function createNextPagesApiContentRefreshHandler(config: NextContentRefreshHandlerConfig): (req: NextPagesContentRefreshRequest, res: NextPagesContentRefreshResponse) => Promise<void>;
|
|
133
|
+
/**
|
|
134
|
+
* Handler for a Next.js App Router route (e.g. app/api/internal/content-refresh/route.ts).
|
|
135
|
+
*/
|
|
136
|
+
declare function handleNextAppRouterContentRefresh(request: Request, config: NextContentRefreshHandlerConfig): Promise<Response>;
|
|
43
137
|
|
|
44
|
-
export { CMSProvider, ContentStoreSDK, FetchCmsBundlesOptions, FetchMergedTranslationBundlesOptions, FetchTranslationBundlesOptions, QueryOptions, SDKConfig, TranslationBundleInfo, trimDepth };
|
|
138
|
+
export { CMSProvider, ContentRefreshAuthError, type ContentRefreshDefaults, type ContentRefreshRequest, type ContentRefreshResult, type ContentRefreshScope, ContentStoreSDK, FetchCmsBundlesOptions, FetchMergedTranslationBundlesOptions, FetchTranslationBundlesOptions, type NextContentRefreshHandlerConfig, type NextPagesContentRefreshHandlerConfig, type NextPagesContentRefreshRequest, type NextPagesContentRefreshResponse, type PostContentRefreshResponse, QueryOptions, SDKConfig, TranslationBundleInfo, assertContentRefreshBasicAuth, createNextPagesApiContentRefreshHandler, encodeBasicAuthCredentials, executeContentRefresh, handleNextAppRouterContentRefresh, postContentRefresh, trimDepth };
|
package/dist/node.js
CHANGED
|
@@ -13,9 +13,7 @@ var ContentStore = class {
|
|
|
13
13
|
credentials: {
|
|
14
14
|
accessKeyId: cfg.accessKeyId,
|
|
15
15
|
secretAccessKey: cfg.secretAccessKey
|
|
16
|
-
}
|
|
17
|
-
requestChecksumCalculation: "WHEN_REQUIRED",
|
|
18
|
-
responseChecksumValidation: "WHEN_REQUIRED"
|
|
16
|
+
}
|
|
19
17
|
});
|
|
20
18
|
this.bucket = cfg.bucket;
|
|
21
19
|
}
|
|
@@ -179,7 +177,7 @@ function trimDepth(value, remaining) {
|
|
|
179
177
|
}
|
|
180
178
|
|
|
181
179
|
// src/shared/lingohub.ts
|
|
182
|
-
var defaultLocales = ["en", "fr", "de", "es", "it", "pt-br", "ru", "zh-hans", "zh-hant", "ko", "ja"];
|
|
180
|
+
var defaultLocales = ["en", "fr", "de", "es", "it", "pt-br", "uk", "ru", "zh-hans", "zh-hant", "ko", "ja"];
|
|
183
181
|
var localeMapping = {
|
|
184
182
|
ios: {
|
|
185
183
|
"pt-br": "pt",
|
|
@@ -232,7 +230,7 @@ var allProjects = {
|
|
|
232
230
|
},
|
|
233
231
|
{
|
|
234
232
|
resource: "ipad",
|
|
235
|
-
fileName: "
|
|
233
|
+
fileName: "Main_iPad.[locale].strings",
|
|
236
234
|
type: "strings",
|
|
237
235
|
localeMapping: localeMapping.ios
|
|
238
236
|
},
|
|
@@ -744,6 +742,137 @@ async function queryCmsBundle(outputDir, cms, contentType, options = {}) {
|
|
|
744
742
|
return items;
|
|
745
743
|
}
|
|
746
744
|
|
|
745
|
+
// src/shared/content-refresh.ts
|
|
746
|
+
import { timingSafeEqual } from "crypto";
|
|
747
|
+
function resolveContentRefreshScope(request, defaults) {
|
|
748
|
+
return request.scope ?? defaults.scope ?? "cms";
|
|
749
|
+
}
|
|
750
|
+
function encodeBasicAuthCredentials(username, password) {
|
|
751
|
+
return Buffer.from(`${username}:${password}`, "utf8").toString("base64");
|
|
752
|
+
}
|
|
753
|
+
function assertContentRefreshBasicAuth(authorizationHeader, expectedCredentialsBase64) {
|
|
754
|
+
if (!expectedCredentialsBase64) {
|
|
755
|
+
throw new ContentRefreshAuthError(
|
|
756
|
+
"Content refresh basic auth is not configured (set CONTENT_REFRESH_BASIC_AUTH)",
|
|
757
|
+
500
|
|
758
|
+
);
|
|
759
|
+
}
|
|
760
|
+
if (!authorizationHeader?.startsWith("Basic ")) {
|
|
761
|
+
throw new ContentRefreshAuthError(
|
|
762
|
+
"Missing or malformed Authorization header (expected Basic)",
|
|
763
|
+
401
|
|
764
|
+
);
|
|
765
|
+
}
|
|
766
|
+
const provided = authorizationHeader.slice(6).trim();
|
|
767
|
+
const bufA = new TextEncoder().encode(provided);
|
|
768
|
+
const bufB = new TextEncoder().encode(expectedCredentialsBase64);
|
|
769
|
+
if (bufA.byteLength !== bufB.byteLength || !timingSafeEqual(bufA, bufB)) {
|
|
770
|
+
throw new ContentRefreshAuthError("Invalid basic auth credentials", 403);
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
var ContentRefreshAuthError = class extends Error {
|
|
774
|
+
constructor(message, statusCode) {
|
|
775
|
+
super(message);
|
|
776
|
+
this.statusCode = statusCode;
|
|
777
|
+
this.name = "ContentRefreshAuthError";
|
|
778
|
+
}
|
|
779
|
+
};
|
|
780
|
+
async function executeContentRefresh(fetchers, request, defaults = {}) {
|
|
781
|
+
const started = Date.now();
|
|
782
|
+
const scope = resolveContentRefreshScope(request, defaults);
|
|
783
|
+
const errors = [];
|
|
784
|
+
let cmsFiles;
|
|
785
|
+
let translationFiles;
|
|
786
|
+
if (scope === "cms" || scope === "all") {
|
|
787
|
+
const cms = request.cms ?? defaults.cms ?? "contentful";
|
|
788
|
+
const contentTypes = request.content_types ?? defaults.contentTypes;
|
|
789
|
+
if (!contentTypes?.length) {
|
|
790
|
+
errors.push({
|
|
791
|
+
step: "cms",
|
|
792
|
+
message: "content_types (or handler defaults.contentTypes) is required for CMS refresh"
|
|
793
|
+
});
|
|
794
|
+
} else {
|
|
795
|
+
try {
|
|
796
|
+
cmsFiles = await fetchers.fetchCmsBundles({ cms, contentTypes });
|
|
797
|
+
} catch (err) {
|
|
798
|
+
errors.push({
|
|
799
|
+
step: "cms",
|
|
800
|
+
message: err instanceof Error ? err.message : String(err)
|
|
801
|
+
});
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
if (scope === "translations" || scope === "all") {
|
|
806
|
+
const projects = request.projects ?? defaults.translationProjects;
|
|
807
|
+
if (!projects?.length) {
|
|
808
|
+
errors.push({
|
|
809
|
+
step: "translations",
|
|
810
|
+
message: "projects (or handler defaults.translationProjects) is required for translation refresh"
|
|
811
|
+
});
|
|
812
|
+
} else {
|
|
813
|
+
const locales = request.locales ?? defaults.locales;
|
|
814
|
+
try {
|
|
815
|
+
const projectMap = Object.fromEntries(
|
|
816
|
+
projects.map((p) => [p, []])
|
|
817
|
+
);
|
|
818
|
+
translationFiles = await fetchers.fetchTranslationBundles({
|
|
819
|
+
projects: projectMap,
|
|
820
|
+
locales
|
|
821
|
+
});
|
|
822
|
+
} catch (err) {
|
|
823
|
+
errors.push({
|
|
824
|
+
step: "translations",
|
|
825
|
+
message: err instanceof Error ? err.message : String(err)
|
|
826
|
+
});
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
return {
|
|
831
|
+
ok: errors.length === 0,
|
|
832
|
+
scope,
|
|
833
|
+
cmsFiles,
|
|
834
|
+
translationFiles,
|
|
835
|
+
errors,
|
|
836
|
+
durationMs: Date.now() - started
|
|
837
|
+
};
|
|
838
|
+
}
|
|
839
|
+
async function postContentRefresh(target, url, basicAuth, request) {
|
|
840
|
+
try {
|
|
841
|
+
const response = await fetch(url, {
|
|
842
|
+
method: "POST",
|
|
843
|
+
headers: {
|
|
844
|
+
Authorization: `Basic ${basicAuth}`,
|
|
845
|
+
"Content-Type": "application/json",
|
|
846
|
+
Accept: "application/json"
|
|
847
|
+
},
|
|
848
|
+
body: JSON.stringify(request)
|
|
849
|
+
});
|
|
850
|
+
const text = await response.text();
|
|
851
|
+
let body;
|
|
852
|
+
if (text) {
|
|
853
|
+
try {
|
|
854
|
+
body = JSON.parse(text);
|
|
855
|
+
} catch {
|
|
856
|
+
body = text;
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
return {
|
|
860
|
+
target,
|
|
861
|
+
ok: response.ok,
|
|
862
|
+
status: response.status,
|
|
863
|
+
body,
|
|
864
|
+
error: response.ok ? void 0 : typeof body === "object" && body !== null && "error" in body ? String(body.error) : `HTTP ${response.status}`
|
|
865
|
+
};
|
|
866
|
+
} catch (err) {
|
|
867
|
+
return {
|
|
868
|
+
target,
|
|
869
|
+
ok: false,
|
|
870
|
+
status: 0,
|
|
871
|
+
error: err instanceof Error ? err.message : String(err)
|
|
872
|
+
};
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
|
|
747
876
|
// src/sdk/client.ts
|
|
748
877
|
var ContentStoreSDK = class {
|
|
749
878
|
store;
|
|
@@ -784,14 +913,97 @@ var ContentStoreSDK = class {
|
|
|
784
913
|
async queryCmsBundle(cms, contentType, options = {}) {
|
|
785
914
|
return queryCmsBundle(this.outputDir, cms, contentType, options);
|
|
786
915
|
}
|
|
916
|
+
/**
|
|
917
|
+
* Download the latest bundles from S3 (same as the hosted refresh HTTP endpoint).
|
|
918
|
+
*/
|
|
919
|
+
async refreshContent(request = {}, defaults = {}) {
|
|
920
|
+
return executeContentRefresh(
|
|
921
|
+
{
|
|
922
|
+
fetchCmsBundles: (opts) => this.fetchCmsBundles(opts),
|
|
923
|
+
fetchTranslationBundles: (opts) => this.fetchTranslationBundles(opts)
|
|
924
|
+
},
|
|
925
|
+
request,
|
|
926
|
+
defaults
|
|
927
|
+
);
|
|
928
|
+
}
|
|
787
929
|
};
|
|
930
|
+
|
|
931
|
+
// src/sdk/next-content-refresh.ts
|
|
932
|
+
function readAuthorizationHeader(value) {
|
|
933
|
+
if (value == null) {
|
|
934
|
+
return void 0;
|
|
935
|
+
}
|
|
936
|
+
if (Array.isArray(value)) {
|
|
937
|
+
return value[0];
|
|
938
|
+
}
|
|
939
|
+
return value;
|
|
940
|
+
}
|
|
941
|
+
async function runContentRefresh(config, authorization, body) {
|
|
942
|
+
const { sdk, basicAuth, defaults = {} } = config;
|
|
943
|
+
try {
|
|
944
|
+
assertContentRefreshBasicAuth(authorization, basicAuth);
|
|
945
|
+
} catch (err) {
|
|
946
|
+
if (err instanceof ContentRefreshAuthError) {
|
|
947
|
+
return { status: err.statusCode, body: { error: err.message } };
|
|
948
|
+
}
|
|
949
|
+
throw err;
|
|
950
|
+
}
|
|
951
|
+
const result = await executeContentRefresh(
|
|
952
|
+
{
|
|
953
|
+
fetchCmsBundles: (opts) => sdk.fetchCmsBundles(opts),
|
|
954
|
+
fetchTranslationBundles: (opts) => sdk.fetchTranslationBundles(opts)
|
|
955
|
+
},
|
|
956
|
+
body,
|
|
957
|
+
defaults
|
|
958
|
+
);
|
|
959
|
+
const status = result.ok ? 200 : result.errors.length > 0 ? 207 : 500;
|
|
960
|
+
return { status, body: result };
|
|
961
|
+
}
|
|
962
|
+
function createNextPagesApiContentRefreshHandler(config) {
|
|
963
|
+
return async (req, res) => {
|
|
964
|
+
if (req.method !== "POST") {
|
|
965
|
+
res.status(405).json({ error: "Method not allowed" });
|
|
966
|
+
return;
|
|
967
|
+
}
|
|
968
|
+
const { status, body } = await runContentRefresh(
|
|
969
|
+
config,
|
|
970
|
+
readAuthorizationHeader(req.headers.authorization),
|
|
971
|
+
req.body ?? {}
|
|
972
|
+
);
|
|
973
|
+
res.status(status).json(body);
|
|
974
|
+
};
|
|
975
|
+
}
|
|
976
|
+
async function handleNextAppRouterContentRefresh(request, config) {
|
|
977
|
+
if (request.method !== "POST") {
|
|
978
|
+
return Response.json({ error: "Method not allowed" }, { status: 405 });
|
|
979
|
+
}
|
|
980
|
+
let body = {};
|
|
981
|
+
try {
|
|
982
|
+
body = await request.json();
|
|
983
|
+
} catch {
|
|
984
|
+
return Response.json({ error: "Invalid JSON body" }, { status: 400 });
|
|
985
|
+
}
|
|
986
|
+
const { status, body: payload } = await runContentRefresh(
|
|
987
|
+
config,
|
|
988
|
+
request.headers.get("authorization") ?? void 0,
|
|
989
|
+
body
|
|
990
|
+
);
|
|
991
|
+
return Response.json(payload, { status });
|
|
992
|
+
}
|
|
788
993
|
export {
|
|
994
|
+
ContentRefreshAuthError,
|
|
789
995
|
ContentStore,
|
|
790
996
|
ContentStoreSDK,
|
|
997
|
+
assertContentRefreshBasicAuth,
|
|
998
|
+
createNextPagesApiContentRefreshHandler,
|
|
999
|
+
encodeBasicAuthCredentials,
|
|
1000
|
+
executeContentRefresh,
|
|
791
1001
|
fetchCmsBundles,
|
|
792
1002
|
fetchMergedTranslationBundles,
|
|
793
1003
|
fetchTranslationBundles,
|
|
794
1004
|
getDefaultS3RetryConfig,
|
|
1005
|
+
handleNextAppRouterContentRefresh,
|
|
1006
|
+
postContentRefresh,
|
|
795
1007
|
queryCmsBundle,
|
|
796
1008
|
trimDepth
|
|
797
1009
|
};
|