inibase 1.0.0-rc.12 → 1.0.0-rc.121
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 +1 -1
- package/README.md +374 -103
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +272 -0
- package/dist/file.d.ts +140 -0
- package/dist/file.js +704 -0
- package/dist/index.d.ts +205 -0
- package/dist/index.js +1477 -0
- package/dist/utils.d.ts +205 -0
- package/dist/utils.js +520 -0
- package/dist/utils.server.d.ts +98 -0
- package/dist/utils.server.js +275 -0
- package/package.json +67 -20
- package/.env.example +0 -1
- package/file.ts +0 -674
- package/index.test.ts +0 -248
- package/index.ts +0 -1587
- package/tsconfig.json +0 -7
- package/utils.ts +0 -366
package/dist/index.js
ADDED
|
@@ -0,0 +1,1477 @@
|
|
|
1
|
+
import "dotenv/config";
|
|
2
|
+
import { randomBytes, randomInt, scryptSync } from "node:crypto";
|
|
3
|
+
import { appendFileSync, existsSync, readFileSync } from "node:fs";
|
|
4
|
+
import { glob, mkdir, readFile, readdir, rename, rm, unlink, writeFile, } from "node:fs/promises";
|
|
5
|
+
import { join, parse } from "node:path";
|
|
6
|
+
import { inspect } from "node:util";
|
|
7
|
+
import Inison from "inison";
|
|
8
|
+
import * as File from "./file.js";
|
|
9
|
+
import * as Utils from "./utils.js";
|
|
10
|
+
import * as UtilsServer from "./utils.server.js";
|
|
11
|
+
// hide ExperimentalWarning glob()
|
|
12
|
+
process.removeAllListeners("warning");
|
|
13
|
+
export default class Inibase {
|
|
14
|
+
pageInfo;
|
|
15
|
+
salt;
|
|
16
|
+
databasePath;
|
|
17
|
+
fileExtension = ".txt";
|
|
18
|
+
tablesMap;
|
|
19
|
+
uniqueMap;
|
|
20
|
+
totalItems;
|
|
21
|
+
constructor(database, mainFolder = ".") {
|
|
22
|
+
this.databasePath = join(mainFolder, database);
|
|
23
|
+
this.tablesMap = new Map();
|
|
24
|
+
this.totalItems = new Map();
|
|
25
|
+
this.pageInfo = {};
|
|
26
|
+
this.uniqueMap = new Map();
|
|
27
|
+
if (!process.env.INIBASE_SECRET) {
|
|
28
|
+
if (existsSync(".env") &&
|
|
29
|
+
readFileSync(".env").includes("INIBASE_SECRET="))
|
|
30
|
+
throw this.createError("NO_ENV");
|
|
31
|
+
this.salt = scryptSync(randomBytes(16), randomBytes(16), 32);
|
|
32
|
+
appendFileSync(".env", `\nINIBASE_SECRET=${this.salt.toString("hex")}\n`);
|
|
33
|
+
}
|
|
34
|
+
else
|
|
35
|
+
this.salt = Buffer.from(process.env.INIBASE_SECRET, "hex");
|
|
36
|
+
}
|
|
37
|
+
static errorMessages = {
|
|
38
|
+
en: {
|
|
39
|
+
TABLE_EMPTY: "Table {variable} is empty",
|
|
40
|
+
TABLE_EXISTS: "Table {variable} already exists",
|
|
41
|
+
TABLE_NOT_EXISTS: "Table {variable} doesn't exist",
|
|
42
|
+
NO_SCHEMA: "Table {variable} does't have a schema",
|
|
43
|
+
FIELD_UNIQUE: "Field {variable} should be unique, got {variable} instead",
|
|
44
|
+
FIELD_REQUIRED: "Field {variable} is required",
|
|
45
|
+
INVALID_ID: "The given ID(s) is/are not valid(s)",
|
|
46
|
+
INVALID_TYPE: "Expect {variable} to be {variable}, got {variable} instead",
|
|
47
|
+
INVALID_PARAMETERS: "The given parameters are not valid",
|
|
48
|
+
INVALID_REGEX_MATCH: "Field {variable} does not match the expected pattern",
|
|
49
|
+
NO_ENV: Number(process.versions.node.split(".").reduce((a, b) => a + b)) >= 26
|
|
50
|
+
? "please run with '--env-file=.env'"
|
|
51
|
+
: "please use dotenv",
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
createError(code, variable, language = "en") {
|
|
55
|
+
const errorMessage = Inibase.errorMessages[language]?.[code];
|
|
56
|
+
if (!errorMessage)
|
|
57
|
+
return new Error("ERR");
|
|
58
|
+
return new Error(variable
|
|
59
|
+
? Array.isArray(variable)
|
|
60
|
+
? errorMessage.replace(/\{variable\}/g, () => variable.shift()?.toString() ?? "")
|
|
61
|
+
: errorMessage.replaceAll("{variable}", `'${variable.toString()}'`)
|
|
62
|
+
: errorMessage.replaceAll("{variable}", ""));
|
|
63
|
+
}
|
|
64
|
+
clear() {
|
|
65
|
+
this.tablesMap = new Map();
|
|
66
|
+
this.totalItems = new Map();
|
|
67
|
+
this.pageInfo = {};
|
|
68
|
+
this.uniqueMap = new Map();
|
|
69
|
+
}
|
|
70
|
+
getFileExtension(tableName) {
|
|
71
|
+
let mainExtension = this.fileExtension;
|
|
72
|
+
// TODO: ADD ENCRYPTION
|
|
73
|
+
// if(this.tablesMap.get(tableName).config.encryption)
|
|
74
|
+
// mainExtension += ".enc"
|
|
75
|
+
if (this.tablesMap.get(tableName).config.compression)
|
|
76
|
+
mainExtension += ".gz";
|
|
77
|
+
return mainExtension;
|
|
78
|
+
}
|
|
79
|
+
_schemaToIdsPath(tableName, schema, prefix = "") {
|
|
80
|
+
const RETURN = {};
|
|
81
|
+
for (const field of schema)
|
|
82
|
+
if ((field.type === "array" || field.type === "object") &&
|
|
83
|
+
field.children &&
|
|
84
|
+
Utils.isArrayOfObjects(field.children))
|
|
85
|
+
Utils.deepMerge(RETURN, this._schemaToIdsPath(tableName, field.children, `${(prefix ?? "") + field.key}.`));
|
|
86
|
+
else if (field.id)
|
|
87
|
+
RETURN[Utils.isValidID(field.id)
|
|
88
|
+
? UtilsServer.decodeID(field.id, this.salt)
|
|
89
|
+
: field.id] = `${(prefix ?? "") + field.key}${this.getFileExtension(tableName)}`;
|
|
90
|
+
return RETURN;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Create a new table inside database, with predefined schema and config
|
|
94
|
+
*
|
|
95
|
+
* @param {string} tableName
|
|
96
|
+
* @param {Schema} [schema]
|
|
97
|
+
* @param {Config} [config]
|
|
98
|
+
*/
|
|
99
|
+
async createTable(tableName, schema, config) {
|
|
100
|
+
const tablePath = join(this.databasePath, tableName);
|
|
101
|
+
if (await File.isExists(tablePath))
|
|
102
|
+
throw this.createError("TABLE_EXISTS", tableName);
|
|
103
|
+
await mkdir(join(tablePath, ".tmp"), { recursive: true });
|
|
104
|
+
await mkdir(join(tablePath, ".cache"));
|
|
105
|
+
// if config not set => load default global env config
|
|
106
|
+
if (!config)
|
|
107
|
+
config = {
|
|
108
|
+
compression: process.env.INIBASE_COMPRESSION == "true",
|
|
109
|
+
cache: process.env.INIBASE_CACHE === "true",
|
|
110
|
+
prepend: process.env.INIBASE_PREPEND === "true",
|
|
111
|
+
};
|
|
112
|
+
if (config) {
|
|
113
|
+
if (config.compression)
|
|
114
|
+
await writeFile(join(tablePath, ".compression.config"), "");
|
|
115
|
+
if (config.cache)
|
|
116
|
+
await writeFile(join(tablePath, ".cache.config"), "");
|
|
117
|
+
if (config.prepend)
|
|
118
|
+
await writeFile(join(tablePath, ".prepend.config"), "");
|
|
119
|
+
}
|
|
120
|
+
if (schema) {
|
|
121
|
+
const lastSchemaID = { value: 0 };
|
|
122
|
+
await writeFile(join(tablePath, "schema.json"), JSON.stringify(UtilsServer.addIdToSchema(schema, lastSchemaID, this.salt), null, 2));
|
|
123
|
+
await writeFile(join(tablePath, `${lastSchemaID.value}.schema`), "");
|
|
124
|
+
}
|
|
125
|
+
else
|
|
126
|
+
await writeFile(join(tablePath, "0.schema"), "");
|
|
127
|
+
await writeFile(join(tablePath, "0-0.pagination"), "");
|
|
128
|
+
}
|
|
129
|
+
// Function to replace the string in one schema.json file
|
|
130
|
+
async replaceStringInFile(filePath, targetString, replaceString) {
|
|
131
|
+
const data = await readFile(filePath, "utf8");
|
|
132
|
+
if (data.includes(targetString)) {
|
|
133
|
+
const updatedContent = data.replaceAll(targetString, replaceString);
|
|
134
|
+
await writeFile(filePath, updatedContent, "utf8");
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Update table schema or config
|
|
139
|
+
*
|
|
140
|
+
* @param {string} tableName
|
|
141
|
+
* @param {Schema} [schema]
|
|
142
|
+
* @param {(Config&{name?: string})} [config]
|
|
143
|
+
*/
|
|
144
|
+
async updateTable(tableName, schema, config) {
|
|
145
|
+
const table = await this.getTable(tableName);
|
|
146
|
+
const tablePath = join(this.databasePath, tableName);
|
|
147
|
+
if (schema) {
|
|
148
|
+
// remove id from schema
|
|
149
|
+
schema = schema.filter(({ key }) => !["id", "createdAt", "updatedAt"].includes(key));
|
|
150
|
+
let schemaIdFilePath;
|
|
151
|
+
for await (const fileName of glob("*.schema", { cwd: tablePath }))
|
|
152
|
+
schemaIdFilePath = join(tablePath, fileName);
|
|
153
|
+
const lastSchemaID = {
|
|
154
|
+
value: schemaIdFilePath ? Number(parse(schemaIdFilePath).name) : 0,
|
|
155
|
+
};
|
|
156
|
+
if (await File.isExists(join(tablePath, "schema.json"))) {
|
|
157
|
+
// update columns files names based on field id
|
|
158
|
+
schema = UtilsServer.addIdToSchema(schema, lastSchemaID, this.salt);
|
|
159
|
+
if (table.schema?.length) {
|
|
160
|
+
const replaceOldPathes = Utils.findChangedProperties(this._schemaToIdsPath(tableName, table.schema), this._schemaToIdsPath(tableName, schema));
|
|
161
|
+
if (replaceOldPathes)
|
|
162
|
+
await Promise.all(Object.entries(replaceOldPathes).map(async ([oldPath, newPath]) => {
|
|
163
|
+
if (await File.isExists(join(tablePath, oldPath)))
|
|
164
|
+
await rename(join(tablePath, oldPath), join(tablePath, newPath));
|
|
165
|
+
}));
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
else
|
|
169
|
+
schema = UtilsServer.addIdToSchema(schema, lastSchemaID, this.salt);
|
|
170
|
+
await writeFile(join(tablePath, "schema.json"), JSON.stringify(schema, null, 2));
|
|
171
|
+
if (schemaIdFilePath)
|
|
172
|
+
await rename(schemaIdFilePath, join(tablePath, `${lastSchemaID.value}.schema`));
|
|
173
|
+
else
|
|
174
|
+
await writeFile(join(tablePath, `${lastSchemaID.value}.schema`), "");
|
|
175
|
+
}
|
|
176
|
+
if (config) {
|
|
177
|
+
if (config.compression !== undefined &&
|
|
178
|
+
config.compression !== table.config.compression) {
|
|
179
|
+
await UtilsServer.execFile("find", [
|
|
180
|
+
tableName,
|
|
181
|
+
"-type",
|
|
182
|
+
"f",
|
|
183
|
+
"-name",
|
|
184
|
+
`*${this.fileExtension}${config.compression ? "" : ".gz"}`,
|
|
185
|
+
"-exec",
|
|
186
|
+
config.compression ? "gzip" : "gunzip",
|
|
187
|
+
"-f",
|
|
188
|
+
"{}",
|
|
189
|
+
"+",
|
|
190
|
+
], { cwd: this.databasePath });
|
|
191
|
+
if (config.compression)
|
|
192
|
+
await writeFile(join(tablePath, ".compression.config"), "");
|
|
193
|
+
else
|
|
194
|
+
await unlink(join(tablePath, ".compression.config"));
|
|
195
|
+
}
|
|
196
|
+
if (config.cache !== undefined && config.cache !== table.config.cache) {
|
|
197
|
+
if (config.cache)
|
|
198
|
+
await writeFile(join(tablePath, ".cache.config"), "");
|
|
199
|
+
else {
|
|
200
|
+
await this.clearCache(tableName);
|
|
201
|
+
await unlink(join(tablePath, ".cache.config"));
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
if (config.prepend !== undefined &&
|
|
205
|
+
config.prepend !== table.config.prepend) {
|
|
206
|
+
await UtilsServer.execFile("find", [
|
|
207
|
+
tableName,
|
|
208
|
+
"-type",
|
|
209
|
+
"f",
|
|
210
|
+
"-name",
|
|
211
|
+
`*${this.fileExtension}${config.compression ? ".gz" : ""}`,
|
|
212
|
+
"-exec",
|
|
213
|
+
"sh",
|
|
214
|
+
"-c",
|
|
215
|
+
`for file; do ${config.compression
|
|
216
|
+
? 'zcat "$file" | tac | gzip > "$file.reversed" && mv "$file.reversed" "$file"'
|
|
217
|
+
: 'tac "$file" > "$file.reversed" && mv "$file.reversed" "$file"'}; done`,
|
|
218
|
+
"_",
|
|
219
|
+
"{}",
|
|
220
|
+
"+",
|
|
221
|
+
], { cwd: this.databasePath });
|
|
222
|
+
if (config.prepend)
|
|
223
|
+
await writeFile(join(tablePath, ".prepend.config"), "");
|
|
224
|
+
else
|
|
225
|
+
await unlink(join(tablePath, ".prepend.config"));
|
|
226
|
+
}
|
|
227
|
+
if (config.name) {
|
|
228
|
+
await rename(tablePath, join(this.databasePath, config.name));
|
|
229
|
+
// replace table name in other linked tables (relationship)
|
|
230
|
+
for await (const schemaPath of glob("**/schema.json", {
|
|
231
|
+
cwd: this.databasePath,
|
|
232
|
+
}))
|
|
233
|
+
await this.replaceStringInFile(schemaPath, `"table": "${tableName}"`, `"table": "${config.name}"`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
this.tablesMap.delete(tableName);
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Get table schema and config
|
|
240
|
+
*
|
|
241
|
+
* @param {string} tableName
|
|
242
|
+
* @return {*} {Promise<TableObject>}
|
|
243
|
+
*/
|
|
244
|
+
async getTable(tableName, encodeIDs = true) {
|
|
245
|
+
const tablePath = join(this.databasePath, tableName);
|
|
246
|
+
if (!(await File.isExists(tablePath)))
|
|
247
|
+
throw this.createError("TABLE_NOT_EXISTS", tableName);
|
|
248
|
+
if (!this.tablesMap.has(tableName))
|
|
249
|
+
this.tablesMap.set(tableName, {
|
|
250
|
+
schema: await this.getTableSchema(tableName, encodeIDs),
|
|
251
|
+
config: {
|
|
252
|
+
compression: await File.isExists(join(tablePath, ".compression.config")),
|
|
253
|
+
cache: await File.isExists(join(tablePath, ".cache.config")),
|
|
254
|
+
prepend: await File.isExists(join(tablePath, ".prepend.config")),
|
|
255
|
+
},
|
|
256
|
+
});
|
|
257
|
+
return this.tablesMap.get(tableName);
|
|
258
|
+
}
|
|
259
|
+
async getTableSchema(tableName, encodeIDs = true) {
|
|
260
|
+
const tablePath = join(this.databasePath, tableName);
|
|
261
|
+
if (!(await File.isExists(join(tablePath, "schema.json"))))
|
|
262
|
+
return undefined;
|
|
263
|
+
const schemaFile = await readFile(join(tablePath, "schema.json"), "utf8");
|
|
264
|
+
if (!schemaFile)
|
|
265
|
+
return undefined;
|
|
266
|
+
let schema = JSON.parse(schemaFile);
|
|
267
|
+
schema = [
|
|
268
|
+
{
|
|
269
|
+
id: 0,
|
|
270
|
+
key: "id",
|
|
271
|
+
type: "id",
|
|
272
|
+
required: true,
|
|
273
|
+
},
|
|
274
|
+
...schema,
|
|
275
|
+
{
|
|
276
|
+
id: -1,
|
|
277
|
+
key: "createdAt",
|
|
278
|
+
type: "date",
|
|
279
|
+
required: true,
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
id: -2,
|
|
283
|
+
key: "updatedAt",
|
|
284
|
+
type: "date",
|
|
285
|
+
},
|
|
286
|
+
];
|
|
287
|
+
if (!encodeIDs)
|
|
288
|
+
return schema;
|
|
289
|
+
return UtilsServer.encodeSchemaID(schema, this.salt);
|
|
290
|
+
}
|
|
291
|
+
async throwErrorIfTableEmpty(tableName) {
|
|
292
|
+
const table = await this.getTable(tableName, false);
|
|
293
|
+
if (!table.schema)
|
|
294
|
+
throw this.createError("NO_SCHEMA", tableName);
|
|
295
|
+
if (!(await File.isExists(join(this.databasePath, tableName, `id${this.getFileExtension(tableName)}`))))
|
|
296
|
+
throw this.createError("TABLE_EMPTY", tableName);
|
|
297
|
+
}
|
|
298
|
+
_validateData(data, schema, skipRequiredField = false) {
|
|
299
|
+
if (Utils.isArrayOfObjects(data))
|
|
300
|
+
for (const single_data of data)
|
|
301
|
+
this._validateData(single_data, schema, skipRequiredField);
|
|
302
|
+
else if (Utils.isObject(data)) {
|
|
303
|
+
for (const field of schema) {
|
|
304
|
+
if (!Object.hasOwn(data, field.key) ||
|
|
305
|
+
data[field.key] === null ||
|
|
306
|
+
data[field.key] === undefined ||
|
|
307
|
+
data[field.key] === "") {
|
|
308
|
+
if (field.required && !skipRequiredField)
|
|
309
|
+
throw this.createError("FIELD_REQUIRED", field.key);
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
if (!Utils.validateFieldType(data[field.key], field.type, (field.type === "array" || field.type === "object") &&
|
|
313
|
+
field.children &&
|
|
314
|
+
!Utils.isArrayOfObjects(field.children)
|
|
315
|
+
? field.children
|
|
316
|
+
: undefined))
|
|
317
|
+
throw this.createError("INVALID_TYPE", [
|
|
318
|
+
field.key,
|
|
319
|
+
Array.isArray(field.type) ? field.type.join(", ") : field.type,
|
|
320
|
+
data[field.key],
|
|
321
|
+
]);
|
|
322
|
+
if ((field.type === "array" || field.type === "object") &&
|
|
323
|
+
field.children &&
|
|
324
|
+
Utils.isArrayOfObjects(field.children))
|
|
325
|
+
this._validateData(data[field.key], field.children, skipRequiredField);
|
|
326
|
+
else {
|
|
327
|
+
if (field.regex) {
|
|
328
|
+
const regex = UtilsServer.getCachedRegex(field.regex);
|
|
329
|
+
if (!regex.test(data[field.key]))
|
|
330
|
+
throw this.createError("INVALID_REGEX_MATCH", [field.key]);
|
|
331
|
+
}
|
|
332
|
+
if (field.unique) {
|
|
333
|
+
let uniqueKey;
|
|
334
|
+
if (typeof field.unique === "boolean")
|
|
335
|
+
uniqueKey = randomInt(55);
|
|
336
|
+
else
|
|
337
|
+
uniqueKey = field.unique;
|
|
338
|
+
if (!this.uniqueMap.has(uniqueKey))
|
|
339
|
+
this.uniqueMap.set(uniqueKey, {
|
|
340
|
+
exclude: new Set(),
|
|
341
|
+
columnsValues: new Map(),
|
|
342
|
+
});
|
|
343
|
+
if (!this.uniqueMap
|
|
344
|
+
.get(uniqueKey)
|
|
345
|
+
.columnsValues.has(field.id))
|
|
346
|
+
this.uniqueMap
|
|
347
|
+
.get(uniqueKey)
|
|
348
|
+
.columnsValues.set(field.id, new Set());
|
|
349
|
+
if (data.id)
|
|
350
|
+
this.uniqueMap.get(uniqueKey).exclude.add(-data.id);
|
|
351
|
+
this.uniqueMap
|
|
352
|
+
.get(uniqueKey)
|
|
353
|
+
.columnsValues.get(field.id)
|
|
354
|
+
.add(data[field.key]);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
async validateData(tableName, data, skipRequiredField = false) {
|
|
361
|
+
// Skip ID and (created|updated)At
|
|
362
|
+
this._validateData(data, this.tablesMap.get(tableName).schema.slice(1, -2), skipRequiredField);
|
|
363
|
+
await this.checkUnique(tableName);
|
|
364
|
+
}
|
|
365
|
+
cleanObject(obj) {
|
|
366
|
+
const cleanedObject = Object.entries(obj).reduce((acc, [key, value]) => {
|
|
367
|
+
if (value !== undefined && value !== null && value !== "")
|
|
368
|
+
acc[key] = value;
|
|
369
|
+
return acc;
|
|
370
|
+
}, {});
|
|
371
|
+
return Object.keys(cleanedObject).length > 0 ? cleanedObject : null;
|
|
372
|
+
}
|
|
373
|
+
formatField(value, fieldType, fieldChildrenType, _formatOnlyAvailiableKeys) {
|
|
374
|
+
if (Array.isArray(fieldType))
|
|
375
|
+
fieldType = Utils.detectFieldType(value, fieldType) ?? fieldType[0];
|
|
376
|
+
if (!value)
|
|
377
|
+
return null;
|
|
378
|
+
if (Array.isArray(value) && !["array", "json"].includes(fieldType))
|
|
379
|
+
value = value[0];
|
|
380
|
+
switch (fieldType) {
|
|
381
|
+
case "array":
|
|
382
|
+
if (!fieldChildrenType)
|
|
383
|
+
return null;
|
|
384
|
+
if (!Array.isArray(value))
|
|
385
|
+
value = [value];
|
|
386
|
+
if (Utils.isArrayOfObjects(fieldChildrenType))
|
|
387
|
+
return this._formatData(value, fieldChildrenType);
|
|
388
|
+
if (!value.length)
|
|
389
|
+
return null;
|
|
390
|
+
return value.map((_value) => this.formatField(_value, fieldChildrenType));
|
|
391
|
+
case "object":
|
|
392
|
+
if (Utils.isArrayOfObjects(fieldChildrenType))
|
|
393
|
+
return this._formatData(value, fieldChildrenType, _formatOnlyAvailiableKeys);
|
|
394
|
+
break;
|
|
395
|
+
case "table":
|
|
396
|
+
if (Utils.isObject(value)) {
|
|
397
|
+
if (Object.hasOwn(value, "id") &&
|
|
398
|
+
(Utils.isValidID(value.id) ||
|
|
399
|
+
Utils.isNumber(value.id)))
|
|
400
|
+
return Utils.isNumber(value.id)
|
|
401
|
+
? Number(value.id)
|
|
402
|
+
: UtilsServer.decodeID(value.id, this.salt);
|
|
403
|
+
}
|
|
404
|
+
else if (Utils.isValidID(value) || Utils.isNumber(value))
|
|
405
|
+
return Utils.isNumber(value)
|
|
406
|
+
? Number(value)
|
|
407
|
+
: UtilsServer.decodeID(value, this.salt);
|
|
408
|
+
break;
|
|
409
|
+
case "password":
|
|
410
|
+
return Utils.isPassword(value)
|
|
411
|
+
? value
|
|
412
|
+
: UtilsServer.hashPassword(String(value));
|
|
413
|
+
case "number":
|
|
414
|
+
return Utils.isNumber(value) ? Number(value) : null;
|
|
415
|
+
case "id":
|
|
416
|
+
return Utils.isNumber(value)
|
|
417
|
+
? value
|
|
418
|
+
: UtilsServer.decodeID(value, this.salt);
|
|
419
|
+
case "json": {
|
|
420
|
+
if (typeof value === "string" && Utils.isStringified(value))
|
|
421
|
+
return value;
|
|
422
|
+
if (Utils.isObject(value)) {
|
|
423
|
+
const cleanedObject = this.cleanObject(value);
|
|
424
|
+
if (cleanedObject)
|
|
425
|
+
return Inison.stringify(cleanedObject);
|
|
426
|
+
}
|
|
427
|
+
else
|
|
428
|
+
return Inison.stringify(value);
|
|
429
|
+
return null;
|
|
430
|
+
}
|
|
431
|
+
default:
|
|
432
|
+
return value;
|
|
433
|
+
}
|
|
434
|
+
return null;
|
|
435
|
+
}
|
|
436
|
+
async checkUnique(tableName) {
|
|
437
|
+
const tablePath = join(this.databasePath, tableName);
|
|
438
|
+
const flattenSchema = Utils.flattenSchema(this.tablesMap.get(tableName).schema);
|
|
439
|
+
for await (const [_uniqueID, valueObject] of this.uniqueMap) {
|
|
440
|
+
let valueExisted = false;
|
|
441
|
+
for await (const [columnID, values] of valueObject.columnsValues) {
|
|
442
|
+
const field = flattenSchema.find(({ id }) => id === columnID);
|
|
443
|
+
const [searchResult, totalLines] = await File.search(join(tablePath, `${columnID}${this.getFileExtension(tableName)}`), "[]", values, undefined, valueObject.exclude, field.type, field.children, 1, undefined, false, this.salt);
|
|
444
|
+
if (searchResult && totalLines > 0) {
|
|
445
|
+
if (valueExisted)
|
|
446
|
+
throw this.createError("FIELD_UNIQUE", [
|
|
447
|
+
field.key,
|
|
448
|
+
Array.isArray(values) ? values.join(", ") : values,
|
|
449
|
+
]);
|
|
450
|
+
valueExisted = true;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
this.uniqueMap = new Map();
|
|
455
|
+
}
|
|
456
|
+
_formatData(data, schema, formatOnlyAvailiableKeys) {
|
|
457
|
+
const clonedData = JSON.parse(JSON.stringify(data));
|
|
458
|
+
if (Utils.isArrayOfObjects(clonedData))
|
|
459
|
+
return clonedData.map((singleData) => this._formatData(singleData, schema, formatOnlyAvailiableKeys));
|
|
460
|
+
if (Utils.isObject(clonedData)) {
|
|
461
|
+
const RETURN = {};
|
|
462
|
+
for (const field of schema) {
|
|
463
|
+
if (!Object.hasOwn(clonedData, field.key)) {
|
|
464
|
+
if (formatOnlyAvailiableKeys)
|
|
465
|
+
continue;
|
|
466
|
+
RETURN[field.key] = this.getDefaultValue(field);
|
|
467
|
+
continue;
|
|
468
|
+
}
|
|
469
|
+
RETURN[field.key] = this.formatField(clonedData[field.key], field.type, field.children, formatOnlyAvailiableKeys);
|
|
470
|
+
}
|
|
471
|
+
return RETURN;
|
|
472
|
+
}
|
|
473
|
+
return [];
|
|
474
|
+
}
|
|
475
|
+
formatData(tableName, data, formatOnlyAvailiableKeys) {
|
|
476
|
+
data = this._formatData(data, this.tablesMap.get(tableName).schema, formatOnlyAvailiableKeys);
|
|
477
|
+
}
|
|
478
|
+
getDefaultValue(field) {
|
|
479
|
+
if (Array.isArray(field.type))
|
|
480
|
+
return this.getDefaultValue({
|
|
481
|
+
...field,
|
|
482
|
+
type: field.type.sort((a, b) => Number(b === "array") - Number(a === "array") ||
|
|
483
|
+
Number(a === "string") - Number(b === "string") ||
|
|
484
|
+
Number(a === "number") - Number(b === "number"))[0],
|
|
485
|
+
});
|
|
486
|
+
switch (field.type) {
|
|
487
|
+
case "array":
|
|
488
|
+
case "object": {
|
|
489
|
+
if (!field.children || !Utils.isArrayOfObjects(field.children))
|
|
490
|
+
return null;
|
|
491
|
+
const RETURN = {};
|
|
492
|
+
for (const f of field.children)
|
|
493
|
+
RETURN[f.key] = this.getDefaultValue(f);
|
|
494
|
+
return RETURN;
|
|
495
|
+
}
|
|
496
|
+
case "boolean":
|
|
497
|
+
return false;
|
|
498
|
+
default:
|
|
499
|
+
return null;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
_combineObjectsToArray(input) {
|
|
503
|
+
return input.reduce((result, current) => {
|
|
504
|
+
for (const [key, value] of Object.entries(current))
|
|
505
|
+
if (Object.hasOwn(result, key) && Array.isArray(result[key]))
|
|
506
|
+
result[key].push(value);
|
|
507
|
+
else
|
|
508
|
+
result[key] = [value];
|
|
509
|
+
return result;
|
|
510
|
+
}, {});
|
|
511
|
+
}
|
|
512
|
+
_CombineData(data, prefix) {
|
|
513
|
+
if (Utils.isArrayOfObjects(data))
|
|
514
|
+
return this._combineObjectsToArray(data.map((single_data) => this._CombineData(single_data)));
|
|
515
|
+
const RETURN = {};
|
|
516
|
+
for (const [key, value] of Object.entries(data)) {
|
|
517
|
+
if (Utils.isObject(value))
|
|
518
|
+
Object.assign(RETURN, this._CombineData(value, `${key}.`));
|
|
519
|
+
else if (Utils.isArrayOfObjects(value)) {
|
|
520
|
+
Object.assign(RETURN, this._CombineData(this._combineObjectsToArray(value), `${(prefix ?? "") + key}.`));
|
|
521
|
+
}
|
|
522
|
+
else if (Utils.isArrayOfArrays(value) &&
|
|
523
|
+
value.every(Utils.isArrayOfObjects))
|
|
524
|
+
Object.assign(RETURN, this._CombineData(this._combineObjectsToArray(value.map(this._combineObjectsToArray)), `${(prefix ?? "") + key}.`));
|
|
525
|
+
else
|
|
526
|
+
RETURN[(prefix ?? "") + key] = File.encode(value);
|
|
527
|
+
}
|
|
528
|
+
return RETURN;
|
|
529
|
+
}
|
|
530
|
+
joinPathesContents(tableName, data) {
|
|
531
|
+
const tablePath = join(this.databasePath, tableName), combinedData = this._CombineData(data);
|
|
532
|
+
const newCombinedData = {};
|
|
533
|
+
for (const [key, value] of Object.entries(combinedData))
|
|
534
|
+
newCombinedData[join(tablePath, `${key}${this.getFileExtension(tableName)}`)] = value;
|
|
535
|
+
return newCombinedData;
|
|
536
|
+
}
|
|
537
|
+
_processSchemaDataHelper(RETURN, item, index, field) {
|
|
538
|
+
// If the item is an object, we need to process its children
|
|
539
|
+
if (Utils.isObject(item)) {
|
|
540
|
+
if (!RETURN[index])
|
|
541
|
+
RETURN[index] = {}; // Ensure the index exists
|
|
542
|
+
if (!RETURN[index][field.key])
|
|
543
|
+
RETURN[index][field.key] = [];
|
|
544
|
+
// Process children fields (recursive if needed)
|
|
545
|
+
for (const child_field of field.children.filter((children) => children.type === "array" &&
|
|
546
|
+
Utils.isArrayOfObjects(children.children))) {
|
|
547
|
+
if (Utils.isObject(item[child_field.key])) {
|
|
548
|
+
for (const [key, value] of Object.entries(item[child_field.key])) {
|
|
549
|
+
for (let _i = 0; _i < value.length; _i++) {
|
|
550
|
+
if ((Array.isArray(value[_i]) && Utils.isArrayOfNulls(value[_i])) ||
|
|
551
|
+
value[_i] === null)
|
|
552
|
+
continue;
|
|
553
|
+
if (!RETURN[index][field.key][_i])
|
|
554
|
+
RETURN[index][field.key][_i] = {};
|
|
555
|
+
if (!RETURN[index][field.key][_i][child_field.key])
|
|
556
|
+
RETURN[index][field.key][_i][child_field.key] = [];
|
|
557
|
+
if (!Array.isArray(value[_i])) {
|
|
558
|
+
if (!RETURN[index][field.key][_i][child_field.key][0])
|
|
559
|
+
RETURN[index][field.key][_i][child_field.key][0] = {};
|
|
560
|
+
RETURN[index][field.key][_i][child_field.key][0][key] =
|
|
561
|
+
value[_i];
|
|
562
|
+
}
|
|
563
|
+
else {
|
|
564
|
+
value[_i].forEach((_element, _index) => {
|
|
565
|
+
// Recursive call to handle nested structure
|
|
566
|
+
this._processSchemaDataHelper(RETURN, _element, _index, child_field);
|
|
567
|
+
// Perform property assignments
|
|
568
|
+
if (!RETURN[index][field.key][_i][child_field.key][_index])
|
|
569
|
+
RETURN[index][field.key][_i][child_field.key][_index] = {};
|
|
570
|
+
RETURN[index][field.key][_i][child_field.key][_index][key] =
|
|
571
|
+
_element;
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
async processSchemaData(tableName, schema, linesNumber, options, prefix) {
|
|
581
|
+
const RETURN = {};
|
|
582
|
+
for (const field of schema) {
|
|
583
|
+
// If the field is of simple type (non-recursive), process it directly
|
|
584
|
+
if (this.isSimpleField(field.type)) {
|
|
585
|
+
await this.processSimpleField(tableName, field, linesNumber, RETURN, options, prefix);
|
|
586
|
+
}
|
|
587
|
+
else if (this.isArrayField(field.type)) {
|
|
588
|
+
// Process array fields (recursive if needed)
|
|
589
|
+
await this.processArrayField(tableName, field, linesNumber, RETURN, options, prefix);
|
|
590
|
+
}
|
|
591
|
+
else if (this.isObjectField(field.type)) {
|
|
592
|
+
// Process object fields (recursive if needed)
|
|
593
|
+
await this.processObjectField(tableName, field, linesNumber, RETURN, options, prefix);
|
|
594
|
+
}
|
|
595
|
+
else if (this.isTableField(field.type)) {
|
|
596
|
+
// Process table reference fields
|
|
597
|
+
await this.processTableField(tableName, field, linesNumber, RETURN, options, prefix);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
return RETURN;
|
|
601
|
+
}
|
|
602
|
+
// Helper function to determine if a field is simple
|
|
603
|
+
isSimpleField(fieldType) {
|
|
604
|
+
const complexTypes = ["array", "object", "table"];
|
|
605
|
+
if (Array.isArray(fieldType))
|
|
606
|
+
return fieldType.every((type) => typeof type === "string" && !complexTypes.includes(type));
|
|
607
|
+
return !complexTypes.includes(fieldType);
|
|
608
|
+
}
|
|
609
|
+
// Process a simple field (non-recursive)
|
|
610
|
+
async processSimpleField(tableName, field, linesNumber, RETURN, _options, prefix) {
|
|
611
|
+
const fieldPath = join(this.databasePath, tableName, `${prefix ?? ""}${field.key}${this.getFileExtension(tableName)}`);
|
|
612
|
+
if (await File.isExists(fieldPath)) {
|
|
613
|
+
const items = await File.get(fieldPath, linesNumber, field.type, field.children, this.salt);
|
|
614
|
+
if (items) {
|
|
615
|
+
for (const [index, item] of Object.entries(items)) {
|
|
616
|
+
if (typeof item === "undefined")
|
|
617
|
+
continue; // Skip undefined items
|
|
618
|
+
if (!RETURN[index])
|
|
619
|
+
RETURN[index] = {}; // Ensure the index exists
|
|
620
|
+
RETURN[index][field.key] = item; // Assign item to the RETURN object
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
// Helper function to check if the field type is array
|
|
626
|
+
isArrayField(fieldType) {
|
|
627
|
+
return ((Array.isArray(fieldType) &&
|
|
628
|
+
fieldType.every((type) => typeof type === "string") &&
|
|
629
|
+
fieldType.includes("array")) ||
|
|
630
|
+
fieldType === "array");
|
|
631
|
+
}
|
|
632
|
+
// Process array fields (recursive if needed)
|
|
633
|
+
async processArrayField(tableName, field, linesNumber, RETURN, options, prefix) {
|
|
634
|
+
if (Array.isArray(field.children)) {
|
|
635
|
+
if (this.isSimpleField(field.children)) {
|
|
636
|
+
await this.processSimpleField(tableName, field, linesNumber, RETURN, options, prefix);
|
|
637
|
+
}
|
|
638
|
+
else if (this.isTableField(field.children)) {
|
|
639
|
+
await this.processTableField(tableName, field, linesNumber, RETURN, options, prefix);
|
|
640
|
+
}
|
|
641
|
+
else {
|
|
642
|
+
// Handling array of objects and filtering nested arrays
|
|
643
|
+
const nestedArrayFields = field.children.filter((children) => children.type === "array" &&
|
|
644
|
+
Utils.isArrayOfObjects(children.children));
|
|
645
|
+
if (nestedArrayFields.length > 0) {
|
|
646
|
+
// one of children has array field type and has children array of object = Schema
|
|
647
|
+
const childItems = await this.processSchemaData(tableName, nestedArrayFields, linesNumber, options, `${(prefix ?? "") + field.key}.`);
|
|
648
|
+
if (childItems)
|
|
649
|
+
for (const [index, item] of Object.entries(childItems))
|
|
650
|
+
this._processSchemaDataHelper(RETURN, item, index, field);
|
|
651
|
+
// Remove nested arrays after processing
|
|
652
|
+
field.children = field.children.filter((children) => !nestedArrayFields.map(({ key }) => key).includes(children.key));
|
|
653
|
+
}
|
|
654
|
+
// Process remaining items for the field's children
|
|
655
|
+
const items = await this.processSchemaData(tableName, field.children, linesNumber, options, `${(prefix ?? "") + field.key}.`);
|
|
656
|
+
// Process the items after retrieval
|
|
657
|
+
if (items) {
|
|
658
|
+
for (const [index, item] of Object.entries(items)) {
|
|
659
|
+
if (typeof item === "undefined")
|
|
660
|
+
continue; // Skip undefined items
|
|
661
|
+
if (!RETURN[index])
|
|
662
|
+
RETURN[index] = {};
|
|
663
|
+
if (Utils.isObject(item)) {
|
|
664
|
+
if (!Utils.isArrayOfNulls(Object.values(item))) {
|
|
665
|
+
if (RETURN[index][field.key])
|
|
666
|
+
Object.entries(item).forEach(([key, value], _index) => {
|
|
667
|
+
for (let _index = 0; _index < value.length; _index++)
|
|
668
|
+
if (RETURN[index][field.key][_index])
|
|
669
|
+
Object.assign(RETURN[index][field.key][_index], {
|
|
670
|
+
[key]: value[_index],
|
|
671
|
+
});
|
|
672
|
+
else
|
|
673
|
+
RETURN[index][field.key][_index] = {
|
|
674
|
+
[key]: value[_index],
|
|
675
|
+
};
|
|
676
|
+
});
|
|
677
|
+
else if (Object.values(item).every((_i) => Utils.isArrayOfArrays(_i)) &&
|
|
678
|
+
prefix)
|
|
679
|
+
RETURN[index][field.key] = item;
|
|
680
|
+
else {
|
|
681
|
+
RETURN[index][field.key] = [];
|
|
682
|
+
Object.entries(item).forEach(([key, value], _ind) => {
|
|
683
|
+
if (!Array.isArray(value)) {
|
|
684
|
+
RETURN[index][field.key][_ind] = {};
|
|
685
|
+
RETURN[index][field.key][_ind][key] = value;
|
|
686
|
+
}
|
|
687
|
+
else
|
|
688
|
+
for (let _i = 0; _i < value.length; _i++) {
|
|
689
|
+
if (value[_i] === null ||
|
|
690
|
+
(Array.isArray(value[_i]) &&
|
|
691
|
+
Utils.isArrayOfNulls(value[_i])))
|
|
692
|
+
continue;
|
|
693
|
+
if (!RETURN[index][field.key][_i])
|
|
694
|
+
RETURN[index][field.key][_i] = {};
|
|
695
|
+
RETURN[index][field.key][_i][key] = value[_i];
|
|
696
|
+
}
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
else
|
|
701
|
+
RETURN[index][field.key] = null;
|
|
702
|
+
}
|
|
703
|
+
else
|
|
704
|
+
RETURN[index][field.key] = item;
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
else if (this.isSimpleField(field.children)) {
|
|
710
|
+
// If `children` is FieldType, handle it as an array of simple types (no recursion needed here)
|
|
711
|
+
await this.processSimpleField(tableName, field, linesNumber, RETURN, options, prefix);
|
|
712
|
+
}
|
|
713
|
+
else if (this.isTableField(field.children)) {
|
|
714
|
+
await this.processTableField(tableName, field, linesNumber, RETURN, options, prefix);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
// Helper function to check if the field type is object
|
|
718
|
+
isObjectField(fieldType) {
|
|
719
|
+
return (fieldType === "object" ||
|
|
720
|
+
(Array.isArray(fieldType) &&
|
|
721
|
+
fieldType.every((type) => typeof type === "string") &&
|
|
722
|
+
fieldType.includes("object")));
|
|
723
|
+
}
|
|
724
|
+
// Process object fields (recursive if needed)
|
|
725
|
+
async processObjectField(tableName, field, linesNumber, RETURN, options, prefix) {
|
|
726
|
+
if (Array.isArray(field.children)) {
|
|
727
|
+
// If `children` is a Schema (array of Field objects), recurse
|
|
728
|
+
const items = await this.processSchemaData(tableName, field.children, linesNumber, options, `${prefix ?? ""}${field.key}.`);
|
|
729
|
+
for (const [index, item] of Object.entries(items)) {
|
|
730
|
+
if (typeof item === "undefined")
|
|
731
|
+
continue; // Skip undefined items
|
|
732
|
+
if (!RETURN[index])
|
|
733
|
+
RETURN[index] = {};
|
|
734
|
+
if (Utils.isObject(item)) {
|
|
735
|
+
if (!Object.values(item).every((i) => i === null))
|
|
736
|
+
RETURN[index][field.key] = item;
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
// Helper function to check if the field type is table
|
|
742
|
+
isTableField(fieldType) {
|
|
743
|
+
return (fieldType === "table" ||
|
|
744
|
+
(Array.isArray(fieldType) &&
|
|
745
|
+
fieldType.every((type) => typeof type === "string") &&
|
|
746
|
+
fieldType.includes("table")));
|
|
747
|
+
}
|
|
748
|
+
// Process table reference fields
|
|
749
|
+
async processTableField(tableName, field, linesNumber, RETURN, options, prefix) {
|
|
750
|
+
if (field.table &&
|
|
751
|
+
(await File.isExists(join(this.databasePath, field.table)))) {
|
|
752
|
+
const fieldPath = join(this.databasePath, tableName, `${prefix ?? ""}${field.key}${this.getFileExtension(tableName)}`);
|
|
753
|
+
if (await File.isExists(fieldPath)) {
|
|
754
|
+
const itemsIDs = await File.get(fieldPath, linesNumber, field.type, field.children, this.salt);
|
|
755
|
+
const isArrayField = this.isArrayField(field.type);
|
|
756
|
+
if (itemsIDs) {
|
|
757
|
+
const searchableIDs = new Map();
|
|
758
|
+
for (const [lineNumber, lineContent] of Object.entries(itemsIDs)) {
|
|
759
|
+
if (typeof lineContent === "undefined")
|
|
760
|
+
continue; // Skip undefined items
|
|
761
|
+
if (!RETURN[lineNumber])
|
|
762
|
+
RETURN[lineNumber] = {};
|
|
763
|
+
if (lineContent !== null && lineContent !== undefined)
|
|
764
|
+
searchableIDs.set(lineNumber, lineContent);
|
|
765
|
+
}
|
|
766
|
+
if (searchableIDs.size) {
|
|
767
|
+
const items = await this.get(field.table, isArrayField
|
|
768
|
+
? Array.from(new Set(Array.from(searchableIDs.values()).flat()))
|
|
769
|
+
: Array.from(new Set(searchableIDs.values())), {
|
|
770
|
+
...options,
|
|
771
|
+
perPage: Number.POSITIVE_INFINITY,
|
|
772
|
+
columns: options.columns
|
|
773
|
+
?.filter((column) => column.includes(`${field.key}.`))
|
|
774
|
+
.map((column) => column.replace(`${field.key}.`, "")),
|
|
775
|
+
});
|
|
776
|
+
if (items) {
|
|
777
|
+
for (const [lineNumber, lineContent] of searchableIDs.entries()) {
|
|
778
|
+
const foundedItem = isArrayField
|
|
779
|
+
? items.filter(({ id }) => lineContent.includes(id))
|
|
780
|
+
: items.find(({ id }) => id === lineContent);
|
|
781
|
+
if (foundedItem)
|
|
782
|
+
RETURN[lineNumber][field.key] = foundedItem;
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
async applyCriteria(tableName, schema, options, criteria, allTrue, searchIn) {
|
|
791
|
+
const tablePath = join(this.databasePath, tableName);
|
|
792
|
+
let RETURN = {};
|
|
793
|
+
if (!criteria)
|
|
794
|
+
return [null, null];
|
|
795
|
+
if (criteria.and && Utils.isObject(criteria.and)) {
|
|
796
|
+
const [searchResult, lineNumbers] = await this.applyCriteria(tableName, schema, options, criteria.and, true, searchIn);
|
|
797
|
+
if (searchResult) {
|
|
798
|
+
RETURN = Utils.deepMerge(RETURN, Object.fromEntries(Object.entries(searchResult).filter(([_k, v], _i) => Object.keys(v).filter((key) => Object.keys(criteria.and).includes(key)).length)));
|
|
799
|
+
delete criteria.and;
|
|
800
|
+
searchIn = lineNumbers;
|
|
801
|
+
}
|
|
802
|
+
else
|
|
803
|
+
return [null, null];
|
|
804
|
+
}
|
|
805
|
+
const criteriaOR = criteria.or;
|
|
806
|
+
if (criteriaOR)
|
|
807
|
+
delete criteria.or;
|
|
808
|
+
if (Object.keys(criteria).length > 0) {
|
|
809
|
+
if (allTrue === undefined)
|
|
810
|
+
allTrue = true;
|
|
811
|
+
let index = -1;
|
|
812
|
+
for await (const [key, value] of Object.entries(criteria)) {
|
|
813
|
+
const field = Utils.getField(key, schema);
|
|
814
|
+
index++;
|
|
815
|
+
let searchOperator = undefined, searchComparedAtValue = undefined, searchLogicalOperator = undefined;
|
|
816
|
+
if (Utils.isObject(value)) {
|
|
817
|
+
if (value?.or &&
|
|
818
|
+
Array.isArray(value?.or)) {
|
|
819
|
+
const searchCriteria = (value?.or)
|
|
820
|
+
.map((single_or) => typeof single_or === "string"
|
|
821
|
+
? Utils.FormatObjectCriteriaValue(single_or)
|
|
822
|
+
: ["=", single_or])
|
|
823
|
+
.filter((a) => a);
|
|
824
|
+
if (searchCriteria.length > 0) {
|
|
825
|
+
searchOperator = searchCriteria.map((single_or) => single_or[0]);
|
|
826
|
+
searchComparedAtValue = searchCriteria.map((single_or) => single_or[1]);
|
|
827
|
+
searchLogicalOperator = "or";
|
|
828
|
+
}
|
|
829
|
+
delete value.or;
|
|
830
|
+
}
|
|
831
|
+
if (value?.and &&
|
|
832
|
+
Array.isArray(value?.and)) {
|
|
833
|
+
const searchCriteria = (value?.and)
|
|
834
|
+
.map((single_and) => typeof single_and === "string"
|
|
835
|
+
? Utils.FormatObjectCriteriaValue(single_and)
|
|
836
|
+
: ["=", single_and])
|
|
837
|
+
.filter((a) => a);
|
|
838
|
+
if (searchCriteria.length > 0) {
|
|
839
|
+
searchOperator = searchCriteria.map((single_and) => single_and[0]);
|
|
840
|
+
searchComparedAtValue = searchCriteria.map((single_and) => single_and[1]);
|
|
841
|
+
searchLogicalOperator = "and";
|
|
842
|
+
}
|
|
843
|
+
delete value.and;
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
else if (Array.isArray(value)) {
|
|
847
|
+
const searchCriteria = value
|
|
848
|
+
.map((single) => typeof single === "string"
|
|
849
|
+
? Utils.FormatObjectCriteriaValue(single)
|
|
850
|
+
: ["=", single])
|
|
851
|
+
.filter((a) => a);
|
|
852
|
+
if (searchCriteria.length > 0) {
|
|
853
|
+
searchOperator = searchCriteria.map((single) => single[0]);
|
|
854
|
+
searchComparedAtValue = searchCriteria.map((single) => single[1]);
|
|
855
|
+
searchLogicalOperator = "and";
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
else if (typeof value === "string") {
|
|
859
|
+
const ComparisonOperatorValue = Utils.FormatObjectCriteriaValue(value);
|
|
860
|
+
if (ComparisonOperatorValue) {
|
|
861
|
+
searchOperator = ComparisonOperatorValue[0];
|
|
862
|
+
searchComparedAtValue = ComparisonOperatorValue[1];
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
else {
|
|
866
|
+
searchOperator = "=";
|
|
867
|
+
searchComparedAtValue = value;
|
|
868
|
+
}
|
|
869
|
+
const [searchResult, totalLines, linesNumbers] = await File.search(join(tablePath, `${key}${this.getFileExtension(tableName)}`), searchOperator ?? "=", searchComparedAtValue ?? null, searchLogicalOperator, allTrue ? searchIn : undefined, field?.type, field?.children, options.perPage, (options.page - 1) * options.perPage + 1, true, this.salt);
|
|
870
|
+
if (searchResult) {
|
|
871
|
+
RETURN = Utils.deepMerge(RETURN, Object.fromEntries(Object.entries(searchResult).map(([id, value]) => [
|
|
872
|
+
id,
|
|
873
|
+
{
|
|
874
|
+
[key]: value,
|
|
875
|
+
},
|
|
876
|
+
])));
|
|
877
|
+
this.totalItems.set(`${tableName}-${key}`, totalLines);
|
|
878
|
+
if (linesNumbers?.size) {
|
|
879
|
+
if (searchIn) {
|
|
880
|
+
for (const lineNumber of linesNumbers)
|
|
881
|
+
searchIn.add(lineNumber);
|
|
882
|
+
}
|
|
883
|
+
else
|
|
884
|
+
searchIn = linesNumbers;
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
else if (allTrue)
|
|
888
|
+
return [null, null];
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
if (criteriaOR && Utils.isObject(criteriaOR)) {
|
|
892
|
+
const [searchResult, lineNumbers] = await this.applyCriteria(tableName, schema, options, criteriaOR, false, searchIn);
|
|
893
|
+
if (searchResult) {
|
|
894
|
+
RETURN = Utils.deepMerge(RETURN, searchResult);
|
|
895
|
+
if (!Object.keys(RETURN).length)
|
|
896
|
+
RETURN = {};
|
|
897
|
+
RETURN = Object.fromEntries(Object.entries(RETURN).filter(([_index, item]) => Object.keys(item).filter((key) => Object.keys(criteriaOR).includes(key)).length));
|
|
898
|
+
if (!Object.keys(RETURN).length)
|
|
899
|
+
RETURN = {};
|
|
900
|
+
searchIn = lineNumbers;
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
return [Object.keys(RETURN).length ? RETURN : null, searchIn];
|
|
904
|
+
}
|
|
905
|
+
_filterSchemaByColumns(schema, columns) {
|
|
906
|
+
return schema
|
|
907
|
+
.map((field) => {
|
|
908
|
+
if (columns.some((column) => column.startsWith("!")))
|
|
909
|
+
return columns.includes(`!${field.key}`) ? null : field;
|
|
910
|
+
if (columns.includes(field.key) || columns.includes("*"))
|
|
911
|
+
return field;
|
|
912
|
+
if ((field.type === "array" || field.type === "object") &&
|
|
913
|
+
Utils.isArrayOfObjects(field.children) &&
|
|
914
|
+
columns.filter((column) => column.startsWith(`${field.key}.`) ||
|
|
915
|
+
column.startsWith(`!${field.key}.`)).length) {
|
|
916
|
+
field.children = this._filterSchemaByColumns(field.children, columns
|
|
917
|
+
.filter((column) => column.startsWith(`${field.key}.`) ||
|
|
918
|
+
column.startsWith(`!${field.key}.`))
|
|
919
|
+
.map((column) => column.replace(`${field.key}.`, "")));
|
|
920
|
+
return field;
|
|
921
|
+
}
|
|
922
|
+
return null;
|
|
923
|
+
})
|
|
924
|
+
.filter((i) => i);
|
|
925
|
+
}
|
|
926
|
+
/**
|
|
927
|
+
* Clear table cache
|
|
928
|
+
*
|
|
929
|
+
* @param {string} tableName
|
|
930
|
+
*/
|
|
931
|
+
async clearCache(tableName) {
|
|
932
|
+
const cacheFolderPath = join(this.databasePath, tableName, ".cache");
|
|
933
|
+
await rm(cacheFolderPath, { recursive: true, force: true });
|
|
934
|
+
await mkdir(cacheFolderPath);
|
|
935
|
+
}
|
|
936
|
+
async get(tableName, where, options = {
|
|
937
|
+
page: 1,
|
|
938
|
+
perPage: 15,
|
|
939
|
+
}, onlyOne, onlyLinesNumbers) {
|
|
940
|
+
const tablePath = join(this.databasePath, tableName);
|
|
941
|
+
// Ensure options.columns is an array
|
|
942
|
+
if (options.columns) {
|
|
943
|
+
options.columns = Array.isArray(options.columns)
|
|
944
|
+
? options.columns
|
|
945
|
+
: [options.columns];
|
|
946
|
+
if (options.columns.length && !options.columns.includes("id"))
|
|
947
|
+
options.columns.push("id");
|
|
948
|
+
}
|
|
949
|
+
// Default values for page and perPage
|
|
950
|
+
options.page = options.page || 1;
|
|
951
|
+
options.perPage = options.perPage || 15;
|
|
952
|
+
let RETURN;
|
|
953
|
+
await this.getTable(tableName);
|
|
954
|
+
let schema = (await this.getTable(tableName)).schema;
|
|
955
|
+
if (!schema)
|
|
956
|
+
throw this.createError("NO_SCHEMA", tableName);
|
|
957
|
+
let pagination;
|
|
958
|
+
for await (const paginationFileName of glob("*.pagination", {
|
|
959
|
+
cwd: tablePath,
|
|
960
|
+
}))
|
|
961
|
+
pagination = parse(paginationFileName).name.split("-").map(Number);
|
|
962
|
+
if (!pagination[1])
|
|
963
|
+
return null;
|
|
964
|
+
if (options.columns?.length)
|
|
965
|
+
schema = this._filterSchemaByColumns(schema, options.columns);
|
|
966
|
+
if (where &&
|
|
967
|
+
((Array.isArray(where) && !where.length) ||
|
|
968
|
+
(Utils.isObject(where) && !Object.keys(where).length)))
|
|
969
|
+
where = undefined;
|
|
970
|
+
if (options.sort) {
|
|
971
|
+
let sortArray, awkCommand = "";
|
|
972
|
+
if (Utils.isObject(options.sort) && !Array.isArray(options.sort)) {
|
|
973
|
+
// {name: "ASC", age: "DESC"}
|
|
974
|
+
sortArray = Object.entries(options.sort).map(([key, value]) => [
|
|
975
|
+
key,
|
|
976
|
+
typeof value === "string" ? value.toLowerCase() === "asc" : value > 0,
|
|
977
|
+
]);
|
|
978
|
+
}
|
|
979
|
+
else
|
|
980
|
+
sortArray = []
|
|
981
|
+
.concat(options.sort)
|
|
982
|
+
.map((column) => [column, true]);
|
|
983
|
+
let cacheKey = "";
|
|
984
|
+
// Criteria
|
|
985
|
+
if (this.tablesMap.get(tableName).config.cache)
|
|
986
|
+
cacheKey = UtilsServer.hashString(inspect(sortArray, { sorted: true }));
|
|
987
|
+
if (where) {
|
|
988
|
+
const lineNumbers = await this.get(tableName, where, undefined, undefined, true);
|
|
989
|
+
if (!lineNumbers?.length)
|
|
990
|
+
return null;
|
|
991
|
+
const itemsIDs = Object.values((await File.get(join(tablePath, `id${this.getFileExtension(tableName)}`), lineNumbers, "number", undefined, this.salt)) ?? {}).map(Number);
|
|
992
|
+
awkCommand = `awk '${itemsIDs.map((id) => `$1 == ${id}`).join(" || ")}'`;
|
|
993
|
+
}
|
|
994
|
+
else
|
|
995
|
+
awkCommand = `awk '${Array.from({ length: options.perPage }, (_, index) => (options.page - 1) * options.perPage +
|
|
996
|
+
index +
|
|
997
|
+
1)
|
|
998
|
+
.map((lineNumber) => `NR==${lineNumber}`)
|
|
999
|
+
.join(" || ")}'`;
|
|
1000
|
+
const filesPathes = [["id", true], ...sortArray].map((column) => join(tablePath, `${column[0]}${this.getFileExtension(tableName)}`));
|
|
1001
|
+
for await (const path of filesPathes.slice(1))
|
|
1002
|
+
if (!(await File.isExists(path)))
|
|
1003
|
+
return null;
|
|
1004
|
+
// Construct the paste command to merge files and filter lines by IDs
|
|
1005
|
+
const pasteCommand = `paste '${filesPathes.join("' '")}'`;
|
|
1006
|
+
// Construct the sort command dynamically based on the number of files for sorting
|
|
1007
|
+
const index = 2;
|
|
1008
|
+
const sortColumns = sortArray
|
|
1009
|
+
.map(([key, ascending], i) => {
|
|
1010
|
+
const field = Utils.getField(key, schema);
|
|
1011
|
+
if (field)
|
|
1012
|
+
return `-k${i + index},${i + index}${Utils.isFieldType(["id", "number", "date"], field.type, field.children)
|
|
1013
|
+
? "n"
|
|
1014
|
+
: ""}${!ascending ? "r" : ""}`;
|
|
1015
|
+
return "";
|
|
1016
|
+
})
|
|
1017
|
+
.join(" ");
|
|
1018
|
+
const sortCommand = `sort ${sortColumns} -T='${join(tablePath, ".tmp")}'`;
|
|
1019
|
+
try {
|
|
1020
|
+
if (cacheKey)
|
|
1021
|
+
await File.lock(join(tablePath, ".tmp"), cacheKey);
|
|
1022
|
+
// Combine && Execute the commands synchronously
|
|
1023
|
+
let lines = (await UtilsServer.exec(this.tablesMap.get(tableName).config.cache
|
|
1024
|
+
? (await File.isExists(join(tablePath, ".cache", `${cacheKey}${this.fileExtension}`)))
|
|
1025
|
+
? `${awkCommand} '${join(tablePath, ".cache", `${cacheKey}${this.fileExtension}`)}'`
|
|
1026
|
+
: `${pasteCommand} | ${sortCommand} -o '${join(tablePath, ".cache", `${cacheKey}${this.fileExtension}`)}' && ${awkCommand} '${join(tablePath, ".cache", `${cacheKey}${this.fileExtension}`)}'`
|
|
1027
|
+
: `${pasteCommand} | ${sortCommand} | ${awkCommand}`, {
|
|
1028
|
+
encoding: "utf-8",
|
|
1029
|
+
})).stdout
|
|
1030
|
+
.trimEnd()
|
|
1031
|
+
.split("\n");
|
|
1032
|
+
if (where)
|
|
1033
|
+
lines = lines.slice((options.page - 1) * options.perPage, options.page * options.perPage);
|
|
1034
|
+
else if (!this.totalItems.has(`${tableName}-*`))
|
|
1035
|
+
this.totalItems.set(`${tableName}-*`, pagination[1]);
|
|
1036
|
+
if (!lines.length)
|
|
1037
|
+
return null;
|
|
1038
|
+
// Parse the result and extract the specified lines
|
|
1039
|
+
const outputArray = lines.map((line) => {
|
|
1040
|
+
const splitedFileColumns = line.split("\t"); // Assuming tab-separated columns
|
|
1041
|
+
const outputObject = {};
|
|
1042
|
+
// Extract values for each file, including `id${this.getFileExtension(tableName)}`
|
|
1043
|
+
filesPathes.forEach((fileName, index) => {
|
|
1044
|
+
const field = Utils.getField(parse(fileName).name, schema);
|
|
1045
|
+
if (field)
|
|
1046
|
+
outputObject[field.key] = File.decode(splitedFileColumns[index], field?.type, field?.children, this.salt);
|
|
1047
|
+
});
|
|
1048
|
+
return outputObject;
|
|
1049
|
+
});
|
|
1050
|
+
const restOfColumns = await this.get(tableName, outputArray.map(({ id }) => id), (({ sort, ...rest }) => rest)(options));
|
|
1051
|
+
return restOfColumns
|
|
1052
|
+
? outputArray.map((item) => ({
|
|
1053
|
+
...item,
|
|
1054
|
+
...restOfColumns.find(({ id }) => id === item.id),
|
|
1055
|
+
}))
|
|
1056
|
+
: outputArray;
|
|
1057
|
+
}
|
|
1058
|
+
finally {
|
|
1059
|
+
if (cacheKey)
|
|
1060
|
+
await File.unlock(join(tablePath, ".tmp"), cacheKey);
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
if (!where) {
|
|
1064
|
+
// Display all data
|
|
1065
|
+
RETURN = Object.values(await this.processSchemaData(tableName, schema, Array.from({ length: options.perPage }, (_, index) => (options.page - 1) * options.perPage +
|
|
1066
|
+
index +
|
|
1067
|
+
1), options));
|
|
1068
|
+
if (!this.totalItems.has(`${tableName}-*`))
|
|
1069
|
+
this.totalItems.set(`${tableName}-*`, pagination[1]);
|
|
1070
|
+
}
|
|
1071
|
+
else if ((Array.isArray(where) && where.every(Utils.isNumber)) ||
|
|
1072
|
+
Utils.isNumber(where)) {
|
|
1073
|
+
// "where" in this case, is the line(s) number(s) and not id(s)
|
|
1074
|
+
let lineNumbers = where;
|
|
1075
|
+
if (!Array.isArray(lineNumbers))
|
|
1076
|
+
lineNumbers = [lineNumbers];
|
|
1077
|
+
if (!this.totalItems.has(`${tableName}-*`))
|
|
1078
|
+
this.totalItems.set(`${tableName}-*`, lineNumbers.length);
|
|
1079
|
+
// useless
|
|
1080
|
+
if (onlyLinesNumbers)
|
|
1081
|
+
return lineNumbers;
|
|
1082
|
+
RETURN = Object.values((await this.processSchemaData(tableName, schema, lineNumbers, options)) ?? {});
|
|
1083
|
+
if (RETURN?.length && !Array.isArray(where))
|
|
1084
|
+
RETURN = RETURN[0];
|
|
1085
|
+
}
|
|
1086
|
+
else if ((Array.isArray(where) && where.every(Utils.isValidID)) ||
|
|
1087
|
+
Utils.isValidID(where)) {
|
|
1088
|
+
let Ids = where;
|
|
1089
|
+
if (!Array.isArray(Ids))
|
|
1090
|
+
Ids = [Ids];
|
|
1091
|
+
const [lineNumbers, countItems] = await File.search(join(tablePath, `id${this.getFileExtension(tableName)}`), "[]", Ids.map((id) => Utils.isNumber(id) ? Number(id) : UtilsServer.decodeID(id, this.salt)), undefined, undefined, "number", undefined, Ids.length, 0, !this.totalItems.has(`${tableName}-*`), this.salt);
|
|
1092
|
+
if (!lineNumbers)
|
|
1093
|
+
return null;
|
|
1094
|
+
if (!this.totalItems.has(`${tableName}-*`))
|
|
1095
|
+
this.totalItems.set(`${tableName}-*`, countItems);
|
|
1096
|
+
if (onlyLinesNumbers)
|
|
1097
|
+
return Object.keys(lineNumbers).length
|
|
1098
|
+
? Object.keys(lineNumbers).map(Number)
|
|
1099
|
+
: null;
|
|
1100
|
+
if (options.columns) {
|
|
1101
|
+
options.columns = options.columns.filter((column) => column !== "id");
|
|
1102
|
+
if (!options.columns?.length)
|
|
1103
|
+
options.columns = undefined;
|
|
1104
|
+
}
|
|
1105
|
+
RETURN = Object.values((await this.processSchemaData(tableName, schema, Object.keys(lineNumbers).map(Number), options)) ?? {});
|
|
1106
|
+
if (RETURN?.length && !Array.isArray(where))
|
|
1107
|
+
RETURN = RETURN[0];
|
|
1108
|
+
}
|
|
1109
|
+
else if (Utils.isObject(where)) {
|
|
1110
|
+
let cachedFilePath = "";
|
|
1111
|
+
// Criteria
|
|
1112
|
+
if (this.tablesMap.get(tableName).config.cache)
|
|
1113
|
+
cachedFilePath = join(tablePath, ".cache", `${UtilsServer.hashString(inspect(where, { sorted: true }))}${this.fileExtension}`);
|
|
1114
|
+
if (this.tablesMap.get(tableName).config.cache &&
|
|
1115
|
+
(await File.isExists(cachedFilePath))) {
|
|
1116
|
+
const cachedItems = (await readFile(cachedFilePath, "utf8")).split(",");
|
|
1117
|
+
if (!this.totalItems.has(`${tableName}-*`))
|
|
1118
|
+
this.totalItems.set(`${tableName}-*`, cachedItems.length);
|
|
1119
|
+
if (onlyLinesNumbers)
|
|
1120
|
+
return onlyOne ? Number(cachedItems[0]) : cachedItems.map(Number);
|
|
1121
|
+
return this.get(tableName, cachedItems
|
|
1122
|
+
.slice((options.page - 1) * options.perPage, options.page * options.perPage)
|
|
1123
|
+
.map(Number), options, onlyOne);
|
|
1124
|
+
}
|
|
1125
|
+
let linesNumbers = null;
|
|
1126
|
+
[RETURN, linesNumbers] = await this.applyCriteria(tableName, schema, options, where);
|
|
1127
|
+
if (RETURN && linesNumbers?.size) {
|
|
1128
|
+
if (!this.totalItems.has(`${tableName}-*`))
|
|
1129
|
+
this.totalItems.set(`${tableName}-*`, linesNumbers.size);
|
|
1130
|
+
if (onlyLinesNumbers)
|
|
1131
|
+
return onlyOne
|
|
1132
|
+
? linesNumbers.values().next().value
|
|
1133
|
+
: Array.from(linesNumbers);
|
|
1134
|
+
const alreadyExistsColumns = Object.keys(Object.values(RETURN)[0]), alreadyExistsColumnsIDs = Utils.flattenSchema(schema)
|
|
1135
|
+
.filter(({ key }) => alreadyExistsColumns.includes(key))
|
|
1136
|
+
.map(({ id }) => id);
|
|
1137
|
+
RETURN = Object.values(Utils.deepMerge(RETURN, await this.processSchemaData(tableName, Utils.filterSchema(schema, ({ id, type, children }) => !alreadyExistsColumnsIDs.includes(id) ||
|
|
1138
|
+
Utils.isFieldType("table", type, children)), Object.keys(RETURN).map(Number), options)));
|
|
1139
|
+
if (this.tablesMap.get(tableName).config.cache)
|
|
1140
|
+
await writeFile(cachedFilePath, Array.from(linesNumbers).join(","));
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
if (!RETURN ||
|
|
1144
|
+
(Utils.isObject(RETURN) && !Object.keys(RETURN).length) ||
|
|
1145
|
+
(Array.isArray(RETURN) && !RETURN.length))
|
|
1146
|
+
return null;
|
|
1147
|
+
const greatestTotalItems = this.totalItems.has(`${tableName}-*`)
|
|
1148
|
+
? this.totalItems.get(`${tableName}-*`)
|
|
1149
|
+
: Math.max(...[...this.totalItems.entries()]
|
|
1150
|
+
.filter(([k]) => k.startsWith(`${tableName}-`))
|
|
1151
|
+
.map(([, v]) => v));
|
|
1152
|
+
this.pageInfo[tableName] = {
|
|
1153
|
+
...(({ columns, ...restOfOptions }) => restOfOptions)(options),
|
|
1154
|
+
perPage: Array.isArray(RETURN) ? RETURN.length : 1,
|
|
1155
|
+
totalPages: Math.ceil(greatestTotalItems / options.perPage),
|
|
1156
|
+
total: greatestTotalItems,
|
|
1157
|
+
};
|
|
1158
|
+
return onlyOne && Array.isArray(RETURN) ? RETURN[0] : RETURN;
|
|
1159
|
+
}
|
|
1160
|
+
async post(tableName, data, options, returnPostedData) {
|
|
1161
|
+
if (!options)
|
|
1162
|
+
options = {
|
|
1163
|
+
page: 1,
|
|
1164
|
+
perPage: 15,
|
|
1165
|
+
};
|
|
1166
|
+
const tablePath = join(this.databasePath, tableName);
|
|
1167
|
+
await this.getTable(tableName);
|
|
1168
|
+
if (!this.tablesMap.get(tableName).schema)
|
|
1169
|
+
throw this.createError("NO_SCHEMA", tableName);
|
|
1170
|
+
if (!returnPostedData)
|
|
1171
|
+
returnPostedData = false;
|
|
1172
|
+
const keys = UtilsServer.hashString(Object.keys(Array.isArray(data) ? data[0] : data).join("."));
|
|
1173
|
+
await this.validateData(tableName, data);
|
|
1174
|
+
const renameList = [];
|
|
1175
|
+
try {
|
|
1176
|
+
await File.lock(join(tablePath, ".tmp"), keys);
|
|
1177
|
+
let paginationFilePath;
|
|
1178
|
+
for await (const fileName of glob("*.pagination", { cwd: tablePath }))
|
|
1179
|
+
paginationFilePath = join(tablePath, fileName);
|
|
1180
|
+
let [lastId, _totalItems] = parse(paginationFilePath)
|
|
1181
|
+
.name.split("-")
|
|
1182
|
+
.map(Number);
|
|
1183
|
+
this.totalItems.set(`${tableName}-*`, _totalItems);
|
|
1184
|
+
if (Utils.isArrayOfObjects(data))
|
|
1185
|
+
for (let index = 0; index < data.length; index++) {
|
|
1186
|
+
const element = data[index];
|
|
1187
|
+
element.id = ++lastId;
|
|
1188
|
+
element.createdAt = Date.now();
|
|
1189
|
+
element.updatedAt = undefined;
|
|
1190
|
+
}
|
|
1191
|
+
else {
|
|
1192
|
+
data.id = ++lastId;
|
|
1193
|
+
data.createdAt = Date.now();
|
|
1194
|
+
data.updatedAt = undefined;
|
|
1195
|
+
}
|
|
1196
|
+
this.formatData(tableName, data);
|
|
1197
|
+
const pathesContents = this.joinPathesContents(tableName, this.tablesMap.get(tableName).config.prepend
|
|
1198
|
+
? Array.isArray(data)
|
|
1199
|
+
? data.toReversed()
|
|
1200
|
+
: data
|
|
1201
|
+
: data);
|
|
1202
|
+
await Promise.allSettled(Object.entries(pathesContents).map(async ([path, content]) => renameList.push(this.tablesMap.get(tableName).config.prepend
|
|
1203
|
+
? await File.prepend(path, content)
|
|
1204
|
+
: await File.append(path, content))));
|
|
1205
|
+
await Promise.allSettled(renameList
|
|
1206
|
+
.filter(([_, filePath]) => filePath)
|
|
1207
|
+
.map(async ([tempPath, filePath]) => rename(tempPath, filePath)));
|
|
1208
|
+
if (this.tablesMap.get(tableName).config.cache)
|
|
1209
|
+
await this.clearCache(tableName);
|
|
1210
|
+
const currentValue = this.totalItems.get(`${tableName}-*`) || 0;
|
|
1211
|
+
this.totalItems.set(`${tableName}-*`, currentValue + (Array.isArray(data) ? data.length : 1));
|
|
1212
|
+
await rename(paginationFilePath, join(tablePath, `${lastId}-${this.totalItems.get(`${tableName}-*`)}.pagination`));
|
|
1213
|
+
if (returnPostedData)
|
|
1214
|
+
return this.get(tableName, this.tablesMap.get(tableName).config.prepend
|
|
1215
|
+
? Array.isArray(data)
|
|
1216
|
+
? data.map((_, index) => index + 1).toReversed()
|
|
1217
|
+
: 1
|
|
1218
|
+
: Array.isArray(data)
|
|
1219
|
+
? data
|
|
1220
|
+
.map((_, index) => this.totalItems.get(`${tableName}-*`) - index)
|
|
1221
|
+
.toReversed()
|
|
1222
|
+
: this.totalItems.get(`${tableName}-*`), options, !Utils.isArrayOfObjects(data));
|
|
1223
|
+
}
|
|
1224
|
+
finally {
|
|
1225
|
+
if (renameList.length)
|
|
1226
|
+
await Promise.allSettled(renameList.map(async ([tempPath, _]) => unlink(tempPath)));
|
|
1227
|
+
await File.unlock(join(tablePath, ".tmp"), keys);
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
async put(tableName, data, where, options = {
|
|
1231
|
+
page: 1,
|
|
1232
|
+
perPage: 15,
|
|
1233
|
+
}, returnUpdatedData) {
|
|
1234
|
+
const renameList = [];
|
|
1235
|
+
const tablePath = join(this.databasePath, tableName);
|
|
1236
|
+
await this.throwErrorIfTableEmpty(tableName);
|
|
1237
|
+
if (!where) {
|
|
1238
|
+
if (Utils.isArrayOfObjects(data)) {
|
|
1239
|
+
if (!data.every((item) => Object.hasOwn(item, "id") && Utils.isValidID(item.id)))
|
|
1240
|
+
throw this.createError("INVALID_ID");
|
|
1241
|
+
return this.put(tableName, data, data.map(({ id }) => id), options, returnUpdatedData || undefined);
|
|
1242
|
+
}
|
|
1243
|
+
if (Object.hasOwn(data, "id")) {
|
|
1244
|
+
if (!Utils.isValidID(data.id))
|
|
1245
|
+
throw this.createError("INVALID_ID", data.id);
|
|
1246
|
+
return this.put(tableName, data, data.id, options, returnUpdatedData || undefined);
|
|
1247
|
+
}
|
|
1248
|
+
await this.validateData(tableName, data, true);
|
|
1249
|
+
this.formatData(tableName, data, true);
|
|
1250
|
+
const pathesContents = this.joinPathesContents(tableName, {
|
|
1251
|
+
...(({ id, ...restOfData }) => restOfData)(data),
|
|
1252
|
+
updatedAt: Date.now(),
|
|
1253
|
+
});
|
|
1254
|
+
try {
|
|
1255
|
+
await File.lock(join(tablePath, ".tmp"));
|
|
1256
|
+
for await (const paginationFileName of glob("*.pagination", {
|
|
1257
|
+
cwd: tablePath,
|
|
1258
|
+
}))
|
|
1259
|
+
this.totalItems.set(`${tableName}-*`, parse(paginationFileName).name.split("-").map(Number)[1]);
|
|
1260
|
+
await Promise.allSettled(Object.entries(pathesContents).map(async ([path, content]) => renameList.push(await File.replace(path, content, this.totalItems.get(`${tableName}-*`)))));
|
|
1261
|
+
await Promise.allSettled(renameList
|
|
1262
|
+
.filter(([_, filePath]) => filePath)
|
|
1263
|
+
.map(async ([tempPath, filePath]) => rename(tempPath, filePath)));
|
|
1264
|
+
if (this.tablesMap.get(tableName).config.cache)
|
|
1265
|
+
await this.clearCache(join(tablePath, ".cache"));
|
|
1266
|
+
if (returnUpdatedData)
|
|
1267
|
+
return await this.get(tableName, undefined, options);
|
|
1268
|
+
}
|
|
1269
|
+
finally {
|
|
1270
|
+
if (renameList.length)
|
|
1271
|
+
await Promise.allSettled(renameList.map(async ([tempPath, _]) => unlink(tempPath)));
|
|
1272
|
+
await File.unlock(join(tablePath, ".tmp"));
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
else if ((Array.isArray(where) && where.every(Utils.isValidID)) ||
|
|
1276
|
+
Utils.isValidID(where)) {
|
|
1277
|
+
const lineNumbers = await this.get(tableName, where, undefined, undefined, true);
|
|
1278
|
+
return this.put(tableName, data, lineNumbers, options, returnUpdatedData || undefined);
|
|
1279
|
+
}
|
|
1280
|
+
else if ((Array.isArray(where) && where.every(Utils.isNumber)) ||
|
|
1281
|
+
Utils.isNumber(where)) {
|
|
1282
|
+
// "where" in this case, is the line(s) number(s) and not id(s)
|
|
1283
|
+
await this.validateData(tableName, data, true);
|
|
1284
|
+
this.formatData(tableName, data, true);
|
|
1285
|
+
const pathesContents = Object.fromEntries(Object.entries(this.joinPathesContents(tableName, Utils.isArrayOfObjects(data)
|
|
1286
|
+
? data.map((item) => ({
|
|
1287
|
+
...item,
|
|
1288
|
+
updatedAt: Date.now(),
|
|
1289
|
+
}))
|
|
1290
|
+
: { ...data, updatedAt: Date.now() })).map(([path, content]) => [
|
|
1291
|
+
path,
|
|
1292
|
+
[...(Array.isArray(where) ? where : [where])].reduce((obj, lineNum, index) => Object.assign(obj, {
|
|
1293
|
+
[lineNum]: Array.isArray(content) ? content[index] : content,
|
|
1294
|
+
}), {}),
|
|
1295
|
+
]));
|
|
1296
|
+
const keys = UtilsServer.hashString(Object.keys(pathesContents)
|
|
1297
|
+
.map((path) => path.replaceAll(this.getFileExtension(tableName), ""))
|
|
1298
|
+
.join("."));
|
|
1299
|
+
try {
|
|
1300
|
+
await File.lock(join(tablePath, ".tmp"), keys);
|
|
1301
|
+
await Promise.allSettled(Object.entries(pathesContents).map(async ([path, content]) => renameList.push(await File.replace(path, content))));
|
|
1302
|
+
await Promise.allSettled(renameList
|
|
1303
|
+
.filter(([_, filePath]) => filePath)
|
|
1304
|
+
.map(async ([tempPath, filePath]) => rename(tempPath, filePath)));
|
|
1305
|
+
if (this.tablesMap.get(tableName).config.cache)
|
|
1306
|
+
await this.clearCache(tableName);
|
|
1307
|
+
if (returnUpdatedData)
|
|
1308
|
+
return this.get(tableName, where, options, !Array.isArray(where));
|
|
1309
|
+
}
|
|
1310
|
+
finally {
|
|
1311
|
+
if (renameList.length)
|
|
1312
|
+
await Promise.allSettled(renameList.map(async ([tempPath, _]) => unlink(tempPath)));
|
|
1313
|
+
await File.unlock(join(tablePath, ".tmp"), keys);
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
else if (Utils.isObject(where)) {
|
|
1317
|
+
const lineNumbers = await this.get(tableName, where, undefined, undefined, true);
|
|
1318
|
+
if (lineNumbers)
|
|
1319
|
+
return this.put(tableName, data, lineNumbers, options, returnUpdatedData || undefined);
|
|
1320
|
+
}
|
|
1321
|
+
else
|
|
1322
|
+
throw this.createError("INVALID_PARAMETERS");
|
|
1323
|
+
}
|
|
1324
|
+
/**
|
|
1325
|
+
* Delete item(s) in a table
|
|
1326
|
+
*
|
|
1327
|
+
* @param {string} tableName
|
|
1328
|
+
* @param {(number | string | (number | string)[] | Criteria)} [where]
|
|
1329
|
+
* @return {boolean | null} {(Promise<boolean | null>)}
|
|
1330
|
+
*/
|
|
1331
|
+
async delete(tableName, where, _id) {
|
|
1332
|
+
const tablePath = join(this.databasePath, tableName);
|
|
1333
|
+
await this.throwErrorIfTableEmpty(tableName);
|
|
1334
|
+
if (!where) {
|
|
1335
|
+
try {
|
|
1336
|
+
await File.lock(join(tablePath, ".tmp"));
|
|
1337
|
+
let paginationFilePath;
|
|
1338
|
+
let pagination;
|
|
1339
|
+
for await (const paginationFileName of glob("*.pagination", {
|
|
1340
|
+
cwd: tablePath,
|
|
1341
|
+
})) {
|
|
1342
|
+
paginationFilePath = join(tablePath, paginationFileName);
|
|
1343
|
+
pagination = parse(paginationFileName)
|
|
1344
|
+
.name.split("-")
|
|
1345
|
+
.map(Number);
|
|
1346
|
+
}
|
|
1347
|
+
await Promise.all((await readdir(tablePath))
|
|
1348
|
+
?.filter((fileName) => fileName.endsWith(this.getFileExtension(tableName)))
|
|
1349
|
+
.map(async (file) => unlink(join(tablePath, file))));
|
|
1350
|
+
if (this.tablesMap.get(tableName).config.cache)
|
|
1351
|
+
await this.clearCache(tableName);
|
|
1352
|
+
await rename(paginationFilePath, join(tablePath, `${pagination[0]}-0.pagination`));
|
|
1353
|
+
return true;
|
|
1354
|
+
}
|
|
1355
|
+
finally {
|
|
1356
|
+
await File.unlock(join(tablePath, ".tmp"));
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
if ((Array.isArray(where) && where.every(Utils.isValidID)) ||
|
|
1360
|
+
Utils.isValidID(where)) {
|
|
1361
|
+
const lineNumbers = await this.get(tableName, where, undefined, undefined, true);
|
|
1362
|
+
return this.delete(tableName, lineNumbers, where);
|
|
1363
|
+
}
|
|
1364
|
+
if ((Array.isArray(where) && where.every(Utils.isNumber)) ||
|
|
1365
|
+
Utils.isNumber(where)) {
|
|
1366
|
+
// "where" in this case, is the line(s) number(s) and not id(s)
|
|
1367
|
+
const files = (await readdir(tablePath))?.filter((fileName) => fileName.endsWith(this.getFileExtension(tableName)));
|
|
1368
|
+
if (files.length) {
|
|
1369
|
+
const renameList = [];
|
|
1370
|
+
try {
|
|
1371
|
+
await File.lock(join(tablePath, ".tmp"));
|
|
1372
|
+
let paginationFilePath;
|
|
1373
|
+
let pagination;
|
|
1374
|
+
for await (const paginationFileName of glob("*.pagination", {
|
|
1375
|
+
cwd: tablePath,
|
|
1376
|
+
})) {
|
|
1377
|
+
paginationFilePath = join(tablePath, paginationFileName);
|
|
1378
|
+
pagination = parse(paginationFileName)
|
|
1379
|
+
.name.split("-")
|
|
1380
|
+
.map(Number);
|
|
1381
|
+
}
|
|
1382
|
+
if (pagination[1] &&
|
|
1383
|
+
pagination[1] - (Array.isArray(where) ? where.length : 1) > 0) {
|
|
1384
|
+
await Promise.all(files.map(async (file) => renameList.push(await File.remove(join(tablePath, file), where))));
|
|
1385
|
+
await Promise.all(renameList
|
|
1386
|
+
.filter(([_, filePath]) => filePath)
|
|
1387
|
+
.map(async ([tempPath, filePath]) => rename(tempPath, filePath)));
|
|
1388
|
+
}
|
|
1389
|
+
else
|
|
1390
|
+
await Promise.all((await readdir(tablePath))
|
|
1391
|
+
?.filter((fileName) => fileName.endsWith(this.getFileExtension(tableName)))
|
|
1392
|
+
.map(async (file) => unlink(join(tablePath, file))));
|
|
1393
|
+
if (this.tablesMap.get(tableName).config.cache)
|
|
1394
|
+
await this.clearCache(tableName);
|
|
1395
|
+
await rename(paginationFilePath, join(tablePath, `${pagination[0]}-${pagination[1] - (Array.isArray(where) ? where.length : 1)}.pagination`));
|
|
1396
|
+
return true;
|
|
1397
|
+
}
|
|
1398
|
+
finally {
|
|
1399
|
+
if (renameList.length)
|
|
1400
|
+
await Promise.allSettled(renameList.map(async ([tempPath, _]) => unlink(tempPath)));
|
|
1401
|
+
await File.unlock(join(tablePath, ".tmp"));
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
else if (Utils.isObject(where)) {
|
|
1406
|
+
const lineNumbers = await this.get(tableName, where, undefined, undefined, true);
|
|
1407
|
+
if (lineNumbers)
|
|
1408
|
+
return this.delete(tableName, lineNumbers);
|
|
1409
|
+
}
|
|
1410
|
+
else
|
|
1411
|
+
throw this.createError("INVALID_PARAMETERS");
|
|
1412
|
+
return false;
|
|
1413
|
+
}
|
|
1414
|
+
async sum(tableName, columns, where) {
|
|
1415
|
+
const RETURN = {};
|
|
1416
|
+
const tablePath = join(this.databasePath, tableName);
|
|
1417
|
+
await this.throwErrorIfTableEmpty(tableName);
|
|
1418
|
+
if (!Array.isArray(columns))
|
|
1419
|
+
columns = [columns];
|
|
1420
|
+
for await (const column of columns) {
|
|
1421
|
+
const columnPath = join(tablePath, `${column}${this.getFileExtension(tableName)}`);
|
|
1422
|
+
if (await File.isExists(columnPath)) {
|
|
1423
|
+
if (where) {
|
|
1424
|
+
const lineNumbers = await this.get(tableName, where, undefined, undefined, true);
|
|
1425
|
+
RETURN[column] = lineNumbers
|
|
1426
|
+
? await File.sum(columnPath, lineNumbers)
|
|
1427
|
+
: 0;
|
|
1428
|
+
}
|
|
1429
|
+
else
|
|
1430
|
+
RETURN[column] = await File.sum(columnPath);
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
return Array.isArray(columns) ? RETURN : Object.values(RETURN)[0];
|
|
1434
|
+
}
|
|
1435
|
+
async max(tableName, columns, where) {
|
|
1436
|
+
const RETURN = {};
|
|
1437
|
+
const tablePath = join(this.databasePath, tableName);
|
|
1438
|
+
await this.throwErrorIfTableEmpty(tableName);
|
|
1439
|
+
if (!Array.isArray(columns))
|
|
1440
|
+
columns = [columns];
|
|
1441
|
+
for await (const column of columns) {
|
|
1442
|
+
const columnPath = join(tablePath, `${column}${this.getFileExtension(tableName)}`);
|
|
1443
|
+
if (await File.isExists(columnPath)) {
|
|
1444
|
+
if (where) {
|
|
1445
|
+
const lineNumbers = await this.get(tableName, where, undefined, undefined, true);
|
|
1446
|
+
RETURN[column] = lineNumbers
|
|
1447
|
+
? await File.max(columnPath, lineNumbers)
|
|
1448
|
+
: 0;
|
|
1449
|
+
}
|
|
1450
|
+
else
|
|
1451
|
+
RETURN[column] = await File.max(columnPath);
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
return RETURN;
|
|
1455
|
+
}
|
|
1456
|
+
async min(tableName, columns, where) {
|
|
1457
|
+
const RETURN = {};
|
|
1458
|
+
const tablePath = join(this.databasePath, tableName);
|
|
1459
|
+
await this.throwErrorIfTableEmpty(tableName);
|
|
1460
|
+
if (!Array.isArray(columns))
|
|
1461
|
+
columns = [columns];
|
|
1462
|
+
for await (const column of columns) {
|
|
1463
|
+
const columnPath = join(tablePath, `${column}${this.getFileExtension(tableName)}`);
|
|
1464
|
+
if (await File.isExists(columnPath)) {
|
|
1465
|
+
if (where) {
|
|
1466
|
+
const lineNumbers = await this.get(tableName, where, undefined, undefined, true);
|
|
1467
|
+
RETURN[column] = lineNumbers
|
|
1468
|
+
? await File.min(columnPath, lineNumbers)
|
|
1469
|
+
: 0;
|
|
1470
|
+
}
|
|
1471
|
+
else
|
|
1472
|
+
RETURN[column] = await File.min(columnPath);
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
return RETURN;
|
|
1476
|
+
}
|
|
1477
|
+
}
|