@c-rex/services 0.1.1 → 0.1.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/index.js CHANGED
@@ -49,14 +49,21 @@ var API = {
49
49
  "content-Type": "application/json"
50
50
  }
51
51
  };
52
- var SDK_CONFIG_KEY = "crex-sdk-config";
53
52
  var FLAGS_BY_LANG = {
54
53
  "en": "US",
55
54
  "de": "DE"
56
55
  };
57
-
58
- // ../core/src/requests.ts
59
- var import_openid_client = require("openid-client");
56
+ var EN_LANG = "en";
57
+ var TOPIC = "TOPIC";
58
+ var DOCUMENT = "DOCUMENT";
59
+ var PACKAGE = "PACKAGE";
60
+ var RESULT_TYPES = {
61
+ TOPIC,
62
+ DOCUMENT,
63
+ PACKAGE
64
+ };
65
+ var DEFAULT_COOKIE_LIMIT = 7 * 24 * 60 * 60 * 1e3;
66
+ var CREX_TOKEN_HEADER_KEY = "crex-token";
60
67
 
61
68
  // ../utils/src/utils.ts
62
69
  var call = async (method, params) => {
@@ -64,7 +71,8 @@ var call = async (method, params) => {
64
71
  const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/rpc`, {
65
72
  method: "POST",
66
73
  headers: { "Content-Type": "application/json" },
67
- body: JSON.stringify({ method, params })
74
+ body: JSON.stringify({ method, params }),
75
+ credentials: "include"
68
76
  });
69
77
  const json = await res.json();
70
78
  if (!res.ok) throw new Error(json.error || "Unknown error");
@@ -84,23 +92,35 @@ var getCountryCodeByLang = (lang) => {
84
92
  };
85
93
 
86
94
  // ../utils/src/memory.ts
87
- function isBrowser() {
88
- return typeof window !== "undefined" && typeof document !== "undefined";
89
- }
90
- function saveInMemory(value, key) {
91
- if (isBrowser()) throw new Error("saveInMemory is not supported in browser");
92
- if (typeof global !== "undefined" && !(key in global)) {
93
- global[key] = null;
95
+ var getCookie = async (key) => {
96
+ const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/cookies?key=${key}`);
97
+ if (!res.ok) {
98
+ return { key, value: null };
94
99
  }
95
- const globalConfig = global[key];
96
- if (globalConfig === null) {
97
- global[key] = value;
100
+ const json = await res.json();
101
+ return json;
102
+ };
103
+ var setCookie = async (key, value, maxAge) => {
104
+ try {
105
+ if (maxAge === void 0) {
106
+ maxAge = DEFAULT_COOKIE_LIMIT;
107
+ }
108
+ await fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/cookies`, {
109
+ method: "POST",
110
+ credentials: "include",
111
+ body: JSON.stringify({
112
+ key,
113
+ value,
114
+ maxAge
115
+ })
116
+ });
117
+ } catch (error) {
118
+ call("CrexLogger.log", {
119
+ level: "error",
120
+ message: `utils.setCookie error: ${error}`
121
+ });
98
122
  }
99
- }
100
- function getFromMemory(key) {
101
- if (isBrowser()) throw new Error("getFromMemory is not supported in browser");
102
- return global[key];
103
- }
123
+ };
104
124
 
105
125
  // ../utils/src/classMerge.ts
106
126
  var import_clsx = require("clsx");
@@ -118,68 +138,153 @@ var generateQueryParams = (params) => {
118
138
  return queryParams;
119
139
  };
120
140
 
141
+ // ../utils/src/token.ts
142
+ var updateToken = async () => {
143
+ try {
144
+ const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/token`, {
145
+ method: "POST",
146
+ credentials: "include"
147
+ });
148
+ const cookies = response.headers.get("set-cookie");
149
+ if (cookies === null) return null;
150
+ const aux = cookies.split(`${CREX_TOKEN_HEADER_KEY}=`);
151
+ if (aux.length == 0) return null;
152
+ const token = aux[1].split(";")[0];
153
+ if (token === void 0) throw new Error("Token is undefined");
154
+ return token;
155
+ } catch (error) {
156
+ call("CrexLogger.log", {
157
+ level: "error",
158
+ message: `utils.updateToken error: ${error}`
159
+ });
160
+ return null;
161
+ }
162
+ };
163
+ var manageToken = async () => {
164
+ try {
165
+ const hasToken = await getCookie(CREX_TOKEN_HEADER_KEY);
166
+ let token = hasToken.value;
167
+ if (hasToken.value === null) {
168
+ const tokenResult = await updateToken();
169
+ if (tokenResult === null) throw new Error("Token is undefined");
170
+ token = tokenResult;
171
+ }
172
+ return token;
173
+ } catch (error) {
174
+ call("CrexLogger.log", {
175
+ level: "error",
176
+ message: `utils.manageToken error: ${error}`
177
+ });
178
+ return null;
179
+ }
180
+ };
181
+
121
182
  // ../core/src/requests.ts
