inibase 1.0.0-rc.125 → 1.0.0-rc.126
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/file.js +18 -29
- package/dist/index.d.ts +1 -1
- package/dist/index.js +74 -47
- package/dist/utils.js +11 -10
- package/package.json +4 -2
package/dist/file.js
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
import { createWriteStream } from "node:fs";
|
|
2
1
|
import { access, appendFile, copyFile, constants as fsConstants, open, readFile, unlink, writeFile, } from "node:fs/promises";
|
|
3
2
|
import { join, resolve } from "node:path";
|
|
4
3
|
import { createInterface } from "node:readline";
|
|
5
4
|
import { Transform } from "node:stream";
|
|
6
5
|
import { pipeline } from "node:stream/promises";
|
|
7
6
|
import { createGunzip, createGzip } from "node:zlib";
|
|
8
|
-
import { spawn as spawnSync } from "node:child_process";
|
|
9
7
|
import Inison from "inison";
|
|
10
8
|
import { detectFieldType, isArrayOfObjects, isStringified, isNumber, isObject, } from "./utils.js";
|
|
11
9
|
import { compare, encodeID, exec, gunzip, gzip } from "./utils.server.js";
|
|
@@ -275,9 +273,9 @@ export async function get(filePath, lineNumbers, fieldType, fieldChildrenType, s
|
|
|
275
273
|
export const replace = async (filePath, replacements, totalItems) => {
|
|
276
274
|
const fileTempPath = filePath.replace(/([^/]+)\/?$/, ".tmp/$1");
|
|
277
275
|
const isReplacementsObject = isObject(replacements);
|
|
278
|
-
const
|
|
276
|
+
const isReplacementsLineNumbered = isReplacementsObject && !Number.isNaN(Number(Object.keys(replacements)[0]));
|
|
279
277
|
if (await isExists(filePath)) {
|
|
280
|
-
if (
|
|
278
|
+
if (isReplacementsLineNumbered) {
|
|
281
279
|
let fileHandle = null;
|
|
282
280
|
let fileTempHandle = null;
|
|
283
281
|
try {
|
|
@@ -328,34 +326,24 @@ export const replace = async (filePath, replacements, totalItems) => {
|
|
|
328
326
|
}
|
|
329
327
|
}
|
|
330
328
|
else {
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
if (code === 0)
|
|
345
|
-
resolve([fileTempPath, filePath]);
|
|
346
|
-
else
|
|
347
|
-
resolve([fileTempPath, null]);
|
|
348
|
-
});
|
|
349
|
-
// Handle errors in spawning the sed process
|
|
350
|
-
sedProcess.on("error", () => {
|
|
351
|
-
resolve([fileTempPath, null]);
|
|
352
|
-
});
|
|
353
|
-
});
|
|
329
|
+
const escapedFilePath = escapeShellPath(filePath);
|
|
330
|
+
const escapedFileTempPath = escapeShellPath(fileTempPath);
|
|
331
|
+
const sedCommand = `sed -e s/.*/${replacements}/ -e /^$/s/^/${replacements}/ ${escapedFilePath}`;
|
|
332
|
+
const command = filePath.endsWith(".gz")
|
|
333
|
+
? `zcat ${escapedFilePath} | ${sedCommand} | gzip > ${escapedFileTempPath}`
|
|
334
|
+
: `${sedCommand} > ${escapedFileTempPath}`;
|
|
335
|
+
try {
|
|
336
|
+
await exec(command);
|
|
337
|
+
return [fileTempPath, filePath];
|
|
338
|
+
}
|
|
339
|
+
catch {
|
|
340
|
+
return [fileTempPath, null];
|
|
341
|
+
}
|
|
354
342
|
}
|
|
355
343
|
}
|
|
356
344
|
else if (isReplacementsObject) {
|
|
357
345
|
try {
|
|
358
|
-
if (
|
|
346
|
+
if (isReplacementsLineNumbered) {
|
|
359
347
|
const replacementsKeys = Object.keys(replacements)
|
|
360
348
|
.map(Number)
|
|
361
349
|
.toSorted((a, b) => a - b);
|
|
@@ -542,7 +530,8 @@ export const search = async (filePath, operator, comparedAtValue, logicalOperato
|
|
|
542
530
|
// Increment the line count for each line.
|
|
543
531
|
linesCount++;
|
|
544
532
|
// Search only in provided linesNumbers
|
|
545
|
-
if (searchIn?.size &&
|
|
533
|
+
if (searchIn?.size &&
|
|
534
|
+
(!searchIn.has(linesCount) || searchIn.has(-linesCount)))
|
|
546
535
|
continue;
|
|
547
536
|
// Decode the line for comparison.
|
|
548
537
|
const decodedLine = decode(line, fieldType, fieldChildrenType, secretKey);
|
package/dist/index.d.ts
CHANGED
|
@@ -62,7 +62,7 @@ export default class Inibase {
|
|
|
62
62
|
private totalItems;
|
|
63
63
|
constructor(database: string, mainFolder?: string);
|
|
64
64
|
private static errorMessages;
|
|
65
|
-
createError(
|
|
65
|
+
createError(name: ErrorCodes, variable?: string | number | (string | number)[], language?: ErrorLang): Error;
|
|
66
66
|
clear(): void;
|
|
67
67
|
private getFileExtension;
|
|
68
68
|
private _schemaToIdsPath;
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import "dotenv/config";
|
|
2
|
-
import { randomBytes,
|
|
2
|
+
import { randomBytes, scryptSync } from "node:crypto";
|
|
3
3
|
import { appendFileSync, existsSync, readFileSync } from "node:fs";
|
|
4
4
|
import { glob, mkdir, readFile, readdir, rename, rm, unlink, writeFile, } from "node:fs/promises";
|
|
5
5
|
import { join, parse } from "node:path";
|
|
@@ -48,15 +48,17 @@ export default class Inibase {
|
|
|
48
48
|
: "please use dotenv",
|
|
49
49
|
},
|
|
50
50
|
};
|
|
51
|
-
createError(
|
|
52
|
-
const errorMessage = Inibase.errorMessages[language]?.[
|
|
51
|
+
createError(name, variable, language = "en") {
|
|
52
|
+
const errorMessage = Inibase.errorMessages[language]?.[name];
|
|
53
53
|
if (!errorMessage)
|
|
54
54
|
return new Error("ERR");
|
|
55
|
-
|
|
55
|
+
const error = new Error(variable
|
|
56
56
|
? Array.isArray(variable)
|
|
57
57
|
? errorMessage.replace(/\{variable\}/g, () => variable.shift()?.toString() ?? "")
|
|
58
58
|
: errorMessage.replaceAll("{variable}", `'${variable.toString()}'`)
|
|
59
59
|
: errorMessage.replaceAll("{variable}", ""));
|
|
60
|
+
error.name = name;
|
|
61
|
+
return error;
|
|
60
62
|
}
|
|
61
63
|
clear() {
|
|
62
64
|
this.tablesMap = new Map();
|
|
@@ -293,10 +295,12 @@ export default class Inibase {
|
|
|
293
295
|
throw this.createError("TABLE_EMPTY", tableName);
|
|
294
296
|
}
|
|
295
297
|
_validateData(data, schema, skipRequiredField = false) {
|
|
296
|
-
if (Utils.isArrayOfObjects(data))
|
|
298
|
+
if (Utils.isArrayOfObjects(data)) {
|
|
297
299
|
for (const single_data of data)
|
|
298
300
|
this._validateData(single_data, schema, skipRequiredField);
|
|
299
|
-
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
if (Utils.isObject(data)) {
|
|
300
304
|
for (const field of schema) {
|
|
301
305
|
if (!Object.hasOwn(data, field.key) ||
|
|
302
306
|
data[field.key] === null ||
|
|
@@ -304,7 +308,7 @@ export default class Inibase {
|
|
|
304
308
|
data[field.key] === "") {
|
|
305
309
|
if (field.required && !skipRequiredField)
|
|
306
310
|
throw this.createError("FIELD_REQUIRED", field.key);
|
|
307
|
-
|
|
311
|
+
continue;
|
|
308
312
|
}
|
|
309
313
|
if (!Utils.validateFieldType(data[field.key], field.type, (field.type === "array" || field.type === "object") &&
|
|
310
314
|
field.children &&
|
|
@@ -313,7 +317,12 @@ export default class Inibase {
|
|
|
313
317
|
: undefined))
|
|
314
318
|
throw this.createError("INVALID_TYPE", [
|
|
315
319
|
field.key,
|
|
316
|
-
Array.isArray(field.type) ? field.type.join(", ") : field.type
|
|
320
|
+
(Array.isArray(field.type) ? field.type.join(", ") : field.type) +
|
|
321
|
+
(Array.isArray(field.children)
|
|
322
|
+
? Utils.isArrayOfObjects(field.children)
|
|
323
|
+
? "[object]"
|
|
324
|
+
: `[${field.children.join("|")}]`
|
|
325
|
+
: `[${field.children}]`),
|
|
317
326
|
data[field.key],
|
|
318
327
|
]);
|
|
319
328
|
if ((field.type === "array" || field.type === "object") &&
|
|
@@ -329,7 +338,7 @@ export default class Inibase {
|
|
|
329
338
|
if (field.unique) {
|
|
330
339
|
let uniqueKey;
|
|
331
340
|
if (typeof field.unique === "boolean")
|
|
332
|
-
uniqueKey =
|
|
341
|
+
uniqueKey = field.id;
|
|
333
342
|
else
|
|
334
343
|
uniqueKey = field.unique;
|
|
335
344
|
if (!this.uniqueMap.has(uniqueKey))
|
|
@@ -433,22 +442,38 @@ export default class Inibase {
|
|
|
433
442
|
async checkUnique(tableName) {
|
|
434
443
|
const tablePath = join(this.databasePath, tableName);
|
|
435
444
|
const flattenSchema = Utils.flattenSchema(this.tablesMap.get(tableName).schema);
|
|
445
|
+
function hasDuplicates(setA, setB) {
|
|
446
|
+
for (const value of setA)
|
|
447
|
+
if (setB.has(value))
|
|
448
|
+
return true; // Stop and return true if a duplicate is found
|
|
449
|
+
return false; // No duplicates found
|
|
450
|
+
}
|
|
436
451
|
for await (const [_uniqueID, valueObject] of this.uniqueMap) {
|
|
437
452
|
let index = 0;
|
|
453
|
+
let shouldContinueParent = false; // Flag to manage parent loop continuation
|
|
454
|
+
const mergedLineNumbers = new Set();
|
|
438
455
|
for await (const [columnID, values] of valueObject.columnsValues) {
|
|
439
456
|
index++;
|
|
440
457
|
const field = flattenSchema.find(({ id }) => id === columnID);
|
|
441
|
-
const [searchResult, totalLines] = await File.search(join(tablePath, `${field.key}${this.getFileExtension(tableName)}`), "[]", Array.from(values), undefined, valueObject.exclude, field.type, field.children, 1, undefined, false, this.salt);
|
|
458
|
+
const [searchResult, totalLines, lineNumbers] = await File.search(join(tablePath, `${field.key}${this.getFileExtension(tableName)}`), "[]", Array.from(values), undefined, valueObject.exclude, field.type, field.children, 1, undefined, false, this.salt);
|
|
442
459
|
if (searchResult && totalLines > 0) {
|
|
443
|
-
if (
|
|
460
|
+
if (valueObject.columnsValues.size === 1 ||
|
|
461
|
+
hasDuplicates(lineNumbers, mergedLineNumbers)) {
|
|
462
|
+
this.uniqueMap = new Map();
|
|
444
463
|
throw this.createError("FIELD_UNIQUE", [
|
|
445
464
|
field.key,
|
|
446
465
|
Array.from(values).join(", "),
|
|
447
466
|
]);
|
|
467
|
+
}
|
|
468
|
+
lineNumbers.forEach(mergedLineNumbers.add, mergedLineNumbers);
|
|
469
|
+
}
|
|
470
|
+
else {
|
|
471
|
+
shouldContinueParent = true; // Flag to skip the rest of this inner loop
|
|
472
|
+
break; // Exit the inner loop
|
|
448
473
|
}
|
|
449
|
-
else
|
|
450
|
-
return;
|
|
451
474
|
}
|
|
475
|
+
if (shouldContinueParent)
|
|
476
|
+
continue;
|
|
452
477
|
}
|
|
453
478
|
this.uniqueMap = new Map();
|
|
454
479
|
}
|
|
@@ -1165,8 +1190,9 @@ export default class Inibase {
|
|
|
1165
1190
|
throw this.createError("NO_SCHEMA", tableName);
|
|
1166
1191
|
if (!returnPostedData)
|
|
1167
1192
|
returnPostedData = false;
|
|
1168
|
-
|
|
1169
|
-
|
|
1193
|
+
let clonedData = JSON.parse(JSON.stringify(data));
|
|
1194
|
+
const keys = UtilsServer.hashString(Object.keys(Array.isArray(clonedData) ? clonedData[0] : clonedData).join("."));
|
|
1195
|
+
await this.validateData(tableName, clonedData);
|
|
1170
1196
|
const renameList = [];
|
|
1171
1197
|
try {
|
|
1172
1198
|
await File.lock(join(tablePath, ".tmp"), keys);
|
|
@@ -1177,24 +1203,24 @@ export default class Inibase {
|
|
|
1177
1203
|
.name.split("-")
|
|
1178
1204
|
.map(Number);
|
|
1179
1205
|
this.totalItems.set(`${tableName}-*`, _totalItems);
|
|
1180
|
-
if (Utils.isArrayOfObjects(
|
|
1181
|
-
for (let index = 0; index <
|
|
1182
|
-
const element =
|
|
1206
|
+
if (Utils.isArrayOfObjects(clonedData))
|
|
1207
|
+
for (let index = 0; index < clonedData.length; index++) {
|
|
1208
|
+
const element = clonedData[index];
|
|
1183
1209
|
element.id = ++lastId;
|
|
1184
1210
|
element.createdAt = Date.now();
|
|
1185
1211
|
element.updatedAt = undefined;
|
|
1186
1212
|
}
|
|
1187
1213
|
else {
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1214
|
+
clonedData.id = ++lastId;
|
|
1215
|
+
clonedData.createdAt = Date.now();
|
|
1216
|
+
clonedData.updatedAt = undefined;
|
|
1191
1217
|
}
|
|
1192
|
-
|
|
1218
|
+
clonedData = this.formatData(clonedData, this.tablesMap.get(tableName).schema, true);
|
|
1193
1219
|
const pathesContents = this.joinPathesContents(tableName, this.tablesMap.get(tableName).config.prepend
|
|
1194
|
-
? Array.isArray(
|
|
1195
|
-
?
|
|
1196
|
-
:
|
|
1197
|
-
:
|
|
1220
|
+
? Array.isArray(clonedData)
|
|
1221
|
+
? clonedData.toReversed()
|
|
1222
|
+
: clonedData
|
|
1223
|
+
: clonedData);
|
|
1198
1224
|
await Promise.allSettled(Object.entries(pathesContents).map(async ([path, content]) => renameList.push(this.tablesMap.get(tableName).config.prepend
|
|
1199
1225
|
? await File.prepend(path, content)
|
|
1200
1226
|
: await File.append(path, content))));
|
|
@@ -1208,14 +1234,14 @@ export default class Inibase {
|
|
|
1208
1234
|
await rename(paginationFilePath, join(tablePath, `${lastId}-${this.totalItems.get(`${tableName}-*`)}.pagination`));
|
|
1209
1235
|
if (returnPostedData)
|
|
1210
1236
|
return this.get(tableName, this.tablesMap.get(tableName).config.prepend
|
|
1211
|
-
? Array.isArray(
|
|
1212
|
-
?
|
|
1237
|
+
? Array.isArray(clonedData)
|
|
1238
|
+
? clonedData.map((_, index) => index + 1).toReversed()
|
|
1213
1239
|
: 1
|
|
1214
|
-
: Array.isArray(
|
|
1215
|
-
?
|
|
1240
|
+
: Array.isArray(clonedData)
|
|
1241
|
+
? clonedData
|
|
1216
1242
|
.map((_, index) => this.totalItems.get(`${tableName}-*`) - index)
|
|
1217
1243
|
.toReversed()
|
|
1218
|
-
: this.totalItems.get(`${tableName}-*`), options, !Utils.isArrayOfObjects(
|
|
1244
|
+
: this.totalItems.get(`${tableName}-*`), options, !Utils.isArrayOfObjects(clonedData));
|
|
1219
1245
|
}
|
|
1220
1246
|
finally {
|
|
1221
1247
|
if (renameList.length)
|
|
@@ -1230,21 +1256,22 @@ export default class Inibase {
|
|
|
1230
1256
|
const renameList = [];
|
|
1231
1257
|
const tablePath = join(this.databasePath, tableName);
|
|
1232
1258
|
await this.throwErrorIfTableEmpty(tableName);
|
|
1259
|
+
let clonedData = JSON.parse(JSON.stringify(data));
|
|
1233
1260
|
if (!where) {
|
|
1234
|
-
if (Utils.isArrayOfObjects(
|
|
1235
|
-
if (!
|
|
1261
|
+
if (Utils.isArrayOfObjects(clonedData)) {
|
|
1262
|
+
if (!clonedData.every((item) => Object.hasOwn(item, "id") && Utils.isValidID(item.id)))
|
|
1236
1263
|
throw this.createError("INVALID_ID");
|
|
1237
|
-
return this.put(tableName,
|
|
1264
|
+
return this.put(tableName, clonedData, clonedData.map(({ id }) => id), options, returnUpdatedData || undefined);
|
|
1238
1265
|
}
|
|
1239
1266
|
if (Object.hasOwn(data, "id")) {
|
|
1240
|
-
if (!Utils.isValidID(
|
|
1241
|
-
throw this.createError("INVALID_ID",
|
|
1242
|
-
return this.put(tableName, data,
|
|
1267
|
+
if (!Utils.isValidID(clonedData.id))
|
|
1268
|
+
throw this.createError("INVALID_ID", clonedData.id);
|
|
1269
|
+
return this.put(tableName, data, clonedData.id, options, returnUpdatedData || undefined);
|
|
1243
1270
|
}
|
|
1244
|
-
await this.validateData(tableName,
|
|
1245
|
-
|
|
1271
|
+
await this.validateData(tableName, clonedData, true);
|
|
1272
|
+
clonedData = this.formatData(clonedData, this.tablesMap.get(tableName).schema, true);
|
|
1246
1273
|
const pathesContents = this.joinPathesContents(tableName, {
|
|
1247
|
-
...(({ id, ...restOfData }) => restOfData)(
|
|
1274
|
+
...(({ id, ...restOfData }) => restOfData)(clonedData),
|
|
1248
1275
|
updatedAt: Date.now(),
|
|
1249
1276
|
});
|
|
1250
1277
|
try {
|
|
@@ -1271,19 +1298,19 @@ export default class Inibase {
|
|
|
1271
1298
|
else if ((Array.isArray(where) && where.every(Utils.isValidID)) ||
|
|
1272
1299
|
Utils.isValidID(where)) {
|
|
1273
1300
|
const lineNumbers = await this.get(tableName, where, undefined, undefined, true);
|
|
1274
|
-
return this.put(tableName,
|
|
1301
|
+
return this.put(tableName, clonedData, lineNumbers, options, returnUpdatedData || undefined);
|
|
1275
1302
|
}
|
|
1276
1303
|
else if ((Array.isArray(where) && where.every(Utils.isNumber)) ||
|
|
1277
1304
|
Utils.isNumber(where)) {
|
|
1278
1305
|
// "where" in this case, is the line(s) number(s) and not id(s)
|
|
1279
|
-
await this.validateData(tableName,
|
|
1280
|
-
|
|
1281
|
-
const pathesContents = Object.fromEntries(Object.entries(this.joinPathesContents(tableName, Utils.isArrayOfObjects(
|
|
1282
|
-
?
|
|
1306
|
+
await this.validateData(tableName, clonedData, true);
|
|
1307
|
+
clonedData = this.formatData(clonedData, this.tablesMap.get(tableName).schema, true);
|
|
1308
|
+
const pathesContents = Object.fromEntries(Object.entries(this.joinPathesContents(tableName, Utils.isArrayOfObjects(clonedData)
|
|
1309
|
+
? clonedData.map((item) => ({
|
|
1283
1310
|
...item,
|
|
1284
1311
|
updatedAt: Date.now(),
|
|
1285
1312
|
}))
|
|
1286
|
-
: { ...
|
|
1313
|
+
: { ...clonedData, updatedAt: Date.now() })).map(([path, content]) => [
|
|
1287
1314
|
path,
|
|
1288
1315
|
[...(Array.isArray(where) ? where : [where])].reduce((obj, lineNum, index) => Object.assign(obj, {
|
|
1289
1316
|
[lineNum]: Array.isArray(content) ? content[index] : content,
|
|
@@ -1312,7 +1339,7 @@ export default class Inibase {
|
|
|
1312
1339
|
else if (Utils.isObject(where)) {
|
|
1313
1340
|
const lineNumbers = await this.get(tableName, where, undefined, undefined, true);
|
|
1314
1341
|
if (lineNumbers)
|
|
1315
|
-
return this.put(tableName,
|
|
1342
|
+
return this.put(tableName, clonedData, lineNumbers, options, returnUpdatedData || undefined);
|
|
1316
1343
|
}
|
|
1317
1344
|
else
|
|
1318
1345
|
throw this.createError("INVALID_PARAMETERS");
|
package/dist/utils.js
CHANGED
|
@@ -318,16 +318,17 @@ export const validateFieldType = (value, fieldType, fieldChildrenType) => {
|
|
|
318
318
|
fieldType = detectedFieldType;
|
|
319
319
|
}
|
|
320
320
|
if (fieldType === "array" && fieldChildrenType)
|
|
321
|
-
return
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
321
|
+
return (Array.isArray(value) &&
|
|
322
|
+
value.every((v) => {
|
|
323
|
+
let _fieldChildrenType = fieldChildrenType;
|
|
324
|
+
if (Array.isArray(_fieldChildrenType)) {
|
|
325
|
+
const detectedFieldType = detectFieldType(v, _fieldChildrenType);
|
|
326
|
+
if (!detectedFieldType)
|
|
327
|
+
return false;
|
|
328
|
+
_fieldChildrenType = detectedFieldType;
|
|
329
|
+
}
|
|
330
|
+
return validateFieldType(v, _fieldChildrenType);
|
|
331
|
+
}));
|
|
331
332
|
switch (fieldType) {
|
|
332
333
|
case "string":
|
|
333
334
|
return isString(value);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "inibase",
|
|
3
|
-
"version": "1.0.0-rc.
|
|
3
|
+
"version": "1.0.0-rc.126",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Karim Amahtil",
|
|
@@ -68,6 +68,7 @@
|
|
|
68
68
|
}
|
|
69
69
|
},
|
|
70
70
|
"devDependencies": {
|
|
71
|
+
"@biomejs/biome": "1.9.4",
|
|
71
72
|
"@types/bun": "^1.1.10",
|
|
72
73
|
"@types/node": "^22.7.4",
|
|
73
74
|
"tinybench": "^3.0.7",
|
|
@@ -81,6 +82,7 @@
|
|
|
81
82
|
"scripts": {
|
|
82
83
|
"prepublish": "npx tsc",
|
|
83
84
|
"build": "npx tsc",
|
|
84
|
-
"benchmark": "./benchmark/run.js"
|
|
85
|
+
"benchmark": "./benchmark/run.js",
|
|
86
|
+
"test": "npx tsx ./tests/inibase.test.ts"
|
|
85
87
|
}
|
|
86
88
|
}
|