tsondb 0.21.11 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/node/config.d.ts +3 -6
- package/dist/src/node/index.d.ts +12 -132
- package/dist/src/node/index.js +66 -521
- package/dist/src/node/readonly.d.ts +158 -0
- package/dist/src/node/readonly.js +321 -0
- package/dist/src/node/server/index.d.ts +6 -5
- package/dist/src/node/server/index.js +7 -3
- package/dist/src/node/shared.d.ts +46 -0
- package/dist/src/node/shared.js +267 -0
- package/dist/src/node/utils/databaseInMemory.d.ts +2 -0
- package/dist/src/node/utils/databaseInMemory.js +10 -0
- package/package.json +1 -1
package/dist/src/node/index.js
CHANGED
|
@@ -1,61 +1,21 @@
|
|
|
1
|
-
import { exists } from "@elyukai/utils/async/node";
|
|
2
|
-
import { on } from "@elyukai/utils/function";
|
|
3
1
|
import { Lazy } from "@elyukai/utils/lazy";
|
|
4
|
-
import { nullableToArray } from "@elyukai/utils/nullable";
|
|
5
|
-
import { compareNumber, reduceCompare } from "@elyukai/utils/ordering";
|
|
6
2
|
import { isError } from "@elyukai/utils/result";
|
|
7
|
-
import { assertExhaustive } from "@elyukai/utils/typeSafety";
|
|
8
3
|
import Debug from "debug";
|
|
9
|
-
import {
|
|
10
|
-
import { findPackageJSON } from "node:module";
|
|
11
|
-
import { join, sep } from "node:path";
|
|
12
|
-
import { stderr } from "node:process";
|
|
13
|
-
import { styleText } from "node:util";
|
|
4
|
+
import { writeFile } from "node:fs/promises";
|
|
14
5
|
import semver from "semver";
|
|
15
6
|
import { simpleGit } from "simple-git";
|
|
16
|
-
import {
|
|
17
|
-
import { countError, countErrors, getErrorMessageForDisplay, HTTPError, wrapErrorsIfAny, } from "../shared/utils/error.js";
|
|
18
|
-
import { parallelizeErrors } from "../shared/utils/validation.js";
|
|
7
|
+
import { HTTPError } from "../shared/utils/error.js";
|
|
19
8
|
import { Git } from "./git.js";
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
9
|
+
import { ReadonlyTSONDB } from "./readonly.js";
|
|
10
|
+
import { validateDeclStructuralIntegrity, } from "./schema/treeOperations/validation.js";
|
|
11
|
+
import { createServer } from "./server/index.js";
|
|
12
|
+
import { createCompressedDatabaseFile, getTSONDBVersion, initData, prepareFolders, validate, writeMetaFile, } from "./shared.js";
|
|
24
13
|
import { Transaction } from "./transaction.js";
|
|
25
|
-
import { checkCustomConstraintsForAllEntities } from "./utils/customConstraints.js";
|
|
26
14
|
import { DatabaseInMemory } from "./utils/databaseInMemory.js";
|
|
27
15
|
import { applyStepsToDisk } from "./utils/databaseOnDisk.js";
|
|
28
|
-
import {
|
|
29
|
-
import { formatInstance, getFileNameForId, getSingletonFileName, readInstance, readSingletonInstance, writeInstance, writeSingletonInstance, } from "./utils/files.js";
|
|
16
|
+
import { writeInstance, writeSingletonInstance } from "./utils/files.js";
|
|
30
17
|
import { attachGitStatusToDatabaseInMemory } from "./utils/git.js";
|
|
31
|
-
|
|
32
|
-
import { checkUniqueConstraintsForAllEntities } from "./utils/unique.js";
|
|
33
|
-
const debug = Debug("tsondb:class");
|
|
34
|
-
const getMetaFilePath = (dataRootPath) => join(dataRootPath, ".meta.json");
|
|
35
|
-
const getTSONDBVersion = async () => {
|
|
36
|
-
const packageJsonPath = findPackageJSON(".", import.meta.url);
|
|
37
|
-
if (!packageJsonPath) {
|
|
38
|
-
throw new Error("Could not find package.json for TSONDB");
|
|
39
|
-
}
|
|
40
|
-
const packageJson = JSON.parse(await readFile(packageJsonPath, "utf-8"));
|
|
41
|
-
return packageJson.version;
|
|
42
|
-
};
|
|
43
|
-
const readMetaFile = async (dataRootPath) => JSON.parse(await readFile(getMetaFilePath(dataRootPath), "utf-8"));
|
|
44
|
-
const writeMetaFile = async (dataRootPath, meta) => writeFile(getMetaFilePath(dataRootPath), JSON.stringify(meta, null, 2), "utf-8");
|
|
45
|
-
const prepareFolders = async (schemaVersion, dataRootPath, entities) => {
|
|
46
|
-
await mkdir(dataRootPath, { recursive: true });
|
|
47
|
-
await Promise.all(entities.map(entity => mkdir(join(dataRootPath, entity.name), { recursive: true })));
|
|
48
|
-
if (!(await exists(getMetaFilePath(dataRootPath)))) {
|
|
49
|
-
const newMetaFile = {
|
|
50
|
-
version: await getTSONDBVersion(),
|
|
51
|
-
schemaVersion,
|
|
52
|
-
};
|
|
53
|
-
await writeMetaFile(dataRootPath, newMetaFile);
|
|
54
|
-
}
|
|
55
|
-
};
|
|
56
|
-
const generateOutputs = async (schema, outputs) => {
|
|
57
|
-
await Promise.all(outputs.map(output => output.run(schema)));
|
|
58
|
-
};
|
|
18
|
+
const debug = Debug("tsondb:main");
|
|
59
19
|
const getGit = async (dataRootPath) => {
|
|
60
20
|
const git = simpleGit({ baseDir: dataRootPath });
|
|
61
21
|
if (await git.checkIsRepo()) {
|
|
@@ -72,62 +32,20 @@ const getGit = async (dataRootPath) => {
|
|
|
72
32
|
return { git };
|
|
73
33
|
}
|
|
74
34
|
};
|
|
75
|
-
const checkLocales = (schema, data, locales) => {
|
|
76
|
-
const { localeEntity } = schema;
|
|
77
|
-
if (localeEntity &&
|
|
78
|
-
!locales.every(locale => data.hasInstanceOfEntityById(localeEntity.name, locale))) {
|
|
79
|
-
throw new Error("All provided locales must exist in the database.");
|
|
80
|
-
}
|
|
81
|
-
};
|
|
82
|
-
const initData = async (dataRootPath, schema, locales, git, gitStatus, skipReferenceCache) => {
|
|
83
|
-
debug("loading database into memory ...");
|
|
84
|
-
let data = await DatabaseInMemory.load(dataRootPath, schema.entities, schema.singletonEntities);
|
|
85
|
-
debug("done");
|
|
86
|
-
checkLocales(schema, data, locales);
|
|
87
|
-
const serializedDeclarationsByName = Object.fromEntries(schema.resolvedDeclarations.map(decl => [decl.name, serializeNode(decl)]));
|
|
88
|
-
let referencesToInstances = {};
|
|
89
|
-
if (!skipReferenceCache) {
|
|
90
|
-
debug("creating references cache ...");
|
|
91
|
-
referencesToInstances = await getReferencesToInstances(data, serializedDeclarationsByName);
|
|
92
|
-
debug("done");
|
|
93
|
-
}
|
|
94
|
-
else {
|
|
95
|
-
debug("skipping references cache creation");
|
|
96
|
-
}
|
|
97
|
-
if (git) {
|
|
98
|
-
const status = gitStatus ?? (await git.client.status());
|
|
99
|
-
data = attachGitStatusToDatabaseInMemory(data, dataRootPath, git.root, status);
|
|
100
|
-
debug("attached git status to instances");
|
|
101
|
-
}
|
|
102
|
-
return { data, referencesToInstances };
|
|
103
|
-
};
|
|
104
35
|
/**
|
|
105
36
|
* The main class for managing a typed JSON database.
|
|
106
37
|
*/
|
|
107
|
-
export class TSONDB {
|
|
108
|
-
#data;
|
|
109
|
-
#dataRootPath;
|
|
110
|
-
#schema;
|
|
111
|
-
#locales;
|
|
38
|
+
export class TSONDB extends ReadonlyTSONDB {
|
|
112
39
|
#git;
|
|
113
|
-
#referencesToInstances;
|
|
114
|
-
#validationOptions;
|
|
115
|
-
#formatterOptions;
|
|
116
40
|
#gitWrapper;
|
|
117
41
|
#locked = false;
|
|
118
42
|
#hooks = {};
|
|
119
43
|
constructor(options) {
|
|
120
|
-
|
|
121
|
-
this.#data = options.data;
|
|
122
|
-
this.#schema = options.schema;
|
|
123
|
-
this.#locales = options.locales ?? [];
|
|
44
|
+
super(options);
|
|
124
45
|
this.#git = options.git;
|
|
125
|
-
this.#referencesToInstances = options.referencesToInstances;
|
|
126
|
-
this.#validationOptions = options.validationOptions;
|
|
127
|
-
this.#formatterOptions = options.formatterOptions;
|
|
128
46
|
this.#gitWrapper = Lazy.of(() => this.#git
|
|
129
|
-
? new Git(this, this.#git.client, this.#git.root, this
|
|
130
|
-
this
|
|
47
|
+
? new Git(this, this.#git.client, this.#git.root, this.dataRootPath, () => this._data, (data) => {
|
|
48
|
+
this._data = data;
|
|
131
49
|
})
|
|
132
50
|
: undefined);
|
|
133
51
|
}
|
|
@@ -154,7 +72,7 @@ export class TSONDB {
|
|
|
154
72
|
throw new Error("Schema does not contain a locale entity, locales cannot be provided.");
|
|
155
73
|
}
|
|
156
74
|
const git = gitRoot ? { client: gitClient, root: gitRoot } : undefined;
|
|
157
|
-
const { data, referencesToInstances } = await initData(options.dataRootPath, options.schema, options.locales ?? [], git, gitStatus, skipReferenceCache);
|
|
75
|
+
const { data, referencesToInstances, meta } = await initData(options.dataRootPath, options.schema, options.locales ?? [], git, gitStatus, skipReferenceCache, false);
|
|
158
76
|
const db = new TSONDB({
|
|
159
77
|
...options,
|
|
160
78
|
data,
|
|
@@ -166,9 +84,9 @@ export class TSONDB {
|
|
|
166
84
|
...options.validationOptions,
|
|
167
85
|
},
|
|
168
86
|
});
|
|
169
|
-
|
|
170
|
-
if (
|
|
171
|
-
debug("migrating database from schema version %s to %s",
|
|
87
|
+
meta.version = await getTSONDBVersion();
|
|
88
|
+
if (meta.schemaVersion !== options.schema.version && options.migrationStages) {
|
|
89
|
+
debug("migrating database from schema version %s to %s", meta.schemaVersion, options.schema.version);
|
|
172
90
|
for (const stage of options.migrationStages) {
|
|
173
91
|
if (semver.valid(stage.toVersion) === null ||
|
|
174
92
|
semver.validRange(stage.fromVersion) === null) {
|
|
@@ -176,11 +94,13 @@ export class TSONDB {
|
|
|
176
94
|
}
|
|
177
95
|
}
|
|
178
96
|
for (const stage of options.migrationStages) {
|
|
179
|
-
if (semver.satisfies(
|
|
97
|
+
if (semver.satisfies(meta.schemaVersion, stage.fromVersion)) {
|
|
180
98
|
debug("applying migration stage from %s to %s", stage.fromVersion, stage.toVersion);
|
|
181
99
|
// oxlint-disable-next-line no-await-in-loop
|
|
182
100
|
await stage.willMigrate?.(db);
|
|
183
|
-
|
|
101
|
+
meta.schemaVersion = stage.toVersion;
|
|
102
|
+
// oxlint-disable-next-line no-await-in-loop
|
|
103
|
+
await writeMetaFile(options.dataRootPath, meta);
|
|
184
104
|
}
|
|
185
105
|
else {
|
|
186
106
|
debug("skipping migration stage from %s to %s", stage.fromVersion, stage.toVersion);
|
|
@@ -190,244 +110,41 @@ export class TSONDB {
|
|
|
190
110
|
else {
|
|
191
111
|
debug("no migration needed, schema version is up to date");
|
|
192
112
|
}
|
|
193
|
-
metaFile.version = await getTSONDBVersion();
|
|
194
|
-
metaFile.schemaVersion = options.schema.version;
|
|
195
|
-
await writeMetaFile(options.dataRootPath, metaFile);
|
|
196
113
|
return db;
|
|
197
114
|
}
|
|
198
|
-
/**
|
|
199
|
-
* Generates outputs based on the provided schema and output configurations.
|
|
200
|
-
*/
|
|
201
|
-
static async generateOutputs(options) {
|
|
202
|
-
await generateOutputs(options.schema, options.outputs);
|
|
203
|
-
}
|
|
204
|
-
/**
|
|
205
|
-
* Generates outputs based on the current schema and the provided output configurations.
|
|
206
|
-
*/
|
|
207
|
-
async generateOutputs(outputs) {
|
|
208
|
-
await generateOutputs(this.#schema, outputs);
|
|
209
|
-
}
|
|
210
115
|
#strucurallyValidateInstance(entity, instanceContent) {
|
|
211
116
|
const validationContext = {
|
|
212
|
-
validationOptions: this
|
|
117
|
+
validationOptions: this._validationOptions,
|
|
213
118
|
useStyling: true,
|
|
214
119
|
};
|
|
215
120
|
return validateDeclStructuralIntegrity(validationContext, [], entity, [], instanceContent);
|
|
216
121
|
}
|
|
217
|
-
#validate(data, options
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
const onlyEntities = checkOnlyEntities.length > 0
|
|
228
|
-
? entities.filter(entity => checkOnlyEntities.includes(entity.name))
|
|
229
|
-
: entities;
|
|
230
|
-
const changedEntities = changedEntityNames
|
|
231
|
-
? entities.filter(entity => changedEntityNames.includes(entity.name))
|
|
232
|
-
: undefined;
|
|
233
|
-
const validationContext = {
|
|
234
|
-
validationOptions: this.#validationOptions,
|
|
235
|
-
useStyling: true,
|
|
236
|
-
};
|
|
237
|
-
const errors = [];
|
|
238
|
-
const summary = [];
|
|
239
|
-
const debugAndAddToSummary = (message) => {
|
|
240
|
-
debug(message);
|
|
241
|
-
summary.push(message);
|
|
242
|
-
};
|
|
243
|
-
if (!skipStructuralValidation) {
|
|
244
|
-
debug("Checking structural integrity ...");
|
|
245
|
-
const structureErrors = onlyEntities
|
|
246
|
-
.flatMap(entity => {
|
|
247
|
-
switch (entity.kind) {
|
|
248
|
-
case NodeKind.EntityDecl:
|
|
249
|
-
return parallelizeErrors(data
|
|
250
|
-
.getAllInstanceContainersOfEntity(entity.name)
|
|
251
|
-
.map(instance => wrapErrorsIfAny(`in file ${styleText("white", `"${this.#dataRootPath}${sep}${styleText("bold", join(entity.name, getFileNameForId(instance.id)))}"`)}`, validateDeclStructuralIntegrity(validationContext, [], entity, [], instance.content))));
|
|
252
|
-
case NodeKind.SingletonEntityDecl: {
|
|
253
|
-
const instance = data.getSingletonInstanceContainerOfEntity(entity.name);
|
|
254
|
-
if (instance) {
|
|
255
|
-
return nullableToArray(wrapErrorsIfAny(`in file ${styleText("white", `"${this.#dataRootPath}${sep}${styleText("bold", getSingletonFileName(entity.name))}"`)}`, validateDeclStructuralIntegrity(validationContext, [], entity, [], instance.content)));
|
|
256
|
-
}
|
|
257
|
-
return [];
|
|
258
|
-
}
|
|
259
|
-
default:
|
|
260
|
-
return assertExhaustive(entity);
|
|
261
|
-
}
|
|
262
|
-
})
|
|
263
|
-
.toSorted((a, b) => a.message.localeCompare(b.message));
|
|
264
|
-
if (structureErrors.length > 0) {
|
|
265
|
-
const errorCount = countErrors(structureErrors);
|
|
266
|
-
debugAndAddToSummary(`${errorCount.toString()} structural integrity violation${errorCount === 1 ? "" : "s"} found`);
|
|
267
|
-
errors.push(...structureErrors);
|
|
268
|
-
}
|
|
269
|
-
else {
|
|
270
|
-
debug("No structural integrity violations found");
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
if (errors.length === 0) {
|
|
274
|
-
if (!skipStructuralValidation) {
|
|
275
|
-
if (checkReferentialIntegrity) {
|
|
276
|
-
debug("Checking referential integrity ...");
|
|
277
|
-
const entityNameValidator = createEntityNameValidator(this.#schema.isEntityName.bind(this.#schema), true);
|
|
278
|
-
const referenceValidator = createReferenceValidator(this.#schema.isEntityName.bind(this.#schema), data, true);
|
|
279
|
-
const referenceErrors = onlyEntities
|
|
280
|
-
.flatMap(entity => {
|
|
281
|
-
switch (entity.kind) {
|
|
282
|
-
case NodeKind.EntityDecl:
|
|
283
|
-
return parallelizeErrors(data
|
|
284
|
-
.getAllInstanceContainersOfEntity(entity.name)
|
|
285
|
-
.map(instance => wrapErrorsIfAny(`in file ${styleText("white", `"${this.#dataRootPath}${sep}${styleText("bold", join(entity.name, getFileNameForId(instance.id)))}"`)}`, validateDeclReferentialIntegrity(validationContext, entityNameValidator, referenceValidator, [], entity, [], instance.content))));
|
|
286
|
-
case NodeKind.SingletonEntityDecl: {
|
|
287
|
-
const instance = data.getSingletonInstanceContainerOfEntity(entity.name);
|
|
288
|
-
if (instance) {
|
|
289
|
-
return nullableToArray(wrapErrorsIfAny(`in file ${styleText("white", `"${this.#dataRootPath}${sep}${styleText("bold", getSingletonFileName(entity.name))}"`)}`, validateDeclReferentialIntegrity(validationContext, entityNameValidator, referenceValidator, [], entity, [], instance.content)));
|
|
290
|
-
}
|
|
291
|
-
return [];
|
|
292
|
-
}
|
|
293
|
-
default:
|
|
294
|
-
return assertExhaustive(entity);
|
|
295
|
-
}
|
|
296
|
-
})
|
|
297
|
-
.toSorted((a, b) => a.message.localeCompare(b.message));
|
|
298
|
-
if (referenceErrors.length > 0) {
|
|
299
|
-
const errorCount = countErrors(referenceErrors);
|
|
300
|
-
debugAndAddToSummary(`${errorCount.toString()} referential integrity violation${errorCount === 1 ? "" : "s"} found`);
|
|
301
|
-
errors.push(...referenceErrors);
|
|
302
|
-
}
|
|
303
|
-
else {
|
|
304
|
-
debug("No referential integrity violations found");
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
else {
|
|
308
|
-
debug("Disabled referential integrity checks, skipping them");
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
debug("Checking unique constraints ...");
|
|
312
|
-
const instanceOverviewsByEntityName = getAllInstanceOverviewsByEntityName(getEntity, data, this.#locales);
|
|
313
|
-
const uniqueConstraintResult = checkUniqueConstraintsForAllEntities(data, (changedEntities ?? onlyEntities).filter(e => isEntityDecl(e)), instanceOverviewsByEntityName);
|
|
314
|
-
if (isError(uniqueConstraintResult)) {
|
|
315
|
-
const errorCount = countError(uniqueConstraintResult.error);
|
|
316
|
-
debugAndAddToSummary(`${errorCount.toString()} unique constraint violation${errorCount === 1 ? "" : "s"} found`);
|
|
317
|
-
errors.push(uniqueConstraintResult.error);
|
|
318
|
-
}
|
|
319
|
-
else {
|
|
320
|
-
debug("No unique constraint violations found");
|
|
321
|
-
}
|
|
322
|
-
debug("Checking custom constraints ...");
|
|
323
|
-
const customConstraintResult = checkCustomConstraintsForAllEntities((...args) => getDisplayName(getEntity, this.#locales, data, ...args), (...args) => getDisplayNameWithId(getEntity, this.#locales, data, ...args), (...args) => getInstanceOverview(data.getInstanceContainerOfEntityById.bind(data), data.getAllChildInstanceContainersForParent.bind(data, getEntity), getEntity, this.#locales, args), getEntity, data, entities);
|
|
324
|
-
if (isError(customConstraintResult)) {
|
|
325
|
-
const errorCount = countError(customConstraintResult.error);
|
|
326
|
-
debugAndAddToSummary(`${errorCount.toString()} custom constraint violation${errorCount === 1 ? "" : "s"} found`);
|
|
327
|
-
errors.push(customConstraintResult.error);
|
|
328
|
-
}
|
|
329
|
-
else {
|
|
330
|
-
debug("No custom constraint violations found");
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
else {
|
|
334
|
-
debugAndAddToSummary("Skipping further checks due to previous structural integrity errors");
|
|
335
|
-
}
|
|
336
|
-
if (logToConsole) {
|
|
337
|
-
console.log(`${data.totalSize.toString()} instance${data.totalSize === 1 ? "" : "s"} checked`);
|
|
338
|
-
if (errors.length === 0) {
|
|
339
|
-
console.log(styleText("green", "All instances are valid"));
|
|
340
|
-
}
|
|
341
|
-
else {
|
|
342
|
-
const errorCount = countErrors(errors);
|
|
343
|
-
console.error(styleText("red", `${errors.map(err => getErrorMessageForDisplay(err)).join("\n\n")}\n`, {
|
|
344
|
-
stream: stderr,
|
|
345
|
-
}));
|
|
346
|
-
console.error(styleText(["red", "bold"], "Summary", { stream: stderr }));
|
|
347
|
-
console.error(styleText("red", [
|
|
348
|
-
`${errorCount.toString()} validation error${errorCount === 1 ? "" : "s"} found`,
|
|
349
|
-
...summary,
|
|
350
|
-
].join("\n"), { stream: stderr }));
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
return errors;
|
|
354
|
-
}
|
|
355
|
-
/**
|
|
356
|
-
* Validates the data in the database.
|
|
357
|
-
*
|
|
358
|
-
* Returns `true` if the data is valid, `false` otherwise.
|
|
359
|
-
*/
|
|
360
|
-
validate() {
|
|
361
|
-
debug("Validating database ...");
|
|
362
|
-
return this.#validate(this.#data).length === 0;
|
|
122
|
+
#validate(data, options) {
|
|
123
|
+
return validate({
|
|
124
|
+
data,
|
|
125
|
+
dataRootPath: this.dataRootPath,
|
|
126
|
+
locales: this.locales,
|
|
127
|
+
schema: this.schema,
|
|
128
|
+
validationOptions: this._validationOptions,
|
|
129
|
+
}, options);
|
|
363
130
|
}
|
|
364
131
|
/**
|
|
365
132
|
* Formats the data on disk according to the current in-memory representation.
|
|
366
133
|
*/
|
|
367
134
|
async format() {
|
|
368
135
|
debug("Formatting database ...");
|
|
369
|
-
await this
|
|
136
|
+
await this._data.forEachInstance(async (entityName, instance) => {
|
|
370
137
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
371
|
-
const entity = this
|
|
372
|
-
await writeInstance(this
|
|
138
|
+
const entity = this.schema.getEntity(entityName);
|
|
139
|
+
await writeInstance(this.dataRootPath, entity, instance.id, instance.content, this._formatterOptions);
|
|
373
140
|
}, true);
|
|
374
|
-
await this
|
|
141
|
+
await this._data.forEachSingletonInstance(async (entityName, instance) => {
|
|
375
142
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
376
|
-
const entity = this
|
|
377
|
-
await writeSingletonInstance(this
|
|
143
|
+
const entity = this.schema.getSingletonEntity(entityName);
|
|
144
|
+
await writeSingletonInstance(this.dataRootPath, entity, instance.content, this._formatterOptions);
|
|
378
145
|
}, true);
|
|
379
146
|
debug("All data is formatted");
|
|
380
147
|
}
|
|
381
|
-
/**
|
|
382
|
-
* Checks if the data on disk is formatted according to the current in-memory representation.
|
|
383
|
-
*/
|
|
384
|
-
async checkFormat() {
|
|
385
|
-
debug("Check database formatting ...");
|
|
386
|
-
let counter = 0;
|
|
387
|
-
let total = 0;
|
|
388
|
-
await this.#data.forEachInstance(async (entityName, instance) => {
|
|
389
|
-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
390
|
-
const entity = this.#schema.getEntity(entityName);
|
|
391
|
-
const instanceOnDisk = await readInstance(this.#dataRootPath, entity, instance.id);
|
|
392
|
-
const currentFormatted = formatInstance(entity, instance.content, this.#formatterOptions);
|
|
393
|
-
if (instanceOnDisk !== currentFormatted) {
|
|
394
|
-
console.log(`Instance ${styleText("yellow", instance.id)} of entity ${styleText("cyan", entityName)} is not formatted correctly.`);
|
|
395
|
-
counter++;
|
|
396
|
-
}
|
|
397
|
-
total++;
|
|
398
|
-
}, true);
|
|
399
|
-
await this.#data.forEachSingletonInstance(async (entityName, instance) => {
|
|
400
|
-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
401
|
-
const entity = this.#schema.getSingletonEntity(entityName);
|
|
402
|
-
const instanceOnDisk = await readSingletonInstance(this.#dataRootPath, entity);
|
|
403
|
-
const currentFormatted = formatInstance(entity, instance.content, this.#formatterOptions);
|
|
404
|
-
if (instanceOnDisk !== currentFormatted) {
|
|
405
|
-
console.log(`Singleton instance of entity ${styleText("cyan", entityName)} is not formatted correctly.`);
|
|
406
|
-
counter++;
|
|
407
|
-
}
|
|
408
|
-
total++;
|
|
409
|
-
}, true);
|
|
410
|
-
if (counter === 0) {
|
|
411
|
-
console.log(styleText("green", `All ${total.toFixed()} instance${total === 1 ? "" : "s"} are formatted correctly`));
|
|
412
|
-
return true;
|
|
413
|
-
}
|
|
414
|
-
else {
|
|
415
|
-
console.error(styleText("red", `${counter.toFixed()} instance${counter === 1 ? "" : "s"} of ${total.toFixed()} instance${total === 1 ? "" : "s"} are not formatted correctly`, {
|
|
416
|
-
stream: stderr,
|
|
417
|
-
}));
|
|
418
|
-
return false;
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
/**
|
|
422
|
-
* Reloads the data from disk into memory.
|
|
423
|
-
*/
|
|
424
|
-
async sync() {
|
|
425
|
-
debug("Syncing with disk ...");
|
|
426
|
-
const { data, referencesToInstances } = await initData(this.#dataRootPath, this.#schema, this.#locales, this.#git, undefined, false);
|
|
427
|
-
this.#data = data;
|
|
428
|
-
this.#referencesToInstances = referencesToInstances;
|
|
429
|
-
debug("Synced with disk");
|
|
430
|
-
}
|
|
431
148
|
/**
|
|
432
149
|
* Runs a transaction on the database.
|
|
433
150
|
*
|
|
@@ -443,15 +160,15 @@ export class TSONDB {
|
|
|
443
160
|
this.#hooks["transaction:before"]?.forEach(hook => hook(this));
|
|
444
161
|
this.#locked = true;
|
|
445
162
|
try {
|
|
446
|
-
const getEntity = this
|
|
447
|
-
const getSingletonEntity = this
|
|
163
|
+
const getEntity = this.schema.getEntity.bind(this.schema);
|
|
164
|
+
const getSingletonEntity = this.schema.getSingletonEntity.bind(this.schema);
|
|
448
165
|
const [txn, res] = fn(new Transaction({
|
|
449
|
-
data: this
|
|
166
|
+
data: this._data,
|
|
450
167
|
getEntity,
|
|
451
168
|
getSingletonEntity,
|
|
452
|
-
referencesToInstances: this
|
|
169
|
+
referencesToInstances: this._referencesToInstances,
|
|
453
170
|
validate: this.#strucurallyValidateInstance.bind(this),
|
|
454
|
-
localeEntity: this
|
|
171
|
+
localeEntity: this.schema.localeEntity,
|
|
455
172
|
steps: [],
|
|
456
173
|
}));
|
|
457
174
|
const txtResult = txn.getResult();
|
|
@@ -467,7 +184,7 @@ export class TSONDB {
|
|
|
467
184
|
throw new AggregateError(errors, "Validation errors occurred");
|
|
468
185
|
}
|
|
469
186
|
debug("Applying changes to disk ...");
|
|
470
|
-
const diskResult = await applyStepsToDisk(this
|
|
187
|
+
const diskResult = await applyStepsToDisk(this.dataRootPath, steps, this._formatterOptions);
|
|
471
188
|
if (isError(diskResult)) {
|
|
472
189
|
debug("Error applying changes to disk: %s", diskResult.error.message);
|
|
473
190
|
throw diskResult.error;
|
|
@@ -478,15 +195,15 @@ export class TSONDB {
|
|
|
478
195
|
if (this.#git) {
|
|
479
196
|
debug("Updating git status ...");
|
|
480
197
|
const status = await this.#git.client.status();
|
|
481
|
-
const newDbWithUpdatedGit = attachGitStatusToDatabaseInMemory(newData, this
|
|
198
|
+
const newDbWithUpdatedGit = attachGitStatusToDatabaseInMemory(newData, this.dataRootPath, this.#git.root, status);
|
|
482
199
|
debug("Git status updated");
|
|
483
|
-
this
|
|
200
|
+
this._data = newDbWithUpdatedGit;
|
|
484
201
|
}
|
|
485
202
|
else {
|
|
486
203
|
debug("No git repository, skipping git status update");
|
|
487
|
-
this
|
|
204
|
+
this._data = newData;
|
|
488
205
|
}
|
|
489
|
-
this
|
|
206
|
+
this._referencesToInstances = newRefs;
|
|
490
207
|
this.#locked = false;
|
|
491
208
|
debug("Transaction successful, released lock");
|
|
492
209
|
this.#hooks["transaction:after"]?.forEach(hook => hook(this));
|
|
@@ -506,7 +223,7 @@ export class TSONDB {
|
|
|
506
223
|
async createInstance(entityName, content, id) {
|
|
507
224
|
const res = await this.runTransaction(txn =>
|
|
508
225
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
509
|
-
txn.createInstance(this
|
|
226
|
+
txn.createInstance(this.schema.getEntity(entityName), content, id));
|
|
510
227
|
return res;
|
|
511
228
|
}
|
|
512
229
|
/**
|
|
@@ -517,7 +234,7 @@ export class TSONDB {
|
|
|
517
234
|
async updateInstance(entityName, id, content) {
|
|
518
235
|
const res = await this.runTransaction(txn =>
|
|
519
236
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
520
|
-
txn.updateInstance(this
|
|
237
|
+
txn.updateInstance(this.schema.getEntity(entityName), id, content));
|
|
521
238
|
return res;
|
|
522
239
|
}
|
|
523
240
|
/**
|
|
@@ -528,7 +245,7 @@ export class TSONDB {
|
|
|
528
245
|
async deleteInstance(entityName, id) {
|
|
529
246
|
const res = await this.runTransaction(txn =>
|
|
530
247
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
531
|
-
txn.deleteInstance(this
|
|
248
|
+
txn.deleteInstance(this.schema.getEntity(entityName), id));
|
|
532
249
|
return res.content;
|
|
533
250
|
}
|
|
534
251
|
/**
|
|
@@ -540,7 +257,7 @@ export class TSONDB {
|
|
|
540
257
|
async createSingletonInstance(entityName, content) {
|
|
541
258
|
const res = await this.runTransaction(txn =>
|
|
542
259
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
543
|
-
txn.createSingletonInstance(this
|
|
260
|
+
txn.createSingletonInstance(this.schema.getSingletonEntity(entityName), content));
|
|
544
261
|
return res;
|
|
545
262
|
}
|
|
546
263
|
/**
|
|
@@ -551,7 +268,7 @@ export class TSONDB {
|
|
|
551
268
|
async updateSingletonInstance(entityName, content) {
|
|
552
269
|
const res = await this.runTransaction(txn =>
|
|
553
270
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
554
|
-
txn.updateSingletonInstance(this
|
|
271
|
+
txn.updateSingletonInstance(this.schema.getSingletonEntity(entityName), content));
|
|
555
272
|
return res;
|
|
556
273
|
}
|
|
557
274
|
/**
|
|
@@ -562,7 +279,7 @@ export class TSONDB {
|
|
|
562
279
|
async deleteSingletonInstance(entityName) {
|
|
563
280
|
const res = await this.runTransaction(txn =>
|
|
564
281
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
565
|
-
txn.deleteSingletonInstance(this
|
|
282
|
+
txn.deleteSingletonInstance(this.schema.getSingletonEntity(entityName)));
|
|
566
283
|
return res.content;
|
|
567
284
|
}
|
|
568
285
|
/**
|
|
@@ -576,7 +293,7 @@ export class TSONDB {
|
|
|
576
293
|
return this.runTransaction(txn => [
|
|
577
294
|
txn.reorderAllInstancesOfEntity(
|
|
578
295
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
579
|
-
typeof entityName === "string" ? this
|
|
296
|
+
typeof entityName === "string" ? this.schema.getEntity(entityName) : entityName, newOrder),
|
|
580
297
|
undefined,
|
|
581
298
|
]);
|
|
582
299
|
}
|
|
@@ -590,124 +307,7 @@ export class TSONDB {
|
|
|
590
307
|
async reorderInstanceOfEntity(entityName, instanceId, newIndex) {
|
|
591
308
|
return this.runTransaction(txn => txn.reorderInstanceOfEntity(
|
|
592
309
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
593
|
-
typeof entityName === "string" ? this
|
|
594
|
-
}
|
|
595
|
-
/**
|
|
596
|
-
* Retrieves an instance of the specified entity by its identifier.
|
|
597
|
-
*/
|
|
598
|
-
getInstanceOfEntityById(...args) {
|
|
599
|
-
return this.#data.getInstanceContainerOfEntityById(...args)?.content;
|
|
600
|
-
}
|
|
601
|
-
/**
|
|
602
|
-
* Retrieves the instance container of the specified entity by its identifier.
|
|
603
|
-
*/
|
|
604
|
-
getInstanceContainerOfEntityById(...args) {
|
|
605
|
-
return this.#data.getInstanceContainerOfEntityById(...args);
|
|
606
|
-
}
|
|
607
|
-
/**
|
|
608
|
-
* Retrieves the overview of an instance of the specified entity by its identifier.
|
|
609
|
-
*/
|
|
610
|
-
getInstanceOverviewOfEntityById(...args) {
|
|
611
|
-
return getInstanceOverview(this.getInstanceContainerOfEntityById.bind(this), this.getAllChildInstanceContainersForParent.bind(this), this.#schema.getEntity.bind(this.#schema), this.#locales, args);
|
|
612
|
-
}
|
|
613
|
-
/**
|
|
614
|
-
* Retrieves all instances of the specified entity.
|
|
615
|
-
*/
|
|
616
|
-
getAllInstancesOfEntity(entityName) {
|
|
617
|
-
return this.#data.getAllInstanceContainersOfEntity(entityName).map(ic => ic.content);
|
|
618
|
-
}
|
|
619
|
-
/**
|
|
620
|
-
* Retrieves all instance containers of the specified entity.
|
|
621
|
-
*/
|
|
622
|
-
getAllInstanceContainersOfEntity(entityName) {
|
|
623
|
-
return this.#data.getAllInstanceContainersOfEntity(entityName);
|
|
624
|
-
}
|
|
625
|
-
/**
|
|
626
|
-
* Retrieves all instance overviews of the specified entity.
|
|
627
|
-
*/
|
|
628
|
-
getAllInstanceOverviewsOfEntity(entity) {
|
|
629
|
-
const normEntity = typeof entity === "object" ? entity : this.#schema.getEntity(entity);
|
|
630
|
-
if (!normEntity) {
|
|
631
|
-
return [];
|
|
632
|
-
}
|
|
633
|
-
return getInstanceOverviewsByEntityName(this.getInstanceContainerOfEntityById.bind(this), this.getAllChildInstanceContainersForParent.bind(this), this.#schema.getEntity.bind(this.#schema), normEntity, this.getAllInstanceContainersOfEntity(normEntity.name), this.#locales);
|
|
634
|
-
}
|
|
635
|
-
/**
|
|
636
|
-
* Retrieves an instance of the specified entity by its identifier.
|
|
637
|
-
*/
|
|
638
|
-
getSingletonInstanceOfEntity(entityName) {
|
|
639
|
-
return this.#data.getSingletonInstanceOfEntity(entityName);
|
|
640
|
-
}
|
|
641
|
-
/**
|
|
642
|
-
* Retrieves the instance container of the specified entity by its identifier.
|
|
643
|
-
*/
|
|
644
|
-
getSingletonInstanceContainerOfEntity(entityName) {
|
|
645
|
-
return this.#data.getSingletonInstanceContainerOfEntity(entityName);
|
|
646
|
-
}
|
|
647
|
-
/**
|
|
648
|
-
* Counts the number of instances registered for the specified entity.
|
|
649
|
-
*/
|
|
650
|
-
countInstancesOfEntity(entityName) {
|
|
651
|
-
return this.#data.countInstancesOfEntity(entityName);
|
|
652
|
-
}
|
|
653
|
-
/**
|
|
654
|
-
* Returns whether the singleton entity has an instance.
|
|
655
|
-
*/
|
|
656
|
-
hasInstanceOfSingletonEntity(entityName) {
|
|
657
|
-
return this.#data.hasInstanceOfSingletonEntity(entityName);
|
|
658
|
-
}
|
|
659
|
-
/**
|
|
660
|
-
* Retrieves overviews for all instances in the database, grouped by entity name.
|
|
661
|
-
*/
|
|
662
|
-
getAllInstanceOverviews() {
|
|
663
|
-
return getAllInstanceOverviewsByEntityName(this.#schema.getEntity.bind(this.#schema), this.#data, this.#locales);
|
|
664
|
-
}
|
|
665
|
-
/**
|
|
666
|
-
* Retrieves all instance containers of the specified child entity for a given parent instance.
|
|
667
|
-
*/
|
|
668
|
-
getAllChildInstanceContainersForParent(childEntityName, parentId) {
|
|
669
|
-
return this.#data.getAllChildInstanceContainersForParent(this.#schema.getEntity.bind(this.#schema), childEntityName, parentId);
|
|
670
|
-
}
|
|
671
|
-
/**
|
|
672
|
-
* Displays the name of an entity instance including its ID. If no display name is found, `undefined` is returned.
|
|
673
|
-
*/
|
|
674
|
-
getDisplayName(...args) {
|
|
675
|
-
return getDisplayName(this.#schema.getEntity.bind(this.#schema), this.#locales, this.#data, ...args);
|
|
676
|
-
}
|
|
677
|
-
/**
|
|
678
|
-
* Displays the name of an entity instance including its ID. If no display name is found, only the ID is returned.
|
|
679
|
-
*/
|
|
680
|
-
getDisplayNameWithId(...args) {
|
|
681
|
-
return getDisplayNameWithId(this.#schema.getEntity.bind(this.#schema), this.#locales, this.#data, ...args);
|
|
682
|
-
}
|
|
683
|
-
/**
|
|
684
|
-
* Checks if a given instance is referenced by other instances in the database.
|
|
685
|
-
*
|
|
686
|
-
* If `otherInstancesToDelete` is provided, those instances will be excluded from the check. This can be useful when planning to delete multiple instances at once and wanting to ignore references from those instances.
|
|
687
|
-
*/
|
|
688
|
-
isReferencedByOtherInstances(instance, otherInstancesToDelete) {
|
|
689
|
-
return isReferencedByOtherInstances(this.#referencesToInstances, instance, otherInstancesToDelete);
|
|
690
|
-
}
|
|
691
|
-
/**
|
|
692
|
-
* The configured locales for this TSONDB instance.
|
|
693
|
-
*/
|
|
694
|
-
get locales() {
|
|
695
|
-
return this.#locales;
|
|
696
|
-
}
|
|
697
|
-
/**
|
|
698
|
-
* Sets the locales for this TSONDB instance.
|
|
699
|
-
*
|
|
700
|
-
* The provided locales must exist in the database if a locale entity is defined in the schema.
|
|
701
|
-
*/
|
|
702
|
-
setLocales(locales) {
|
|
703
|
-
checkLocales(this.#schema, this.#data, locales);
|
|
704
|
-
this.#locales = locales;
|
|
705
|
-
}
|
|
706
|
-
/**
|
|
707
|
-
* The loaded schema for this TSONDB instance.
|
|
708
|
-
*/
|
|
709
|
-
get schema() {
|
|
710
|
-
return this.#schema;
|
|
310
|
+
typeof entityName === "string" ? this.schema.getEntity(entityName) : entityName, instanceId, newIndex));
|
|
711
311
|
}
|
|
712
312
|
/**
|
|
713
313
|
* The Git wrapper for this TSONDB instance, if a git repository is connected.
|
|
@@ -721,73 +321,6 @@ export class TSONDB {
|
|
|
721
321
|
isGitRepo() {
|
|
722
322
|
return this.#git !== undefined;
|
|
723
323
|
}
|
|
724
|
-
/**
|
|
725
|
-
* Searches for instances matching the provided query in their ID or display name.
|
|
726
|
-
*
|
|
727
|
-
* The results are sorted by relevance. A custom relevance score function can be provided.
|
|
728
|
-
*/
|
|
729
|
-
search(query, options = {}) {
|
|
730
|
-
if (!query || query.trim().length === 0) {
|
|
731
|
-
return [];
|
|
732
|
-
}
|
|
733
|
-
const { getRelevanceScore = ({ id, displayName }) => {
|
|
734
|
-
const searchableName = displayName.toLowerCase();
|
|
735
|
-
return id.startsWith(query) || searchableName.startsWith(lowerCaseQuery)
|
|
736
|
-
? 1
|
|
737
|
-
: id.includes(query) || searchableName.includes(lowerCaseQuery)
|
|
738
|
-
? 0.5
|
|
739
|
-
: 0;
|
|
740
|
-
}, } = options;
|
|
741
|
-
const lowerCaseQuery = query.toLowerCase();
|
|
742
|
-
return this.#data
|
|
743
|
-
.getAllInstances()
|
|
744
|
-
.flatMap(([entityName, instances]) => {
|
|
745
|
-
const entity = this.#schema.getEntity(entityName);
|
|
746
|
-
if (entity && isEntityDeclWithParentReference(entity)) {
|
|
747
|
-
return [];
|
|
748
|
-
}
|
|
749
|
-
return instances
|
|
750
|
-
.map((instance) => {
|
|
751
|
-
const { name, localeId } = getDisplayNameFromEntityInstance(
|
|
752
|
-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
753
|
-
entity, instance, this.#schema.getEntity.bind(this.#schema), this.getInstanceContainerOfEntityById.bind(this), this.getAllChildInstanceContainersForParent.bind(this), this.#locales);
|
|
754
|
-
return [
|
|
755
|
-
entityName,
|
|
756
|
-
{
|
|
757
|
-
id: instance.id,
|
|
758
|
-
displayName: name,
|
|
759
|
-
displayNameLocaleId: localeId,
|
|
760
|
-
relevance: getRelevanceScore({
|
|
761
|
-
entityName,
|
|
762
|
-
id: instance.id,
|
|
763
|
-
content: instance.content,
|
|
764
|
-
displayName: name,
|
|
765
|
-
displayNameLocaleId: localeId,
|
|
766
|
-
query,
|
|
767
|
-
queryLowerCase: lowerCaseQuery,
|
|
768
|
-
}),
|
|
769
|
-
},
|
|
770
|
-
];
|
|
771
|
-
})
|
|
772
|
-
.filter(instance => instance[1].relevance > 0);
|
|
773
|
-
})
|
|
774
|
-
.toSorted(reduceCompare(on(item => item[1].relevance, compareNumber), (a, b) => a[1].displayName.localeCompare(b[1].displayName, a[1].displayNameLocaleId)));
|
|
775
|
-
}
|
|
776
|
-
/**
|
|
777
|
-
* Retrieves the name of the entity for a given instance ID. If no instance with the provided ID is found, `undefined` is returned.
|
|
778
|
-
*/
|
|
779
|
-
getEntityNameOfInstanceId(id) {
|
|
780
|
-
return this.#data.getEntityNameOfInstanceId(id);
|
|
781
|
-
}
|
|
782
|
-
/**
|
|
783
|
-
* Compresses the entire database into a single JSON file at the specified path.
|
|
784
|
-
*
|
|
785
|
-
* Instead of storing each instance in its own file, this method serializes the entire database into a single JSON file. This can be useful for backup, transfer or archiving purposes. The resulting file contains all instances organized by entity names. No spaces or indentation are used in the JSON output to minimize file size.
|
|
786
|
-
* @param path The file path where the compressed database will be saved.
|
|
787
|
-
*/
|
|
788
|
-
async compressToSingleFile(path) {
|
|
789
|
-
await writeFile(path, JSON.stringify(this.#data), "utf-8");
|
|
790
|
-
}
|
|
791
324
|
/**
|
|
792
325
|
* Registers a new hook for the specified trigger. The callback will be invoked whenever the trigger is activated.
|
|
793
326
|
*/
|
|
@@ -807,5 +340,17 @@ export class TSONDB {
|
|
|
807
340
|
}
|
|
808
341
|
}
|
|
809
342
|
}
|
|
343
|
+
/**
|
|
344
|
+
* Compresses the entire database into a single JSON file at the specified path.
|
|
345
|
+
*
|
|
346
|
+
* Instead of storing each instance in its own file, this method serializes the entire database into a single JSON file. This can be useful for backup, transfer or archiving purposes. The resulting file contains all instances organized by entity names. No spaces or indentation are used in the JSON output to minimize file size.
|
|
347
|
+
* @param path The file path where the compressed database will be saved.
|
|
348
|
+
* @param indentation Optional. If provided, the JSON output will be formatted with the specified number of spaces for indentation. If not provided, the output will be minified without any extra spaces.
|
|
349
|
+
*/
|
|
350
|
+
async compressToSingleFile(path, indentation) {
|
|
351
|
+
await writeFile(path, JSON.stringify(createCompressedDatabaseFile(this._data, this.schema.version), undefined, indentation), "utf-8");
|
|
352
|
+
}
|
|
353
|
+
serve(config, callback) {
|
|
354
|
+
createServer(this, { serverOptions: config, validationOptions: this._validationOptions }, callback);
|
|
355
|
+
}
|
|
810
356
|
}
|
|
811
|
-
export { createServer } from "./server/index.js";
|