122
183
  var import_next_cookies = require("@c-rex/utils/next-cookies");
123
- var CREX_TOKEN_HEADER_KEY = "crex-token";
124
- var CREX_TOKEN_EXPIRY_HEADER_KEY = "crex-token-expiry";
125
- var CrexApi = class {
126
- customerConfig;
127
- apiClient;
128
- async manageToken() {
129
- const headersAux = {};
130
- if (this.customerConfig.OIDC.client.enabled) {
131
- let token = getFromMemory(CREX_TOKEN_HEADER_KEY);
132
- let tokenExpiry = getFromMemory(CREX_TOKEN_EXPIRY_HEADER_KEY);
133
- const now = Math.floor(Date.now() / 1e3);
134
- if (!(token && tokenExpiry > now + 60)) {
135
- const { token: tokenAux, tokenExpiry: tokenExpiryAux } = await this.getToken();
136
- token = tokenAux;
137
- saveInMemory(token, CREX_TOKEN_HEADER_KEY);
138
- saveInMemory(tokenExpiryAux, CREX_TOKEN_EXPIRY_HEADER_KEY);
139
- }
140
- headersAux["Authorization"] = `Bearer ${token}`;
141
- }
142
- return headersAux;
184
+
185
+ // ../core/src/cache.ts
186
+ var CrexCache = class {
187
+ /**
188
+ * Retrieves a value from the cache by key.
189
+ *
190
+ * @param key - The cache key to retrieve
191
+ * @returns The cached value as a string, or null if not found
192
+ */
193
+ async get(key) {
194
+ const cookie = await getCookie(key);
195
+ return cookie.value;
143
196
  }
144
- async getToken() {
145
- let token = "";
146
- let tokenExpiry = 0;
197
+ /**
198
+ * Stores a value in the cache with the specified key.
199
+ * Checks if the value size is within cookie size limits (4KB).
200
+ *
201
+ * @param key - The cache key
202
+ * @param value - The value to store
203
+ */
204
+ async set(key, value) {
147
205
  try {
148
- const now = Math.floor(Date.now() / 1e3);
149
- const issuer = await import_openid_client.Issuer.discover(this.customerConfig.OIDC.client.issuer);
150
- const client = new issuer.Client({
151
- client_id: this.customerConfig.OIDC.client.id,
152
- client_secret: this.customerConfig.OIDC.client.secret
153
- });
154
- const tokenSet = await client.grant({ grant_type: "client_credentials" });
155
- token = tokenSet.access_token;
156
- tokenExpiry = now + tokenSet.expires_at;
157
- console.log("token", token);
206
+ const byteLength = new TextEncoder().encode(value).length;
207
+ if (byteLength <= 4096) {
208
+ await setCookie(key, value);
209
+ } else {
210
+ console.warn(`Cookie ${key} value is too large to be stored in a single cookie`);
211
+ }
158
212
  } catch (error) {
159
213
  call("CrexLogger.log", {
160
214
  level: "error",
161
- message: `API.getToken error when request ${this.customerConfig.OIDC.client.issuer}. Error: ${error}`
215
+ message: `CrexCache.set error: ${error}`
162
216
  });
163
217
  }
164
- return {
165
- token,
166
- tokenExpiry
167
- };
168
218
  }
219
+ /**
220
+ * Generates a cache key based on request parameters.
221
+ * Combines URL, method, and optionally params, body, and headers into a single string key.
222
+ *
223
+ * @param options - Request options to generate key from
224
+ * @param options.url - The request URL
225
+ * @param options.method - The HTTP method
226
+ * @param options.body - Optional request body
227
+ * @param options.params - Optional query parameters
228
+ * @param options.headers - Optional request headers
229
+ * @returns A string cache key
230
+ */
231
+ generateCacheKey({
232
+ url,
233
+ method,
234
+ body,
235
+ params,
236
+ headers
237
+ }) {
238
+ let cacheKey = `${url}-${method}`;
239
+ if (params !== void 0 && Object.keys(params).length > 0) {
240
+ cacheKey += `-${JSON.stringify(params)}`;
241
+ }
242
+ if (body !== void 0 && Object.keys(body).length > 0) {
243
+ cacheKey += `-${JSON.stringify(body)}`;
244
+ }
245
+ if (headers !== void 0 && Object.keys(headers).length > 0) {
246
+ cacheKey += `-${JSON.stringify(headers)}`;
247
+ }
248
+ return cacheKey;
249
+ }
250
+ };
251
+
252
+ // ../core/src/requests.ts
253
+ var CrexApi = class {
254
+ customerConfig;
255
+ apiClient;
256
+ cache;
257
+ /**
258
+ * Initializes the API client if it hasn't been initialized yet.
259
+ * Loads customer configuration, creates the axios instance, and initializes the cache.
260
+ *
261
+ * @private
262
+ */
169
263
  async initAPI() {
170
264
  if (!this.customerConfig) {
171
- const jsonConfigs = await (0, import_next_cookies.getCookie)(SDK_CONFIG_KEY);
172
- if (!jsonConfigs) {
173
- throw new Error("SDK not initialized");
174
- }
175
- this.customerConfig = JSON.parse(jsonConfigs);
265
+ this.customerConfig = await (0, import_next_cookies.getConfigs)();
176
266
  }
177
267
  if (!this.apiClient) {
178
268
  this.apiClient = import_axios.default.create({
179
269
  baseURL: this.customerConfig.baseUrl
180
270
  });
181
271
  }
272
+ if (!this.cache) {
273
+ this.cache = new CrexCache();
274
+ }
182
275
  }
276
+ /**
277
+ * Executes an API request with caching, authentication, and retry logic.
278
+ *
279
+ * @param options - Request options
280
+ * @param options.url - The URL to request
281
+ * @param options.method - The HTTP method to use
282
+ * @param options.params - Optional query parameters
283
+ * @param options.body - Optional request body
284
+ * @param options.headers - Optional request headers
285
+ * @returns The response data
286
+ * @throws Error if the request fails after maximum retries
287
+ */
183
288
  async execute({
184
289
  url,
185
290
  method,
@@ -189,10 +294,14 @@ var CrexApi = class {
189
294
  }) {
190
295
  await this.initAPI();
191
296
  let response = void 0;
192
- headers = {
193
- ...headers,
194
- ...await this.manageToken()
195
- };
297
+ if (this.customerConfig.OIDC.client.enabled) {
298
+ const token = await manageToken();
299
+ headers = {
300
+ ...headers,
301
+ Authorization: `Bearer ${token}`
302
+ };
303
+ this.apiClient.defaults.headers.common["Authorization"] = `Bearer ${token}`;
304
+ }
196
305
  for (let retry = 0; retry < API.MAX_RETRY; retry++) {
197
306
  try {
198
307
  response = await this.apiClient.request({
@@ -204,10 +313,10 @@ var CrexApi = class {
204
313
  });
205
314
  break;
206
315
  } catch (error) {
207
- console.log(
208
- "error",
209
- `API.execute ${retry + 1}\xBA error when request ${url}. Error: ${error}`
210
- );
316
+ call("CrexLogger.log", {
317
+ level: "error",
318
+ message: `API.execute ${retry + 1}\xBA error when request ${url}. Error: ${error}`
319
+ });
211
320
  if (retry === API.MAX_RETRY - 1) {
212
321
  throw error;
213
322
  }
@@ -224,10 +333,26 @@ var CrexApi = class {
224
333
  var BaseService = class {
225
334
  api;
226
335
  endpoint;
336
+ /**
337
+ * Creates a new instance of BaseService.
338
+ *
339
+ * @param endpoint - The API endpoint URL for this service
340
+ */
227
341
  constructor(endpoint) {
228
342
  this.api = new CrexApi();
229
343
  this.endpoint = endpoint;
230
344
  }
345
+ /**
346
+ * Makes an API request to the specified endpoint.
347
+ *
348
+ * @param options - Request configuration options
349
+ * @param options.path - Optional path to append to the endpoint
350
+ * @param options.params - Optional query parameters to include in the request
351
+ * @param options.method - HTTP method to use (defaults to 'get')
352
+ * @param options.transformer - Optional function to transform the response data
353
+ * @returns The response data, optionally transformed
354
+ * @throws Error if the API request fails
355
+ */
231
356
  async request({
232
357
  path = "",
233
358
  method = "get",
@@ -262,7 +387,15 @@ var RenditionsService = class extends BaseService {
262
387
  constructor() {
263
388
  super("Renditions/");
264
389
  }
265
- async getHTMLRendition(renditions) {
390
+ /**
391
+ * Retrieves the HTML rendition from a list of renditions.
392
+ * Filters for renditions with format 'application/xhtml+xml' and rel 'view'.
393
+ *
394
+ * @param renditions - Array of rendition objects to process
395
+ * @returns A promise that resolves to the HTML content as a string, or empty string if no suitable rendition is found
396
+ * @throws Error if the API request fails
397
+ */
398
+ async getHTMLRendition({ renditions }) {
266
399
  const filteredRenditions = renditions.filter(
267
400
  (item2) => item2.format == "application/xhtml+xml"
268
401
  );
@@ -280,17 +413,48 @@ var RenditionsService = class extends BaseService {
280
413
  });
281
414
  return response;
282
415
  }
283
- async getDocumentRendition(renditions, type) {
416
+ /**
417
+ * Processes a list of renditions and categorizes them into files to download and files to open.
418
+ * Excludes renditions with formats 'application/xhtml+xml', 'application/json', and 'application/llm+xml'.
419
+ *
420
+ * @param renditions - Array of rendition objects to process
421
+ * @returns An object containing arrays of file renditions categorized as 'filesToDownload' and 'filesToOpen'
422
+ */
423
+ getFileRenditions = ({ renditions }) => {
424
+ if (renditions == void 0 || renditions.length == 0) {
425
+ return {
426
+ filesToDownload: [],
427
+ filesToOpen: []
428
+ };
429
+ }
284
430
  const filteredRenditions = renditions.filter(
285
- (item2) => item2.format == `application/${type}`
431
+ (item) => item.format != "application/xhtml+xml" && item.format != "application/json" && item.format != "application/llm+xml"
286
432
  );
287
- if (filteredRenditions.length == 0 || filteredRenditions[0] == void 0) return "";
288
- const item = filteredRenditions[0];
289
- const filteredLinks = item.links.filter((item2) => item2.rel == "view");
290
- if (filteredLinks.length == 0 || filteredLinks[0] == void 0) return "";
291
- const url = filteredLinks[0].href;
292
- return url;
293
- }
433
+ if (filteredRenditions.length == 0 || filteredRenditions[0] == void 0) {
434
+ return {
435
+ filesToDownload: [],
436
+ filesToOpen: []
437
+ };
438
+ }
439
+ const filesToDownload = filteredRenditions.map((item) => {
440
+ const filteredLinks = item.links.filter((item2) => item2.rel == "download");
441
+ return {
442
+ format: item.format,
443
+ link: filteredLinks[0].href
444
+ };
445
+ });
446
+ const filesToOpen = filteredRenditions.map((item) => {
447
+ const filteredLinks = item.links.filter((item2) => item2.rel == "view");
448
+ return {
449
+ format: item.format,
450
+ link: filteredLinks[0].href
451
+ };
452
+ });
453
+ return {
454
+ filesToDownload,
455
+ filesToOpen
456
+ };
457
+ };
294
458
  };
295
459
 
296
460
  // src/directoryNodes.ts
@@ -298,11 +462,36 @@ var DirectoryNodesService = class extends BaseService {
298
462
  constructor() {
299
463
  super("DirectoryNodes/");
300
464
  }
465
+ /**
466
+ * Retrieves a specific directory node by its ID.
467
+ *
468
+ * @param id - The unique identifier of the directory node
469
+ * @returns A promise that resolves to the directory node data
470
+ * @throws Error if the API request fails
471
+ */
301
472
  async getItem(id) {
302
473
  return await this.request({
303
474
  path: id
304
475
  });
305
476
  }
477
+ /**
478
+ * Retrieves a list of directory nodes based on specified filters.
479
+ *
480
+ * @param options - Options for filtering the directory nodes list
481
+ * @param options.filters - Optional array of filter strings to apply
482
+ * @returns A promise that resolves to the directory nodes response
483
+ * @throws Error if the API request fails
484
+ */
485
+ async getList({
486
+ filters = []
487
+ }) {
488
+ const remainFilters = createParams(filters, "Filter");
489
+ return await this.request({
490
+ params: {
491
+ ...remainFilters
492
+ }
493
+ });
494
+ }
306
495
  };
307
496
 
308
497
  // src/transforms/documentTypes.ts
@@ -322,6 +511,14 @@ var DocumentTypesService = class extends BaseService {
322
511
  constructor() {
323
512
  super("DocumentTypes/");
324
513
  }
514
+ /**
515
+ * Retrieves document type labels for the specified fields.
516
+ * The labels are restricted to English language (EN-us).
517
+ *
518
+ * @param fields - Array of field names to retrieve labels for
519
+ * @returns A promise that resolves to an array of label strings
520
+ * @throws Error if the API request fails
521
+ */
325
522
  async getLabels(fields) {
326
523
  const params = [
327
524
  {
@@ -338,9 +535,33 @@ var DocumentTypesService = class extends BaseService {
338
535
  };
339
536
 
340
537
  // src/transforms/information.ts
341
- var transformInformationUnits = (data) => {
538
+ var import_next_cookies2 = require("@c-rex/utils/next-cookies");
539
+ var transformInformationUnits = async (data) => {
540
+ const config = (0, import_next_cookies2.getConfigs)();
541
+ const items = await Promise.all(data.items.map(async (item) => {
542
+ const type = item.class.labels.filter((item2) => item2.language === EN_LANG)[0].value.toUpperCase();
543
+ const service = new RenditionsService();
544
+ const { filesToOpen, filesToDownload } = service.getFileRenditions({ renditions: item?.renditions });
545
+ let link = `/topics/${item.shortId}`;
546
+ if (config.results.articlePageLayout == "BLOG") {
547
+ link = `/blog/${item.shortId}`;
548
+ } else if (type == RESULT_TYPES.DOCUMENT) {
549
+ link = `/documents/${item.shortId}`;
550
+ }
551
+ return {
552
+ language: item.labels[0].language,
553
+ title: item.labels[0].value,
554
+ type,
555
+ localeType: "",
556
+ shortId: item.shortId,
557
+ disabled: config.results.disabledResults.includes(type),
558
+ link,
559
+ filesToOpen,
560
+ filesToDownload
561
+ };
562
+ }));
342
563
  return {
343
- items: data.items.map((item) => item),
564
+ items,
344
565
  pageInfo: data.pageInfo
345
566
  };
346
567
  };
@@ -350,6 +571,18 @@ var InformationUnitsService = class extends BaseService {
350
571
  constructor() {
351
572
  super("InformationUnits/");
352
573
  }
574
+ /**
575
+ * Retrieves a list of information units based on specified criteria.
576
+ *
577
+ * @param options - Options for filtering and paginating the information units list
578
+ * @param options.queries - Optional search query string
579
+ * @param options.page - Optional page number for pagination (defaults to 1)
580
+ * @param options.fields - Optional array of fields to include in the response
581
+ * @param options.filters - Optional array of filter strings to apply
582
+ * @param options.languages - Optional array of language codes to filter by
583
+ * @returns A promise that resolves to the information units response
584
+ * @throws Error if the API request fails
585
+ */
353
586
  async getList({
354
587
  queries = "",
355
588
  page = 1,
@@ -364,8 +597,8 @@ var InformationUnitsService = class extends BaseService {
364
597
  "Filter"
365
598
  );
366
599
  const params = [
367
- { key: "pageSize", value: "9" },
368
- { key: "PageNumber", value: (page - 1).toString() },
600
+ { key: "pageSize", value: "12" },
601
+ { key: "PageNumber", value: page.toString() },
369
602
  ...remainFields,
370
603
  ...languageParams,
371
604
  ...remainFilters
@@ -380,42 +613,71 @@ var InformationUnitsService = class extends BaseService {
380
613
  transformer: transformInformationUnits
381
614
  });
382
615
  }
616
+ /**
617
+ * Retrieves a specific information unit by its ID.
618
+ * Includes renditions, directory nodes, version information, titles, languages, and labels.
619
+ *
620
+ * @param options - Options for retrieving the information unit
621
+ * @param options.id - The unique identifier of the information unit
622
+ * @returns A promise that resolves to the information unit data
623
+ * @throws Error if the API request fails
624
+ */
383
625
  async getItem({ id }) {
384
626
  const params = [
385
627
  { key: "Fields", value: "renditions" },
386
628
  { key: "Fields", value: "directoryNodes" },
387
629
  { key: "Fields", value: "versionOf" },
388
- { key: "Fields", value: "titles" }
630
+ { key: "Fields", value: "titles" },
631
+ { key: "Fields", value: "languages" },
632
+ { key: "Fields", value: "labels" }
389
633
  ];
390
634
  return await this.request({
391
635
  path: id,
392
636
  params
393
637
  });
394
638
  }
395
- async getSuggestions({ query }) {
639
+ /**
640
+ * Retrieves autocomplete suggestions based on a query prefix.
641
+ *
642
+ * @param options - Options for retrieving suggestions
643
+ * @param options.query - The query prefix to get suggestions for
644
+ * @param options.language - The language of the suggestions
645
+ * @returns A promise that resolves to an array of suggestion strings
646
+ * @throws Error if the API request fails
647
+ */
648
+ async getSuggestions({ query, language }) {
396
649
  return await this.request({
397
650
  path: `Suggestions`,
398
- params: [{ key: "prefix", value: query }],
651
+ params: [
652
+ { key: "prefix", value: query },
653
+ { key: "lang", value: language }
654
+ ],
399
655
  transformer: (data) => {
400
- return data.suggestions.map((item) => item.value);
656
+ const suggestions = [];
657
+ const comparableList = [];
658
+ data.suggestions.forEach((item) => {
659
+ suggestions.push(item.value);
660
+ comparableList.push(item.value.toLowerCase());
661
+ });
662
+ if (!comparableList.includes(query.toLowerCase())) {
663
+ return [query, ...suggestions];
664
+ }
665
+ return suggestions;
401
666
  }
402
667
  });
403
668
  }
404
669
  };
405
670
 
406
671
  // src/language.ts
672
+ var import_next_cookies3 = require("@c-rex/utils/next-cookies");
407
673
  var LanguageService = class extends BaseService {
408
- constructor(endpoint) {
409
- super(endpoint);
674
+ constructor() {
675
+ const configs = (0, import_next_cookies3.getConfigs)();
676
+ super(configs.languageSwitcher.endpoint);
410
677
  }
411
678
  /*
412
679
  public static async getInstance(): Promise<LanguageService> {
413
- const jsonConfigs = await getCookie(SDK_CONFIG_KEY);
414
- if (!jsonConfigs) {
415
- throw new Error("SDK not initialized");
416
- }
417
-
418
- const customerConfig = JSON.parse(jsonConfigs) as ConfigInterface;
680
+ const customerConfig = await getConfigs()
419
681
 
420
682
  if (!LanguageService.instance) {
421
683
  LanguageService.instance = new LanguageService(customerConfig.languageSwitcher.endpoint);
@@ -423,6 +685,13 @@ var LanguageService = class extends BaseService {
423
685
  return LanguageService.instance;
424
686
  }
425
687
  */
688
+ /**
689
+ * Retrieves a list of available languages and their associated countries.
690
+ * Transforms the API response to include language code, country code, and original value.
691
+ *
692
+ * @returns A promise that resolves to an array of language and country objects
693
+ * @throws Error if the API request fails
694
+ */
426
695
  async getLanguagesAndCountries() {
427
696
  return await this.request({
428
697
  transformer: (data) => {